Android 中構建快速可靠的 UI 測試
前言
讓我一起來看看 Iván Carballo和他的團隊是如何使用Espresso, Mockito 和Dagger 2 編寫250個UI測試,並且只花了三分鐘就執行成功的。
在這篇文章中,我們會探索如何使用Mockito(譯者注:Mockito是java編寫的一個單元測試框架),Dagger 2 去建立快速可靠的Android UI測試。如果你正在開始編寫Android中的UI 測試或者希望改善已有測試效能的開發者,那麼這篇文章值得一讀。
我第一次在安卓應用中使用UI自動化測試是在幾年前使用Robotium(譯者注:Robotium是android中的一個自動化測試框架)。我認為測試環境越逼真越好。在最終測試中應當表現得如同超人一般能夠迅速的點選任意一個位置而且並不會報錯,對吧?我認為mocking測試很糟糕。為什麼我們需要在測試的時候改變應用的行為?那不是欺騙嗎?幾個月後我們有了大概100個測試用例要花費40分鐘去執行起來。它們是如此的不穩定,即使應用的功能上並沒有任何錯誤,通常有一大半的機率會執行失敗。我們花了大量的時間去編寫它們,但是這些測試用例卻沒有幫我們找到任何問題。
但正如John Dewey所說,失敗是具有啟發意義的。
失敗是有啟發意義的。智者總能從失敗和成功中學到同樣多的東西。
我們確實學到。我們認識到在測試中依賴於真實的API 介面是一個糟糕的做法。因為你失去了對返回的資料結果的控制,你也就不能對你的測試做預先處理。也就是說網路錯誤和外部API介面錯誤都會導致你的測試出錯。如果你的wifi出錯了,你肯定不希望你的測試也會跟著出錯。你當然希望這時UI測試能夠成功執行。如果你還依賴外部的API介面那麼你完全是在做整合測試(integration tests),也就得不到我們期望的結果。
Mock測試正式解決之道
(Mocking is the solution)
Mock 測試也就是通過一個模擬(mock)的物件去替換一個真實的物件以便於測試。它主要應用於編寫單元測試,但在UI測試中也會非常有用。你可以參照不同的方法去模擬java物件但使用Mockito 確實是一個簡單有效的解決方案。在下面的例子中你可以看到一個模擬的UserApi
類並且stub(譯者注:stub,也即“樁”,主要出現在整合測試的過程中,從上往下的整合時,作為下方程式的替代。可以理解為對方法進行預先的處理,達到修改的效果。下文中不做翻譯)了其中的一個方法,因此它總會返回一個使用者名稱username
的靜態陣列。
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();
一旦你建立了一個mock物件你需要確保應用測試的時候使用的是這個模擬的物件,並且在執行的時候使用的是真實物件。這也是一個難點所在,如果你的程式碼構建得並不是易於測試(test-friendly)的,替換真實物件的過程會變得異常艱難甚至是說不可能完成。還要注意的是,你想要模擬的程式碼必須獨立到一個單獨的類裡面。比如說,如果你直接從你的activity中使用HttpURLConnection呼叫REST API 進行資料訪問(我希望你不要這麼做), 這個操作過程模擬起來也就會非常困難。
在測試之前考慮一下系統架構,糟糕的系統架構往往會導致測試用例和mock測試難於編寫,mock測試也會變得不穩定。
一個易於測試的架構
A test friendly architecture
構建一個易於測試的架構有許多種方式。在這裡我將使用 ribot 中使用的架構 (譯者注:也就是在開篇提到的Android應用架構)作為範例,你也可以應用這樣的架構方式到任何架構中。我們的架構是基於MVP模式,我們決定在UI測試中去模擬(mock)整個Model層,因此我們可以對資料由更多的操作性,也就能夠寫出更有價值和可靠的測試。
DataManager
是Model層中唯一暴露給Presenter層的資料的類,因此為了測試Model層我們只需要替換為一個模擬
的DataManger
即可。
使用Dagger注入模擬的DataManager
Using Dagger to inject a mock DataManager
一旦我們明確了需要模擬什麼物件,那麼接下來就該考慮在測試中如何替換真實的物件。我們通過Dagger2 解決這個問題(一個Android中的依賴注入框架),如果你還沒有接觸過Dagger ,在繼續閱讀下去之前我建議你閱讀使用Dagger2 進行依賴注入【英】 。我們的應用至少包含一個Dagger 的Module和Component。通常被叫做ApplicationComponent
和ApplicationModule
。你可以在下面看到一個簡化版的只提供了DataManger
例項的類。當然你也可以採用第二種方法,在DataManager
的建構函式上使用@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,你可能有同樣的配置步驟,現在的做法是建立一個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
物件。
建立JUnit,並且設定TestComponent
Creating a JUnit rule that sets the TestComponent
為了確保在每次測試前TestComponent
被設定到Application
類中,我們可以建立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
將會建立TestComponent
的例項物件,這也就會覆寫apply方法並返回一個新的 Statement
,新的Statement
會:
1 設定TestComponent
給Application
類的component
物件。
2呼叫基類的Statement
的evaluate()
方法(這是在test的時候執行)
3 設定Application
的component
欄位為空,也就讓其恢復到初始狀態。我們能夠通過這種方式預防測試用例之間的相互影響
通過上面的程式碼我們可以通過getMockDataManager()
方法獲取模擬的DataManager
物件。這也就允許我們能夠給得到DataManager
物件並且stub它的方法。需要注意的是,這隻有TestApplicationComponent
的provideDataManger
方法使用@Singleton
註解的時候有效。如果它沒有被指定為單例的,那麼我們通過getMockDataManager
方法得到的例項物件將會不同於應用使用的例項物件。因此,我們也不可能stub它。
編寫測試用例
Writing the tests
現在我們有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 的資料。它返回的是Single 物件,並且傳送一串字串。 我們也需要一個Activity展示使用者名稱usernames
到RecyclerView
上面,我們假設這個Activity
叫做UsernamesActivity
。如果你遵循MVP模式你也會有相應的presenter但為了直觀理解,這裡不做presenter操作。
現在我們想要測試這個簡單的 Activity
有至少三個情況需要測試:
1如果API返回一個有效的使用者名稱列表資料,那麼它們會被展示到列表上面。
2 如果API返回空的資料,那麼介面會顯示“空的列表”
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
類中設定好。
你可能注意到了三個測試用例遵循同樣的構建方式:
1 通過when (xxx).thenReturn(yyy)
設定前置條件。這是通過stub loadUsernames()方法實現的。例如,第一個測試的前置條件是有一個有效的使用者名稱列表。
2 通過main.launchActivity(null)
執行activity。
3 通過check(matches(isDisplayed()));
檢查檢視的展示,並且展示相應前置條件期望的值。
這是一個非常有效的解決方案,它允許你測試不同的場景,因為你對整個application
的初始狀態擁有絕對的控制權。如果你不使用mock來編寫上面的三個用例,幾乎不可能達到這樣的效果因為真實的API介面總會返回同樣的資料。
如果你想要檢視使用這個測試方法的完整例項,你可以在github檢視專案ribot Android boilerplate 或者 ribot app.
當然這個解決方案也有一些瑕疵。首先在每個test之前都會stub顯得非常繁瑣。複雜的介面可能需要在每個測試之前有5-10個stub。將一些stub移到初始化setup()
方法中是有用的但經常不同的測試需要不同的stub。第二個問題是UI測試和潛在的實現存在著耦合,也就意味著如果你重構DataManager
,那麼你也需要修改stub。
雖然這樣,我們也在ribot 的幾個應用中應用了這個UI測試方法,事實證明這中方法也是有好處的。例如,我們最近的一個Android應用中有250個UI測試能夠在三分鐘之內執行成功。其中也有380個Model層和Presenter層的單元測試。
好了,我希望這篇文章讓你對UI測試的認知以及編寫更好的測試程式碼有一個很好的幫助。
相關文章
- 快速構建vue ui元件庫VueUI元件
- 4、Android UI測試AndroidUI
- iOS 單元測試和 UI 測試快速入門iOSUI
- Xcode 7 中的 UI 測試XCodeUI
- Android UI 測試指南之 EspressoAndroidUIEspresso
- 可靠性測試
- 從工程化角度討論如何快速構建可靠React元件React元件
- android-MVP架構中Presenter的單元測試AndroidMVP架構
- 如何測試電腦程式中的 UI 元素?UI
- 軟體效能測試和可靠性測試
- 通過構建自己的JavaScript測試框架來了解JS測試JavaScript框架JS
- jenkins 構建歷史中的測試報告打不開Jenkins測試報告
- 構建高效的自動化測試框架框架
- 前端測試套件構建實踐前端套件
- android中Fragment的建構函式AndroidFragment函式
- android 5個自動化測試Ui框架AndroidUI框架
- 構建自己的React UI元件庫: 構建首頁ReactUI元件
- [譯] 更可靠的 React 元件:從"可測試的"到"測試通過的"React元件
- Android使用Espresso進行UI自動化測試AndroidEspressoUI
- Android UI 自動化測試實現過程AndroidUI
- Element-UI 中 Make 自動化構建分析UI
- 全副武裝!Android UI 自動化測試在 RxImagePicker 中的實踐歷程AndroidUI
- 解放雙手 - Android 開發應該嘗試的 UI 自動化測試AndroidUI
- [譯] 為你的 iOS App 構建分離測試iOSAPP
- 如何在敏捷中交付可靠的架構?敏捷架構
- 自動化測試系列(三)|UI測試UI
- Spring Boot 構建 Restful API 和測試Spring BootRESTAPI
- 使用SQLT來構建Oracle測試用例SQLOracle
- 一種新的UI測試方法:視覺感知測試UI視覺
- 可靠性測試-故障注入工具
- MQTT 安全解析:構建可靠的物聯網系統MQQT
- 使用Intellij中的Spring Initializr來快速構建SpriIntelliJSpring
- 軟體測試軟環境的構建與優化優化
- 網站建設中如何測試完成的網站?網站
- 如何構建可控,可靠,可擴充套件的 PWA 應用套件
- 優步是如何用Kafka構建可靠的重試處理保證資料不丟失Kafka
- 基於開源的 ChatGPT Web UI 專案,快速構建屬於自己的 ChatGPT 站點ChatGPTWebUI
- Android單元測試-對Activity的測試Android