亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android 中構(gòu)建快速可靠的 UI 測試

 更新時(shí)間:2016年08月25日 17:41:13   作者:Anthony  
本文主要介紹Android 中構(gòu)建快速可靠的 UI 測試,這里整理了相關(guān)資料及相關(guān)代碼,有興趣的小伙伴可以參考下

前言

讓我一起來看看 Iván Carballo和他的團(tuán)隊(duì)是如何使用Espresso, Mockito 和Dagger 2 編寫250個(gè)UI測試,并且只花了三分鐘就運(yùn)行成功的。

在這篇文章中,我們會(huì)探索如何使用Mockito(譯者注:Mockito是java編寫的一個(gè)單元測試框架),Dagger 2 去創(chuàng)建快速可靠的Android UI測試。如果你正在開始編寫Android中的UI 測試或者希望改善已有測試性能的開發(fā)者,那么這篇文章值得一讀。

我第一次在安卓應(yīng)用中使用UI自動(dòng)化測試是在幾年前使用Robotium(譯者注:Robotium是android中的一個(gè)自動(dòng)化測試框架)。我認(rèn)為測試環(huán)境越逼真越好。在最終測試中應(yīng)當(dāng)表現(xiàn)得如同超人一般能夠迅速的點(diǎn)擊任意一個(gè)位置而且并不會(huì)報(bào)錯(cuò),對吧?我認(rèn)為mocking測試很糟糕。為什么我們需要在測試的時(shí)候改變應(yīng)用的行為?那不是欺騙嗎?幾個(gè)月后我們有了大概100個(gè)測試用例要花費(fèi)40分鐘去運(yùn)行起來。它們是如此的不穩(wěn)定,即使應(yīng)用的功能上并沒有任何錯(cuò)誤,通常有一大半的幾率會(huì)運(yùn)行失敗。我們花了大量的時(shí)間去編寫它們,但是這些測試用例卻沒有幫我們找到任何問題。

但正如John Dewey所說,失敗是具有啟發(fā)意義的。
失敗是有啟發(fā)意義的。智者總能從失敗和成功中學(xué)到同樣多的東西。

我們確實(shí)學(xué)到。我們認(rèn)識(shí)到在測試中依賴于真實(shí)的API 接口是一個(gè)糟糕的做法。因?yàn)槟闶チ藢Ψ祷氐臄?shù)據(jù)結(jié)果的控制,你也就不能對你的測試做預(yù)先處理。也就是說網(wǎng)絡(luò)錯(cuò)誤和外部API接口錯(cuò)誤都會(huì)導(dǎo)致你的測試出錯(cuò)。如果你的wifi出錯(cuò)了,你肯定不希望你的測試也會(huì)跟著出錯(cuò)。你當(dāng)然希望這時(shí)UI測試能夠成功運(yùn)行。如果你還依賴外部的API接口那么你完全是在做集成測試(integration tests),也就得不到我們期望的結(jié)果。

Mock測試正式解決之道

(Mocking is the solution)

Mock 測試也就是通過一個(gè)模擬(mock)的對象去替換一個(gè)真實(shí)的對象以便于測試。它主要應(yīng)用于編寫單元測試,但在UI測試中也會(huì)非常有用。你可以參照不同的方法去模擬java對象但使用Mockito 確實(shí)是一個(gè)簡單有效的解決方案。在下面的例子中你可以看到一個(gè)模擬的UserApi 類并且stub(譯者注:stub,也即“樁”,主要出現(xiàn)在集成測試的過程中,從上往下的集成時(shí),作為下方程序的替代??梢岳斫鉃閷Ψ椒ㄟM(jìn)行預(yù)先的處理,達(dá)到修改的效果。下文中不做翻譯)了其中的一個(gè)方法,因此它總會(huì)返回一個(gè)用戶名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)建了一個(gè)mock對象你需要確保應(yīng)用測試的時(shí)候使用的是這個(gè)模擬的對象,并且在運(yùn)行的時(shí)候使用的是真實(shí)對象。這也是一個(gè)難點(diǎn)所在,如果你的代碼構(gòu)建得并不是易于測試(test-friendly)的,替換真實(shí)對象的過程會(huì)變得異常艱難甚至是說不可能完成。還要注意的是,你想要模擬的代碼必須獨(dú)立到一個(gè)單獨(dú)的類里面。比如說,如果你直接從你的activity中使用HttpURLConnection調(diào)用REST API 進(jìn)行數(shù)據(jù)訪問(我希望你不要這么做), 這個(gè)操作過程模擬起來也就會(huì)非常困難。

