在計算機編程中,單元測試(Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。但是什么叫"程序單元"呢?是一個模塊、還是一個類、還是一個方法(函數)呢?不同的人、不同的語言,都有不同的理解。一般的定義,尤其是是在OOP領域,是一個類的一個方法。在此,我們也這樣理解:單元測試,是為了測試某一個類的某一個方法能否正常工作,而寫的測試代碼。
單元測試的三個步驟:
這里需要強調一個觀念,那就是單元測試只是測試一個方法單元,它不是測試一整個流程。舉個例子來說,一個Login頁面,上面有兩個輸入框和一個button。兩個輸入框分別用于輸入用戶名和密碼。點擊button以后,有一個UserManager會去執行performlogin操作,然后將結果返回,更新頁面。那么我們給這個東西做單元測試的時候,不是測這一整個login流程。這種整個流程的測試:給兩個輸入框設置正確的用戶名和密碼,點擊login button,最后頁面得到更新。叫做集成測試,而不是單元測試。當然,集成測試也是有他的必要性的,然而這不是每個程序員應該花多少精力所在的地方。為什么是這樣呢?因為集成測試設置起來很麻煩,運行起來很慢,在保證代碼質量、改善代碼設計方面更起不到任何作用,因此它的重要程度并不是那么高
Android中的單元測試分為兩種,Local Unit Tests 和 Instrumented Tests,前者運行在JVM,后者需要運行再Android設備
Local Unit Tests運行在本地JVM,不需要安裝APP,所以運行時間很快。也因此不能依賴Android的API,所以大多數時候需要用Mock的形式來做替換(后面會提到)
module-name/src/test/java
一般使用到的測試框架
使用Gradle添加相應的庫
dependencies {
// Required -- JUnit 4 framework
testCompile 'junit:junit:4.12'
// Optional -- Mockito framework
testCompile 'org.mockito:mockito-core:1.10.19'
}
這是Java界用的最廣泛,也是最基礎的一個框架,其他的很多框架,包括我們后面會看到的Mockito,都是基于或兼容JUnit4的。 使用比較簡單,最多的是其Assert
類提供的assertXXX
方法。
假設這樣的一個類需要測試
public class Calculator {
public int add(int one, int another) {
return one + another;
}
public int multiply(int one, int another) {
return one * another;
}
如果不使用單元測試框架,我們可能需要這樣來驗證這段代碼的正確性:
public class CalculatorTest {
public static void main(String[] args) {
Calculator calculator = new Calculator();
int sum = calculator.add(1, 2);
if(sum == 3) {
System.out.println("add() works!")
} else {
System.out.println("add() does not works!")
}
int product = calculator.multiply(2, 4);
if (product == 8) {
System.out.println("multiply() works!")
} else {
System.out.println("multiply() does not works!")
}
}
}
然后我們再通過某種方式,比如命令行或IDE,運行這個CalculatorTest
的main
方法,在看著terminal的輸出,才知道測試是通過還是失敗。想一下,如果我們有很多的類,每個類都有很多方法,那么就要寫一堆這樣的代碼,每個類對于一個含有main方法的test類,同時main方法里面會有一堆代碼。這樣既寫起來痛苦,跑起來更痛苦,比如說,你怎么樣一次性跑所有的測試類呢?所以,一個測試框架為我們做的最基本的事情,就是允許我們按照某種更簡單的方式寫測試代碼,把每一個測試單元寫在一個測試方法里面,然后它會自動找出所有的測試方法,并且根據你的需要,運行所有的測試方法,或者是運行單個測試方法,或者是運行部分測試方法等等。 對于上面的例子,如果使用Junit的話,我們可以按照如下的方式寫測試代碼:
public class CalculatorTest {
Calculator mCalculator;
@Before
public void setup() {
mCalculator = new Calculator();
}
@Test
public void testAdd() throws Exception {
int sum = calculator.add(1, 2);
Assert.assertEquals(3, sum);
}
@Test
public void testMultiply() throws Exception {
int product = calculator.multiply(2, 4);
Assert.assertEquals(8, product);
}
}
上面的@Before
修飾的方法,會在測試開始前調用,這里是新建了一個Calculator
對象,所以之后的一些測試單元都可以直接使用這個實例,@Test
修飾的方法,是用來需要測試的單元,例如testAdd
方法是用來測試Calculator
類的加法操作,測試單元內使用Assert
類提供的assertXXX
方法來驗證結果。如果還有其他的測試方法,則以此類推。 另外還有一些可能需要用到的修飾符,如@After
,@BeforeClass
,@AfterClass
等。
Mockito的兩個重要的功能是,驗證Mock對象的方法的調用和可以指定mock對象的某些方法的行為。(對于不懂Mock概念的同學來說,第一次看到的確很可能很難理解)
這是項目中的一個例子:
/**
* @param <T> 用于過濾的實體類型
*/
public interface BaseItemFilter<T> {
/**
* @param item
* @return true:不過濾;false:需要過濾
*/
boolean accept(T item);
}
BaseItemFilter
是用來判斷某種指定類型的實體是否需要過濾的,類似java中的FileFilter
,目的是為了用了過濾不符合要求的實體。
以下是我們的關鍵服務過濾器的實現:
public class EssentialProcessFilter implements BaseItemFilter<RunningAppBean> {
/**
* 系統關鍵進程及用戶主要的進程
*/
private static HashSet<String> sCoreList = new HashSet<String>();
/**
* 加載系統核心進程列表
* @param context
*/
public static void loadCoreList(Context context) {
if (sCoreList.isEmpty()) {
final Resources r = context.getResources();
String[] corePackages = r.getStringArray(R.array.default_core_list);
Collections.addAll(sCoreList, corePackages);
}
}
@Override
public boolean accept(RunningAppBean appModle) {
return appModle != null && !(isEssentialProcess(appModle.mPackageName) || isEssentialProcessMock(appModle.mPackageName, appModle.mIsSysApp));
}
/**
* 判斷進程是否屬于重要進程
* @param process
* @return
*/
public static boolean isEssentialProcess(String process) {
return sCoreList.contains(process);
}
/**
* 系統關鍵進程關鍵詞模糊匹配
* @param packageName
* @param isSystemApp
* @return
*/
public static boolean isEssentialProcessMock(String packageName, boolean isSystemApp) {
return 省略...額外的一些判斷;
}
}
可以看到,這里的關鍵服務的判斷的判斷規則可以分兩部分,一個是從String.xml
中預設的一段Arrays
數組查找是否右符合的,這個需要在初始化或某個時機預先調用EssentialProcessFilter#loadCoreList(Context context)
方法來加載,另外的一個判斷是在EssentialProcessFilter#isEssentialProcessMock
方法中定義,這個類中accept
方法,定義了只要符合其中一種規則,那么我們就需要把它過濾。
這個時候我們來寫單元測試,你一開始就會發現你沒有辦法新建一個Context
對象來讀取String.xml
,即使你想盡任何方法新建一個ContextImpl
實例,最后你還是會出錯的,主要原因再在于Gradle運行Local Unit Test 所使用的android.jar
里面所有API都是空實現,并拋出異常的。 現在想想,我們實際上并不需要真的讀取String.xml
,我們需要驗證的是記錄在我們的關鍵列表集合是否生效,既然這樣,我們前面說過了,Mockito的兩個重要的功能是,驗證Mock對象的方法的調用和可以指定mock對象的某些方法的行為。我們是否可以Mock一個Context
對象并且指定它讀取String.xml
的行為?答案是可以的,如下就是使用Mockito的一段測試代碼
public class TestListFilter2 {
@Mock
Context mContext;
@Mock
Resources mResources;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
Mockito.when(mContext.getResources()).thenReturn(mResources);
Mockito.when(mResources.getStringArray(R.array.default_core_list)).thenReturn(getEssentialProcessArray());
//模擬加載XML資源
EssentialProcessFilter.loadCoreList(mContext);
}
/**
* 測試關鍵服務的過濾器
*/
@Test
public void testEssentialFilter() {
EssentialProcessFilter processFilter = new EssentialProcessFilter();
ListFilter<RunningAppBean> listFilter = Mockito.spy(ListFilter.class);
listFilter.addFilter(processFilter);
List<RunningAppBean> list = new ArrayList<RunningAppBean>();
list.addAll(getEssentialAppBean());
list.addAll(getNormalRunningApp());
List<RunningAppBean> result = Mockito.mock(ArrayList.class);
for (RunningAppBean runningAppBean : list) {
if (listFilter.accept(runningAppBean)) {
result.add(runningAppBean);
}
}
Mockito.verify(listFilter, Mockito.times(list.size())).accept(Mockito.any(RunningAppBean.class));
Mockito.verify(result, Mockito.times(getNormalRunningApp().size())).add(Mockito.any(RunningAppBean.class));
}
/**
* 關鍵服務應用包名
*/
public String[] getEssentialProcessArray() {
return new String[]{"android.process.acore", "android.process.media", "android.tts", "android.uid.phone", "android.uid.shared", "android.uid.system"};
}
}
上面的代碼,我們使用@Mock
來Mock了Context和Resource對象,這需要我們在setup
的時候使用MockitoAnnotations.initMocks(this)
方法來使得這些注解生效,如果再不使用@Mock
注解的時候,我們還可以使用Mockito.mock
方法來Mock對象。這里我們指定了Context
對象在調用getResources
方法的時候返回一個同樣是Mock的Resources
對象,這里的Resources
對象,指定了在調用getStringArray(R.array.default_core_list)
方法的時候返回的字符串數組的數據是通過我們的getEssentialProcessArray
方法獲得的,而不是真的是加載String.xml
資源。最后調用EssentialProcessFilter.loadCoreList(mContext)
方法使得EssentialProcessFilter
內記錄的關鍵服務集合的數據源就是我們指定的。目前,我們使用的就是改變Mock對象的行為的功能。
在測試單元testEssentialFilter
方法中,使用Mockito.spy(ListFilter.class)
來Mock一個ListFilter
對象(這是一個BaseItemFilter
的實現,里面記錄了BaseItemFilter
的集合,用了記錄一系列的過濾規則),這里使用spy
方法Mock出來的對象除非指定它的行為,否者調用這個對象的默認實現,而使用mock
方法Mock出來的對象,如果不指定對象的行為的話,所有非void方法都將返回默認值:int、long類型方法將返回0,boolean方法將返回false,對象方法將返回null等等,我們也同樣可以使用@spy
注解來Mock對象。這里的listFilter
對象使用spy
是為了使用默認的行為,確保accept
方法的邏輯正確執行,而result
對象使用mock
方式來Mock,是因為我們不需要真的把過濾后的數據添加到集合中,而只需要驗證這個Mock對象的add
方法調用的多少次即可。
最后就是對Mock對象的行為的驗證,分別驗證了listFilter#accept
方法和result#add
方法的執行次數,其中Mockito#any
系列方法用來指定無論傳入任何參數值。
這里舉一個新的例子,在使用MVP模式開發一個登錄頁
這是我們的LoginPresenter
,Presenter
的職責是作為View
和Model
之間的橋梁,UserManager
就是這里的Model
層,它用來處理用戶登錄的業務邏輯
public class LoginPresenter {
private final LoginView mLoginView;
private UserManager mUserManager = new UserManager();
public LoginPresenter(LoginView loginView) {
mLoginView = loginView;
}
public void login(String username, String password) {
//....
mLoginView.showLoginHint();
mUserManager.performLogin(username, password);
//...
}
這段代碼存在了一些問題
UserManager
生成方式,如需要用new UserManager(String config)
初始化,需要修改LoginPresenter
代碼UserManager
對象對LoginPresenter
的影響很困難,因為UserManager
的初始化被寫死在了LoginPresenter
的構造函數中(現在的Mockito可以使用@InjectMocks
來很大程度緩解這個問題)為了把依賴解耦,我們一般可以作如下改變
public class LoginPresenter {
private final LoginView mLoginView;
private final UserManager mUserManager;
//將UserManager作為構造方法參數傳進來
public LoginPresenter(LoginView loginView,UserManager userManager) {
this.mLoginView = loginView;
this.mUserManager = userManager;
}
public void login(String username, String password) {
//... some other code
mUserManager.performLogin(username, password);
}
}
這就是一種常見的依賴注入方式,這種方式的好處是,依賴關系非常明顯。你必須在創建這個類的時候,就提供必要的dependency。這從某種程度上來說,也是在說明這個類所完成的功能。實現依賴注入很簡單,比較有名的Dagger、Dragger2這些框架可以讓這種實現變得更加簡單,簡潔,優雅,有興趣可以自行了解。 作出這種修改之后,我們就可以很容易的Mock出UserManager
對象來對LoginPresenter
做單元測試
很多時候,我們耗時的業務邏輯都是再異步線程中處理的,這樣就會對我們的測試造成了一些困難,因為很可能我們的異步任務執行完之前,我們的Test單元已經執行完了,為了能夠順利驗證,其中一個思路是,首先需要一個回調接口來告訴我們處理完成,之后再我們知道回調后,繼續執行測試代碼,這里的一個簡單實現就是使用CountDownLatch
,也可以使用wait
和notifyAll
,下面以CountDownLatch
為例子
這里是我們的回調接口
public interface ScanListener<T> {
/**
* @param t 掃描結果
*/
void onScannedCompleted(T t);
}
測試代碼如下:
public class TestRunningAppScanner {
CountDownLatch mSignal = null;
RunningAppScanner mRunningAppScanner;
@Mock
BaseScanner.ScanListener<List<RunningAppBean>> mRunningAppBeanScanListener;
@Captor
ArgumentCaptor<List<RunningAppBean>> mListArgumentCaptor;
@Before
public void setUp() {
mSignal = new CountDownLatch(1);
MockitoAnnotations.initMocks(this);
mRunningAppScanner = Mockito.spy(new RunningAppScanner(InstrumentationRegistry.getTargetContext()));
}
@After
public void tearDown() {
mSignal.countDown();
}
@Test
public void testRunningApp() throws InterruptedException {
Assert.assertFalse(mRunningAppScanner.isRunning());
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
mSignal.countDown();
return invocation.getArguments();
}
}).when(mRunningAppBeanScanListener).onScannedCompleted((List<RunningAppBean>) Mockito.any());
mRunningAppScanner.setScanListener(mRunningAppBeanScanListener);
mRunningAppScanner.startScanning();
Assert.assertTrue(mRunningAppScanner.isRunning());
mSignal.await();
Assert.assertFalse(mRunningAppScanner.isRunning());
Mockito.verify(mRunningAppBeanScanListener, Mockito.times(1)).onScannedCompleted(mListArgumentCaptor.capture());
//必定最少有1個進程再運行
Assert.assertTrue(mListArgumentCaptor.getValue().size() > 0);
}
}
上面代碼中Mock了一個ScanListener
類型的回調接口,并在testRunningApp
方法中,指定了它onScannedCompleted
方法的行為,RunningAppScanner#startScanning
方法是用來啟動異步邏輯的,在開始異步邏輯之前設置了回調接口,在開始異步邏輯后,就使用了mSignal.await()
方法來阻塞當前線程,直到onScannedCompleted
方法的執行,會調用到在setup的時候新建的CountDownLatch
對象的countDown()
,表明異步任務的完成,以繼續執行下面的測試代碼
上面的測試代碼實際上是屬于Instrumented Tests
,它的異步邏輯是去掃描當前設備運行的進程,需要運行再Android設備上,它的異步邏輯是實際運行的而且會掃描出真實結果。但有些時候,我們只是想驗證狀態和交互結果,而不需要它真的執行異步邏輯,我們可以使用其他的方式來進行測試,我們以一個登錄流程為例子
這是登錄流程的時序圖
回調的定義
public interface LoginCallback {
void onSuccess();
void onFail(int code);
}
這是LoginPresenter
public class LoginPresenter {
private final LoginView mLoginView;
private final UserManager mUserManager;
//將UserManager作為構造方法參數傳進來
public LoginPresenter(LoginView loginView, UserManager userManager) {
this.mLoginView = loginView;
this.mUserManager = userManager;
}
public void login(String username, String password) {
mLoginView.showLoginHint();
mUserManager.performLogin(username, password, new UserManager.LoginCallback() {
@Override
public void onSuccess() {
mLoginView.showSuccess();
}
@Override
public void onFail(int code) {
mLoginView.showError();
}
});
}
}
我們現在需要對LoginPresenter
進行測試,我們的測試點在于接收到不同的回調結果的時候,對View進行不同的展示,至于UserManager
的測試則應該是另外的一個測試單元的 LoginPresenter
的測試代碼
public class TestDemo {
@Mock
LoginView mLoginView;
@Mock
UserManager mUserManager;
LoginPresenter mLoginPresenter;
@Captor
ArgumentCaptor<LoginCallback> mCallbackArgumentCaptor;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
mLoginPresenter = new LoginPresenter(mLoginView, mUserManager);
}
@Test
public void showError() {
mLoginPresenter.login("username", "password");
Mockito.verify(mUserManager, Mockito.times(1)).performLogin(Mockito.anyString(), Mockito.anyString(), mCallbackArgumentCaptor.capture());
mCallbackArgumentCaptor.getValue().onFail(0);
Mockito.verify(mLoginView, Mockito.times(1)).showError();
}
@Test
public void showSuccess() {
mLoginPresenter.login("username", "password");
Mockito.verify(mUserManager, Mockito.times(1)).performLogin(Mockito.anyString(), Mockito.anyString(), mCallbackArgumentCaptor.capture());
mCallbackArgumentCaptor.getValue().onSuccess();
Mockito.verify(mLoginView, Mockito.times(1)).showSuccess();
}
}
這里的關鍵是@Captor ArgumentCaptor<LoginCallback> mCallbackArgumentCaptor;
,ArgumentCaptor是作用是用來捕獲所傳入方法特定的參數,然后可以進一步對參數進行斷言,而且是在方法調用之后使用。這里我們捕獲到UserManager#performLogin
的第三個LoginCallback
的參數,然后直接使用它來做相應回調來驗證不同情況下的反饋結果是否正確
使用Mockito并不可以Mock對象的靜態方法、private修飾的方法、static方法、構造函數等,使用JMockit或PowerMock是可以解決這樣的問題,有時間的話可以去實踐下
Instrumented Unit tests是需要運行再Android設備上的(物理/虛擬),通常我們使用Mock的方式不能很好解決對Android的API的依賴的這個問題,而使用這種測試方式可以依賴Android的API,使用Android提供的Instrumentation系統,將單元測試代碼運行在模擬器或者是真機上,但很多情況來說,我們還是會需要和Mockito一起使用的。這中方案速度相對慢,因為每次運行一次單元測試,都需要將整個項目打包成apk,上傳到模擬器或真機上,就跟運行了一次app似得,所以。
module-name/src/androidTests/java/
一般使用到的測試框架
通過Gralde添加相應的庫
dependencies {
androidTestCompile 'com.android.support:support-annotations:24.0.0'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
// Optional -- Hamcrest library
androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
// Optional -- UI testing with Espresso
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
// Optional -- UI testing with UI Automator
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
}
另外還需要在你的App的模塊的build.gralde
文件添加如下設置:
android {
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
AndroidJUnitRunner是JUnit4運行在Android平臺上的兼容版本,使用也是很簡單的且方法類似,以上面測試異步任務為例子
@RunWith(AndroidJUnit4.class)
@MediumTest
public class TestRunningAppScanner {
CountDownLatch mSignal = null;
RunningAppScanner mRunningAppScanner;
@Mock
BaseScanner.ScanListener<List<RunningAppBean>> mRunningAppBeanScanListener;
@Before
public void setUp() {
mSignal = new CountDownLatch(1);
MockitoAnnotations.initMocks(this);
mRunningAppScanner = Mockito.spy(new RunningAppScanner(InstrumentationRegistry.getTargetContext()));
}
@After
public void tearDown() {
mSignal.countDown();
}
@Test
public void testRunningApp() throws InterruptedException {
Assert.assertFalse(mRunningAppScanner.isRunning());
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
mSignal.countDown();
return invocation.getArguments();
}
}).when(mRunningAppBeanScanListener).onScannedCompleted((List<RunningAppBean>) Mockito.any());
mRunningAppScanner.setScanListener(mRunningAppBeanScanListener);
mRunningAppScanner.startScanning();
Assert.assertTrue(mRunningAppScanner.isRunning());
mSignal.await();
Assert.assertFalse(mRunningAppScanner.isRunning());
Mockito.verify(mRunningAppBeanScanListener, Mockito.times(1)).onScannedCompleted((List<RunningAppBean>) Mockito.any());
}
}
需要在測試類的定義上加上@RunWith(AndroidJUnit4.class)
標注,另外@MediumTest
標注是用來指定測試規模,有三種類型可以指定,限制如下,因為這里的異步任務的目的是掃描所有正在運行的App,所以這里使用@MediumTest
,其他的Assert#XXX
、@Befor
,@After
使用一致,另外這里還搭配Mockito使用。因為需要真正用到系統的一些服務(AMS,PKMS)或資源,這里的InstrumentationRegistry
可以為我們提供Instrumentation
對象,測試App的Context
對象,目標App的Context
對象和通過命令行啟動測試所提供的參數信息。這里的 InstrumentationRegistry.getTargetContext()
就是用來獲取目標App的Context
對象。
Feature | SmallTest | MediumTest | LargeTest |
---|---|---|---|
Network | No | localhost only | Yes |
Database | No | Yes | Yes |
File system access | No | Yes | Yes |
Use external systems | No | Discouraged | Yes |
Multiple threads | No | Yes | Yes |
Sleep statements | No | Yes | Yes |
System properties | No | Yes | Yes |
Execution Time (ms) | 200< | 1000< | >1000 |
Espresso提供大量的方法用來進行UI測試,這些API可以讓你寫的簡潔和可靠的自動化UI測試,站在用戶的角度測試,所有邏輯是黑盒
Espresso的三個重要功能:
使用流程
public static ViewInteraction onView(final Matcher<View> viewMatcher) {}
Espresso.onView
方法接收一個Matcher<View>
類型的入參,并返回一個ViewInteraction
對象。ViewMatchers
對象提供了大量的withXXX
方法用來定位元素,常用的有withId
,withText
和一系列的isXXX
方法用來判斷元素的狀態。如果單一的匹配條件無法精確地匹配出來唯一的控件,我們可能還需要額外的匹配條件,此時可以用AllOf#allOf()
方法來進行復合匹配條件的構造(下面的AdapterView節有使用到):
onView(allOf(withId(id), withText(text)))
當定位到元素后,返回一個ViewInteraction
對象,其perform
方法可以接收一系列ViewAction
用來進行模擬用戶操作,ViewActions
類提供了大量的操作實現,常用的有typeText
,click
等
public ViewInteraction perform(final ViewAction... viewActions) {}
最后為了驗證操作是否符合預期,我們還是需要定位到元素,獲得一個ViewInteraction
對象,其check
方法接收了一個ViewAssertion
的入參,該入參的作用就是檢查結果是否符合我們的預期。
public ViewInteraction check(final ViewAssertion viewAssert) {}
ViewAssertion
提供如下方法,這個方法接收了一個匹配規則,然后根據這個規則為我們生成了一個ViewAssertion
對象,這個Matcher<View>
的如參和定位元素的時候是一個用法的
public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {}
下面是測試主頁部分UI測試的代碼
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityUITest {
/**
* {@link ActivityTestRule} is a JUnit {@link Rule @Rule} to launch your activity under test.
* <p/>
* Rules are interceptors which are executed for each test method and are important building
* blocks of Junit tests.
*/
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class, true, false);
//默認在測試之前啟動該Activity
// public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class);
@Test
public void testNavigateToAdvanceFragment() {
Intent intent = new Intent();
//攜帶信息的方式啟動Activity
intent.putExtra("EXTRA", "Test");
mActivityRule.launchActivity(intent);
// Open Drawer to click on navigation.
Espresso.onView(ViewMatchers.withId(R.id.drawer_layout))
.check(ViewAssertions.matches(DrawerMatchers.isClosed(Gravity.LEFT)))
.perform(DrawerActions.open()); // Open Drawer
// navigate to advance fragment.
Espresso.onView(ViewMatchers.withText(R.string.advanced_settings_group_title_advanced_features))
.perform(ViewActions.click());
// Left Drawer should be closed.
Espresso.onView(ViewMatchers.withId(R.id.drawer_layout))
.check(ViewAssertions.matches(DrawerMatchers.isClosed(Gravity.LEFT)));
Espresso.onView(ViewMatchers.withText(R.string.fingerprint_settings_title)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
}
}
@Rule
+ActivityTestRule
用來標記需要測試的Activity,測試框架會再運行@Test標注的測試用例之前會啟動這個指定的Activity(類似還有IntentsTestRule
,ServiceTestRule
分別用于測試Intent和Service),有些情況我們測試的Actvity
需要再Intent
那里攜帶一些信息,這里也是可以通過ActivityTestRule
的不同構造函數+ActivityTestRule#launchActivity
方法來完成
它的作用是找到R.id.drawer_layout
的View,這是一個DrawerLayout
,DrawerMatchers
提供了用于判斷DrawerLayout
的狀態,DrawerActions
類提供了用于操作DrawerLayout
的操作。首先是判斷DrawerLayout
是隱藏的,然后打開抽屜,并找到R.string.advanced_settings_group_title_advanced_features
標記的Item,進行一次點擊操作,最后驗證當前顯示的是高級界面
不同于之前提到的靜態的控件,AdapterView
在加載數據時,可能只有一部分顯示在了屏幕上,對于沒有顯示在屏幕上的那部分數據,我們通過Espresso.onView()
是沒有辦法找到的。所以需要使用Espresso.onData()
,為了精確定位,通常還需要我們自定義Matcher
基本流程的區別在于元素定位上:
public static DataInteraction onData(Matcher<? extends Object> dataMatcher) {}
這個方法接收一個Matcher<? extends Object>
類型的入參,并返回一個DataInteraction
對象。DataInteraction
關注于AdapterView的數據。由于AdapterView的數據源可能很長,很多時候無法一次性將所有數據源顯示在屏幕上,因此我們主要先關注AdapterView中包含的數據,而非一次性就進行視圖元素的匹配,入參Matcher<? extends Object>
構造一個針對于Object(數據)匹配的匹配規則。從以上對比可以看出,我們在使用onData()
方法對AdapterView進行測試的時候,我們的思路就轉變成了首先關注這個AdapterView的具體數據,而不是UI上呈現的內容。當然,我們最終的目標還是要找到目標的UI元素,但是我們是通過其數據源來進行入手的
自定義Matcher
對于針對于Object(數據)的匹配器一般都需要我們自定義的,如下是針對AdapterView的數據是LockerItem
類型的匹配器,用于LockerItem
對象的title
字段是否為輸入的字符串,復寫的matchesSafely()
方法是真正執行匹配的地方了:
/**
* 查找指定關鍵字的搜索條件
* @param name 需要搜索的關鍵字
*/
public static Matcher<Object> lockItemMatcher(final String name) {
return new BoundedMatcher<Object, LockerItem>(LockerItem.class) {
@Override
protected boolean matchesSafely(LockerItem item) {
return item != null && item.getTitle().equals(name);
}
@Override
public void describeTo(Description description) {
description.appendText("SearchItem has Name: " + name);
}
};
}
指定AdapterView
通常還需要我們指定AdapterView,避免存在多個AdapterView的時候所帶來的麻煩,通過inAdapterView
方法來匹配,匹配規則和之前的視圖匹配一樣,可以同id來進行匹配
public DataInteraction inAdapterView(Matcher<View> adapterMatcher){}
Child View
有時候我們需要對一個View中的某個子控件進行操作(比如點擊一個ListView條目中的某個指定Button),這時我們可以通過`onChildView``方法指定相應的子控件
DataInteraction onChildView(Matcher<View> childMatcher) {}
操作元素
同處理普通元素差不多
同處理普通元素差不多
下面是一個找到id為R.id.main_list
和數據類型為LockerItem
的AdapterView中標題為WeChat
的元素,并對該元素中id為R.id.item_ripple
的子元素模擬一次點擊事件
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityUITest {
/**
* {@link ActivityTestRule} is a JUnit {@link Rule @Rule} to launch your activity under test.
* <p/>
* Rules are interceptors which are executed for each test method and are important building
* blocks of Junit tests.
*/
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class, true, false);
@Test
public void testLockFirstItem() throws InterruptedException {
Intent intent = new Intent();
intent.putExtra("EXTRA", "Test");
mActivityRule.launchActivity(intent);
onData(allOf(is(instanceOf(LockerItem.class)),lockItemMatcher("WeChat"))).inAdapterView(withId(R.id.main_list)).onChildView(withId(R.id.item_ripple)).perform(click());
//...
}
/**
* 查找指定關鍵字的搜索條件
* @param name 需要搜索的關鍵字
*/
public static Matcher<Object> lockItemMatcher(final String name) {
return new BoundedMatcher<Object, LockerItem>(LockerItem.class) {
@Override
protected boolean matchesSafely(LockerItem item) {
return item != null && item.getTitle().equals(name);
}
@Override
public void describeTo(Description description) {
description.appendText("SearchItem has Name: " + name);
}
};
}
}
Google官方的Android Architecture Blueprints這個項目除了是Google利用不同的流行的開源庫組合來搭建MVP框架的實踐,同時還有完整的測試用例,所以學習價值很高,所以如果沒了解過,現在就去看看吧,必定受益匪淺
這是這個項目對于MVP模式的各層做測試的各種測試框架使用
原文轉自:http://www.jianshu.com/p/00ab03f3d394