Android 中構(gòu)建快速可靠的 UI 測試
前言
讓我一起來看看 Iván Carballo和他的團隊是如何使用Espresso, Mockito 和Dagger 2 編寫250個UI測試,并且只花了三分鐘就運行成功的。
在這篇文章中,我們會探索如何使用Mockito(譯者注:Mockito是java編寫的一個單元測試框架),Dagger 2 去創(chuàng)建快速可靠的Android UI測試。如果你正在開始編寫Android中的UI 測試或者希望改善已有測試性能的開發(fā)者,那么這篇文章值得一讀。
我第一次在安卓應用中使用UI自動化測試是在幾年前使用Robotium(譯者注:Robotium是android中的一個自動化測試框架)。我認為測試環(huán)境越逼真越好。在最終測試中應當表現(xiàn)得如同超人一般能夠迅速的點擊任意一個位置而且并不會報錯,對吧?我認為mocking測試很糟糕。為什么我們需要在測試的時候改變應用的行為?那不是欺騙嗎?幾個月后我們有了大概100個測試用例要花費40分鐘去運行起來。它們是如此的不穩(wěn)定,即使應用的功能上并沒有任何錯誤,通常有一大半的幾率會運行失敗。我們花了大量的時間去編寫它們,但是這些測試用例卻沒有幫我們找到任何問題。
但正如John Dewey所說,失敗是具有啟發(fā)意義的。
失敗是有啟發(fā)意義的。智者總能從失敗和成功中學到同樣多的東西。
我們確實學到。我們認識到在測試中依賴于真實的API 接口是一個糟糕的做法。因為你失去了對返回的數(shù)據(jù)結(jié)果的控制,你也就不能對你的測試做預先處理。也就是說網(wǎng)絡錯誤和外部API接口錯誤都會導致你的測試出錯。如果你的wifi出錯了,你肯定不希望你的測試也會跟著出錯。你當然希望這時UI測試能夠成功運行。如果你還依賴外部的API接口那么你完全是在做集成測試(integration tests),也就得不到我們期望的結(jié)果。
Mock測試正式解決之道
(Mocking is the solution)
Mock 測試也就是通過一個模擬(mock)的對象去替換一個真實的對象以便于測試。它主要應用于編寫單元測試,但在UI測試中也會非常有用。你可以參照不同的方法去模擬java對象但使用Mockito 確實是一個簡單有效的解決方案。在下面的例子中你可以看到一個模擬的UserApi 類并且stub(譯者注:stub,也即“樁”,主要出現(xiàn)在集成測試的過程中,從上往下的集成時,作為下方程序的替代。可以理解為對方法進行預先的處理,達到修改的效果。下文中不做翻譯)了其中的一個方法,因此它總會返回一個用戶名username的靜態(tài)數(shù)組。
class UsersApi {
String[] getUserNames() { }
}
// Create the mock version of a UsersApi class
UsersApi mockApi = Mockito.mock(UsersApi.class);
// Stub the getUserNames() method
when(mockApi.getUserNames())
.thenReturn(new String[]{"User1", "User2", "User3"});
// The call below will always return an array containing the
// three users named above
mockApi.getUserNames();
一旦你創(chuàng)建了一個mock對象你需要確保應用測試的時候使用的是這個模擬的對象,并且在運行的時候使用的是真實對象。這也是一個難點所在,如果你的代碼構(gòu)建得并不是易于測試(test-friendly)的,替換真實對象的過程會變得異常艱難甚至是說不可能完成。還要注意的是,你想要模擬的代碼必須獨立到一個單獨的類里面。比如說,如果你直接從你的activity中使用HttpURLConnection調(diào)用REST API 進行數(shù)據(jù)訪問(我希望你不要這么做), 這個操作過程模擬起來也就會非常困難。
在測試之前考慮一下系統(tǒng)架構(gòu),糟糕的系統(tǒng)架構(gòu)往往會導致測試用例和mock測試難于編寫,mock測試也會變得不穩(wěn)定。
一個易于測試的架構(gòu)
A test friendly architecture
構(gòu)建一個易于測試的架構(gòu)有許多種方式。在這里我將使用 ribot 中使用的架構(gòu) (譯者注:也就是在開篇提到的Android應用架構(gòu))作為范例,你也可以應用這樣的架構(gòu)方式到任何架構(gòu)中。我們的架構(gòu)是基于MVP模式,我們決定在UI測試中去模擬(mock)整個Model層,因此我們可以對數(shù)據(jù)由更多的操作性,也就能夠?qū)懗龈袃r值和可靠的測試。