在測試之前考慮一下系統(tǒng)架構(gòu),糟糕的系統(tǒng)架構(gòu)往往會(huì)導(dǎo)致測試用例和mock測試難于編寫,mock測試也會(huì)變得不穩(wěn)定。

一個(gè)易于測試的架構(gòu)

A test friendly architecture

構(gòu)建一個(gè)易于測試的架構(gòu)有許多種方式。在這里我將使用 ribot 中使用的架構(gòu) (譯者注:也就是在開篇提到的Android應(yīng)用架構(gòu))作為范例,你也可以應(yīng)用這樣的架構(gòu)方式到任何架構(gòu)中。我們的架構(gòu)是基于MVP模式,我們決定在UI測試中去模擬(mock)整個(gè)Model層,因此我們可以對數(shù)據(jù)由更多的操作性,也就能夠?qū)懗龈袃r(jià)值和可靠的測試。

MVP架構(gòu)

DataManager是Model層中唯一暴露給Presenter層的數(shù)據(jù)的類,因此為了測試Model層我們只需要替換為一個(gè)模擬
的DataManger即可。

使用Dagger注入模擬的DataManager

Using Dagger to inject a mock DataManager

一旦我們明確了需要模擬什么對象,那么接下來就該考慮在測試中如何替換真實(shí)的對象。我們通過Dagger2 解決這個(gè)問題(一個(gè)Android中的依賴注入框架),如果你還沒有接觸過Dagger ,在繼續(xù)閱讀下去之前我建議你閱讀使用Dagger2 進(jìn)行依賴注入【英】 。我們的應(yīng)用至少包含一個(gè)Dagger 的Module和Component。通常被叫做ApplicationComponent 和ApplicationModule。你可以在下面看到一個(gè)簡化版的只提供了DataManger實(shí)例的類。當(dāng)然你也可以采用第二種方法,在DataManager的構(gòu)造函數(shù)上使用@inject注解。這里我直接提供一個(gè)方法便于理解。(譯者注:這里將兩個(gè)類ApplicationComponent 和ApplicationModule寫在一起,便于直觀理解)

@Module

public class ApplicationModule {

@Provides

@Singleton

public DataManager provideDataManager() {

return mDataManager;

}

}

@Singleton

@Component(modules = ApplicationModule.class)

public interface ApplicationComponent {

DataManager dataManager();

}

應(yīng)用的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)建一個(gè)test的時(shí)候需要用到的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會(huì)使用模擬的DataManager對象。

創(chuàng)建JUnit,并且設(shè)定TestComponent

Creating a JUnit rule that sets the TestComponent

為了確保在每次測試前TestComponent被設(shè)置到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將會(huì)創(chuàng)建TestComponent的實(shí)例對象,這也就會(huì)覆寫apply方法并返回一個(gè)新的 Statement,新的Statement會(huì):

1 設(shè)定TestComponent給Application類的component對象。

2調(diào)用基類的Statement 的evaluate()方法(這是在test的時(shí)候執(zhí)行)

3 設(shè)置Application的component字段為空,也就讓其恢復(fù)到初始狀態(tài)。我們能夠通過這種方式預(yù)防測試用例之間的相互影響
通過上面的代碼我們可以通過getMockDataManager()方法獲取模擬的DataManager對象。這也就允許我們能夠給得到DataManager對象并且stub它的方法。需要注意的是,這只有TestApplicationComponent的provideDataManger方法使用@Singleton注解的時(shí)候有效。如果它沒有被指定為單例的,那么我們通過getMockDataManager方法得到的實(shí)例對象將會(huì)不同于應(yīng)用使用的實(shí)例對象。因此,我們也不可能stub它。

編寫測試用例

Writing the tests

現(xiàn)在我們有Dagger正確的配置,并且TestComponentRule也可以使用了,我們還有一件事要做,那就是編寫測試用例。我們使用 Espresso編寫UI測試。它并不是完美的但是它是一個(gè)快速可靠的Android測試框架。在編寫測試用例之前我們需要一個(gè)app去測試。假如我們有一個(gè)非常簡單的app,從REST API 中加載用戶名,并且展示到RecyclerView上面。那么DataManger將會(huì)是下面這個(gè)樣子:

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ā)送一串字符串。 我們也需要一個(gè)Activity展示用戶名usernames到RecyclerView上面,我們假設(shè)這個(gè)Activity叫做UsernamesActivity。如果你遵循MVP模式你也會(huì)有相應(yīng)的presenter但為了直觀理解,這里不做presenter操作。

現(xiàn)在我們想要測試這個(gè)簡單的 Activity有至少三個(gè)情況需要測試:

1如果API返回一個(gè)有效的用戶名列表數(shù)據(jù),那么它們會(huì)被展示到列表上面。
2 如果API返回空的數(shù)據(jù),那么界面會(huì)顯示“空的列表”
3 如果API 請求失敗,那么界面會(huì)顯示“加載用戶名失敗”

下面依次展示三個(gè)測試:

@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會(huì)讓我們從測試中啟動(dòng)UsernamesActivity 。注意我們使用 RuleChain 來確保 TestComponentRule總是在ActivityTestRule前運(yùn)行。這也是確保TestComponent在任何Activity運(yùn)行之前在Application類中設(shè)定好。

你可能注意到了三個(gè)測試用例遵循同樣的構(gòu)建方式:

1 通過when (xxx).thenReturn(yyy)設(shè)置前置條件。這是通過stub loadUsernames()方法實(shí)現(xiàn)的。例如,第一個(gè)測試的前置條件是有一個(gè)有效的用戶名列表。
2 通過main.launchActivity(null)運(yùn)行activity。
3 通過check(matches(isDisplayed()));檢查視圖的展示,并且展示相應(yīng)前置條件期望的值。

這是一個(gè)非常有效的解決方案,它允許你測試不同的場景,因?yàn)槟銓φ麄€(gè)application的初始狀態(tài)擁有絕對的控制權(quán)。如果你不使用mock來編寫上面的三個(gè)用例,幾乎不可能達(dá)到這樣的效果因?yàn)檎鎸?shí)的API接口總會(huì)返回同樣的數(shù)據(jù)。

如果你想要查看使用這個(gè)測試方法的完整實(shí)例,你可以在github查看項(xiàng)目ribot Android boilerplate 或者 ribot app.
當(dāng)然這個(gè)解決方案也有一些瑕疵。首先在每個(gè)test之前都會(huì)stub顯得非常繁瑣。復(fù)雜的界面可能需要在每個(gè)測試之前有5-10個(gè)stub。將一些stub移到初始化setup()方法中是有用的但經(jīng)常不同的測試需要不同的stub。第二個(gè)問題是UI測試和潛在的實(shí)現(xiàn)存在著耦合,也就意味著如果你重構(gòu)DataManager,那么你也需要修改stub。

雖然這樣,我們也在ribot 的幾個(gè)應(yīng)用中應(yīng)用了這個(gè)UI測試方法,事實(shí)證明這中方法也是有好處的。例如,我們最近的一個(gè)Android應(yīng)用中有250個(gè)UI測試能夠在三分鐘之內(nèi)運(yùn)行成功。其中也有380個(gè)Model層和Presenter層的單元測試。

好了,我希望這篇文章讓你對UI測試的認(rèn)知以及編寫更好的測試代碼有一個(gè)很好的幫助。

相關(guān)文章

最新評論