MVP架構(gòu)
DataManager是Model層中唯一暴露給Presenter層的數(shù)據(jù)的類,因此為了測試Model層我們只需要替換為一個模擬
的DataManger即可。
使用Dagger注入模擬的DataManager
Using Dagger to inject a mock DataManager
一旦我們明確了需要模擬什么對象,那么接下來就該考慮在測試中如何替換真實的對象。我們通過Dagger2 解決這個問題(一個Android中的依賴注入框架),如果你還沒有接觸過Dagger ,在繼續(xù)閱讀下去之前我建議你閱讀使用Dagger2 進行依賴注入【英】 。我們的應用至少包含一個Dagger 的Module和Component。通常被叫做ApplicationComponent 和ApplicationModule。你可以在下面看到一個簡化版的只提供了DataManger實例的類。當然你也可以采用第二種方法,在DataManager的構(gòu)造函數(shù)上使用@inject注解。這里我直接提供一個方法便于理解。(譯者注:這里將兩個類ApplicationComponent 和ApplicationModule寫在一起,便于直觀理解)
@Module
public class ApplicationModule {
@Provides
@Singleton
public DataManager provideDataManager() {
return mDataManager;
}
}
@Singleton
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {
DataManager dataManager();
}
應用的ApplicationComponent 在Application類中初始化:
public class MyApplication extends Application {
ApplicationComponent mApplicationComponent;
public ApplicationComponent getComponent() {
if (mApplicationComponent == null) {
mApplicationComponent = DaggerApplicationComponent.builder()
.applicationModule(new ApplicationModule(this))
.build();
}
return mApplicationComponent;
}
// Needed to replace the component with a test specific one
public void setComponent(ApplicationComponent applicationComponent) {
mApplicationComponent = applicationComponent;
}
}
如果你使用過Dagger2,你可能有同樣的配置步驟,現(xiàn)在的做法是創(chuàng)建一個test的時候需要用到的Module和Component
@Module
public class TestApplicationModule {
// We provide a mock version of the DataManager using Mockito
@Provides
@Singleton
public DataManager provideDataManager() {
return Mockito.mock(DataManager.class);
}
}
@Singleton
@Component(modules = TestApplicationModule.class)
public interface TestComponent extends ApplicationComponent {
// Empty because extends ApplicationComponent
}
上面的TestApplicationModule使用Mockito提供了模擬的DataManger對象,TestComponent是ApplicationComponent的繼承類,使用了TestApplicationModule作為module,而不是ApplicationModule。這也就意味著如果我們在我們的Application類中初始化TestComponent會使用模擬的DataManager對象。
創(chuàng)建JUnit,并且設定TestComponent
Creating a JUnit rule that sets the TestComponent
為了確保在每次測試前TestComponent被設置到Application類中,我們可以創(chuàng)建JUnit 4 的 TestRule
public class TestComponentRule implements TestRule {
private final TestComponent mTestComponent;
private final Context mContext;
public TestComponentRule(Context context) {
mContext = context;
MyApplication application = (MyApplication) context.getApplicationContext();
mTestComponent = DaggerTestComponent.builder()
.applicationTestModule(new ApplicationTestModule(application))
.build();
}
public DataManager getMockDataManager() {
return mTestComponent.dataManager();
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
MyApplication application = (MyApplication) context.getApplicationContext();
// Set the TestComponent before the test runs
application.setComponent(mTestComponent);
base.evaluate();
// Clears the component once the tets finishes so it would use the default one.
application.setComponent(null);
}
};
}
}
TestComponentRule將會創(chuàng)建TestComponent的實例對象,這也就會覆寫apply方法并返回一個新的 Statement,新的Statement會:
1 設定TestComponent給Application類的component對象。
2調(diào)用基類的Statement 的evaluate()方法(這是在test的時候執(zhí)行)
3 設置Application的component字段為空,也就讓其恢復到初始狀態(tài)。我們能夠通過這種方式預防測試用例之間的相互影響
通過上面的代碼我們可以通過getMockDataManager()方法獲取模擬的DataManager對象。這也就允許我們能夠給得到DataManager對象并且stub它的方法。需要注意的是,這只有TestApplicationComponent的provideDataManger方法使用@Singleton注解的時候有效。如果它沒有被指定為單例的,那么我們通過getMockDataManager方法得到的實例對象將會不同于應用使用的實例對象。因此,我們也不可能stub它。
編寫測試用例
Writing the tests
現(xiàn)在我們有Dagger正確的配置,并且TestComponentRule也可以使用了,我們還有一件事要做,那就是編寫測試用例。我們使用 Espresso編寫UI測試。它并不是完美的但是它是一個快速可靠的Android測試框架。在編寫測試用例之前我們需要一個app去測試。假如我們有一個非常簡單的app,從REST API 中加載用戶名,并且展示到RecyclerView上面。那么DataManger將會是下面這個樣子:
public DataManager {
// Loads usernames from a REST API using a Retrofit
public Single<List<String>> loadUsernames() {
return mUsersService.getUsernames();
}
}
loadUsername()方法使用Retrofit和Rxjava 去加載REST API 的數(shù)據(jù)。它返回的是Single 對象,并且發(fā)送一串字符串。 我們也需要一個Activity展示用戶名usernames到RecyclerView上面,我們假設這個Activity叫做UsernamesActivity。如果你遵循MVP模式你也會有相應的presenter但為了直觀理解,這里不做presenter操作。
現(xiàn)在我們想要測試這個簡單的 Activity有至少三個情況需要測試:
1如果API返回一個有效的用戶名列表數(shù)據(jù),那么它們會被展示到列表上面。
2 如果API返回空的數(shù)據(jù),那么界面會顯示“空的列表”
3 如果API 請求失敗,那么界面會顯示“加載用戶名失敗”
下面依次展示三個測試:
@Test
public void usernamesDisplay() {
// Stub the DataManager with a list of three usernames
List<String> expectedUsernames = Arrays.asList("Joe", "Jemma", "Matt");
when(component.getMockDataManager().loadUsernames())
.thenReturn(Single.just(expectedUsernames));
// Start the Activity
main.launchActivity(null);
// Check that the three usernames are displayed
for (Sting username:expectedUsernames) {
onView(withText(username))
.check(matches(isDisplayed()));
}
}
@Test
public void emptyMessageDisplays() {
// Stub an empty list
when(component.getMockDataManager().loadUsernames())
.thenReturn(Single.just(Collections.emptyList()));
// Start the Activity
main.launchActivity(null);
// Check the empty list message displays
onView(withText("Empty list"))
.check(matches(isDisplayed()));
}
@Test
public void errorMessageDisplays() {
// Stub with a Single that emits and error
when(component.getMockDataManager().loadUsernames())
.thenReturn(Single.error(new RuntimeException()));
// Start the Activity
main.launchActivity(null);
// Check the error message displays
onView(withText("Error loading usernames"))
.check(matches(isDisplayed()));
}
}
通過上面的代碼,我們使用TestComponentRule 和android 官方測試框架提供的ActivityTestRule。ActivityTestRule會讓我們從測試中啟動UsernamesActivity 。注意我們使用 RuleChain 來確保 TestComponentRule總是在ActivityTestRule前運行。這也是確保TestComponent在任何Activity運行之前在Application類中設定好。
你可能注意到了三個測試用例遵循同樣的構(gòu)建方式:
1 通過when (xxx).thenReturn(yyy)設置前置條件。這是通過stub loadUsernames()方法實現(xiàn)的。例如,第一個測試的前置條件是有一個有效的用戶名列表。
2 通過main.launchActivity(null)運行activity。
3 通過check(matches(isDisplayed()));檢查視圖的展示,并且展示相應前置條件期望的值。
這是一個非常有效的解決方案,它允許你測試不同的場景,因為你對整個application的初始狀態(tài)擁有絕對的控制權(quán)。如果你不使用mock來編寫上面的三個用例,幾乎不可能達到這樣的效果因為真實的API接口總會返回同樣的數(shù)據(jù)。
如果你想要查看使用這個測試方法的完整實例,你可以在github查看項目ribot Android boilerplate 或者 ribot app.
當然這個解決方案也有一些瑕疵。首先在每個test之前都會stub顯得非常繁瑣。復雜的界面可能需要在每個測試之前有5-10個stub。將一些stub移到初始化setup()方法中是有用的但經(jīng)常不同的測試需要不同的stub。第二個問題是UI測試和潛在的實現(xiàn)存在著耦合,也就意味著如果你重構(gòu)DataManager,那么你也需要修改stub。
雖然這樣,我們也在ribot 的幾個應用中應用了這個UI測試方法,事實證明這中方法也是有好處的。例如,我們最近的一個Android應用中有250個UI測試能夠在三分鐘之內(nèi)運行成功。其中也有380個Model層和Presenter層的單元測試。
好了,我希望這篇文章讓你對UI測試的認知以及編寫更好的測試代碼有一個很好的幫助。
- 詳解appium+python 啟動一個app步驟
- Python腳本在Appium庫上對移動應用實現(xiàn)自動化測試
- 詳解Android單元測試最佳實踐
- android開機自啟動APP及使用adb命令測試方法
- Android利用Espresso進行UI自動化測試的方法詳解
- 在Android打包中區(qū)分測試和正式環(huán)境淺析
- Android單元測試之對Activity的測試示例
- 淺談Android單元測試的作用以及簡單示例
- Android和iOS 測試五個最好的開源自動化工具
- 簡單談談android studio 的單元測試
- Android Monkey壓力測試詳細介紹
- Ubuntu中為Android系統(tǒng)上實現(xiàn)內(nèi)置C可執(zhí)行程序測試Linux內(nèi)核驅(qū)動程序
- Android App開發(fā)的自動化測試框架UI Automator使用教程
- Android自動測試工具Monkey的實現(xiàn)方法
- Android測試中Appium的一些錯誤解決技巧
相關文章
Android EditText實現(xiàn)輸入金額類型詳解
EditText是Android中一個非常實用的控件,有很多InputType,可以來達到不同的輸入效果,下面這篇文章主要給大家介紹了關于Android EditText實現(xiàn)輸入金額類型的相關資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-09-09
詳解Kotlin Android開發(fā)中的環(huán)境配置
這篇文章主要介紹了詳解Kotlin Android開發(fā)中的環(huán)境配置的相關資料,需要的朋友可以參考下2017-06-06
Android入門之動態(tài)BroadCast的使用教程
系統(tǒng)自己在很多時候都會發(fā)送廣播,比如電量低或者充足,剛啟動完,插入耳機,你有一條新的微信消息,這種都是使用BroadCast機制去實現(xiàn)的。BroadCast分為靜態(tài)和動態(tài)BroadCast兩種,本文就來聊聊動態(tài)BroadCast的使用,需要的可以參考一下2022-12-12
Android使用TabLayout+Fragment實現(xiàn)頂部選項卡
本文通過實例代碼給大家介紹了Android使用TabLayout+Fragment實現(xiàn)頂部選項卡功能,包括TabLyout的使用,感興趣的朋友參考下本文吧2017-05-05
Android應用內(nèi)懸浮窗的實現(xiàn)方案示例
本篇文章主要介紹了Android應用內(nèi)懸浮窗的實現(xiàn)方案示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08

