安卓自動化測試入門-6-Espresso Test
本文翻譯自Riggaroo的《 Introduction to Automated Android Testing – Part 6 》
注意:以下的測試特指“程序員編寫的自動化代碼測試”
水平有限,歡迎指教。如有錯漏,多多包涵。
作者的項目地址:
https://github.com/riggaroo/GithubUsersSearchApp。
請注意:每個分支對應這一系列博客的每一篇文章。
在前面5篇博客中,我們覆蓋了從草稿建立一個Android App的各個方面的知識。我們專注於在這個過程中編寫單元測試。以下是前面幾篇博客的鏈接:
原文:
- Post #1 – Why should we write tests?
- Post #2 – Set up your app for testing
- Post #3 – Creating API calls
- Post #4 – Creating repositories
- Post #5 – Following the MVP pattern
在本系列博客的最後一篇文章裏面,我們將會介紹如何爲Part5創建的View編寫Espresso測試。相關的GitHub資源可以在 這裏 找到。
當數據是動態的時候,要測試一個View包含預期的確切數據是不現實的。我們的測試不應該因爲隨時可能變動的數據而不通過。爲了讓測試變得可靠並可重複,我們不應該直接調用生產環境的API。
通過模擬出API調用的返回值,我們可以編寫基於模擬數據的測試。有以下幾種方法可以模擬API代用的返回值:
- Option 1 - 使用 WireMock 來運行一個獨立的服務器,這個服務器提供相同的靜態JSON給API調用。
- Option 2 - 使用OkHttp的 MockWebServer 功能,可以在你的設備上運行一個網絡服務器來響應你的網絡請求。
- Option 3 - 創建Retrofit REST接口的特殊實現,並返回假數據。
顯然,如何選擇完全是根據你個人的需要及習慣。在我的情況下,WireMock意味着額外的工作量,因爲我還需要在一個靜態的IP地址上架設一臺獨立的服務器。
MockWebServer比WireMock容易使用一些,因爲你不需要去架設一臺獨立服務器(服務器運行於你的設備上)。MockWebServer還可以靈活地配置出各種不同的應用場景。還具有一些有用的特性,例如設定某個請求的調用失敗頻率,或者模擬低網速下的網絡請求返回( 瞭解更多 )。
我準備使用Option 3,這樣可以方便地測試UI顯示的數據是否與模擬的響應數據匹配。如果我需要添加低網速的測試(或者一些非功能性的測試),我會選擇Option 2。如果你無法使用OkHttp,
可以選擇Option 1,因爲WireMock可以與任何Http客戶端合作。
使用Gradle flavors模擬數據
通過使用Gradle flavors我們可以輕易地模擬API返回值。如果你閱讀了Part2的Gradle flavors部分,你應該已經有了一個”mock”和一個”production” flavor。
1 . 確保你切換到了mockDebug flavor。
2 . 在src目錄下新建一個mock文件夾,然後在裏面新建一個java文件夾,然後在裏面新建一個包,包名跟主包名相同(za.co.riggaroo.gus.data.remote
)。新建一個類,名爲MockGithubUserRestServiceImpl
。最後你的文件目錄應該像下圖所示:
3 . 新建一個prod文件夾,移動之前定義的Injection
類到這個文件夾(包也一樣)。我們將會創建另一個Injection
類到mock文件夾裏面。這個類將會注入模擬出來的GitHub服務,而不是生產環境API。
在mock文件夾裏面的Injection
類中,我們僅僅只是返回之前創建的MockGithubUserRestServiceImpl
。而在prod文件夾中的Injection
類,我們返回真實的Retrofit GitHub服務。
Mock Injection
class:
public class Injection {
private static GithubUserRestService userRestService;
public static UserRepository provideUserRepo() {
return new UserRepositoryImpl(provideGithubUserRestService());
}
static GithubUserRestService provideGithubUserRestService() {
if (userRestService == null) {
userRestService = new MockGithubUserRestServiceImpl();
}
return userRestService;
}
}
Prod Injection
class:
public class Injection {
private static final String BASE_URL = "https://api.github.com";
private static OkHttpClient okHttpClient;
private static GithubUserRestService userRestService;
private static Retrofit retrofitInstance;
public static UserRepository provideUserRepo() {
return new UserRepositoryImpl(provideGithubUserRestService());
}
static GithubUserRestService provideGithubUserRestService() {
if (userRestService == null) {
userRestService = getRetrofitInstance().create(GithubUserRestService.class);
}
return userRestService;
}
static OkHttpClient getOkHttpClient() {
if (okHttpClient == null) {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
okHttpClient = new OkHttpClient.Builder().addInterceptor(logging).build();
}
return okHttpClient;
}
static Retrofit getRetrofitInstance() {
if (retrofitInstance == null) {
Retrofit.Builder retrofit = new Retrofit.Builder().client(Injection.getOkHttpClient()).baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create());
retrofitInstance = retrofit.build();
}
return retrofitInstance;
}
}
4 . Mock服務返回的數據依賴於你的特定需要。下面是我的實現:
public class MockGithubUserRestServiceImpl implements GithubUserRestService {
private final List<User> usersList = new ArrayList<>();
private User dummyUser1, dummyUser2;
public MockGithubUserRestServiceImpl() {
dummyUser1 = new User("riggaroo", "Rebecca Franks",
"https://riggaroo.co.za/wp-content/uploads/2016/03/rebeccafranks_circle.png", "Android Dev");
dummyUser2 = new User("riggaroo2", "Rebecca's Alter Ego",
"https://s-media-cache-ak0.pinimg.com/564x/e7/cf/f3/e7cff3be614f68782386bfbeecb304b1.jpg", "A unicorn");
usersList.add(dummyUser1);
usersList.add(dummyUser2);
}
@Override
public Observable<UsersList> searchGithubUsers(final String searchTerm) {
return Observable.just(new UsersList(usersList));
}
@Override
public Observable<User> getUser(final String username) {
if (username.equals("riggaroo")) {
return Observable.just(dummyUser1);
} else if (username.equals("riggaroo2")) {
return Observable.just(dummyUser2);
}
return Observable.just(null);
}
}
在當前情況下,我僅僅只是返回一些假數據。讓我們跑一下mock版本的app,我們應該看到不管我們搜索什麼,都會返回一樣的結果。
Cool!我們現在有了一個可用的假數據App了。現在可以開始寫Espresso UI測試了。
編寫Espresso測試的基礎知識
當我們編寫 Espresso 測試,下面的範式用來執行你的UI的功能:
onView(withId(R.id.menu_search)) // withId(R.id.menu_search) is a ViewMatcher
.perform(click()) // click() is a ViewAction
.check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
ViewMatcher - 用於查找一個Activity裏面的View。Espresso定義了各種各樣的matcher。例如:
withId(R.id.menu_search)
,withText("Search")
,withTag("custom_tag")
。ViewAction - 用於模擬人與View的交互,點擊View什麼的。例如:
click()
,doubleClick()
,swipeUp()
,typeText()
。ViewAssertion - 用於對View的某些狀態做出斷言。例如,
doesNotExist()
,isAbove()
,isBelow()
。
這裏給大家一份PDF版本的Espresso方法小抄: android-espresso-testing.pdf 。值得注意的是傳統的 hamcrest matcher 也能用在Espresso測試中。(譯者注:就是用來合併多個matcher形成合集的一些方法,像與、或、非。)例如:not()
,allOf()
,anyOf()
。
編寫Espresso UI測試
如果你可以想起來,我們在 Part2 已經介紹了Espresso需要的依賴了。現在我們來了解如何編寫Espresso測試。
1 . 創建一個androidTestMock文件夾。在這個文件夾裏面的測試只運行於mock的環境,而不是運行於production的環境。接着創建一個包za.co.riggaroo.gus.presentation.search
。在包內新建一個類UserSearchActivityTest
。你的項目結構應該像下圖所示:
2 . 我們先從簡單的開始,驗證當Activity啓動後,會顯示”Start typing to search”文本。
public class UserSearchActivityTest {
@Rule
public ActivityTestRule<UserSearchActivity> testRule = new ActivityTestRule<>(UserSearchActivity.class);
@Test
public void searchActivity_onLaunch_HintTextDisplayed() {
//讓Activity自啓動
//用戶沒有進行操作
//然後
onView(withText("Start typing to search"))
.check(matches(isDisplayed()));
}
}
@Rule
和ActivityTestRule
指明瞭這個測試要運行的是哪個Activity。當前這個測試運行的是UserSearchActivity
。這樣就可以自啓動UserSearchActivity
。通過傳遞額外參數,可以指定是否自啓動該Activity。
這個searchActivity_onLaunch_HintTextDisplayed()
測試相當簡單。它搜索包含指定文本的View,並斷定這個文本在UI上是可見的。
3 . 下一個測試稍微複雜一些:
@Test
public void searchText_ReturnsCorrectlyFromWebService_DisplaysResult() {
//讓Activity自啓動
//When
onView(allOf(withId(R.id.menu_search), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))).perform(
click()); // 當使用SearchView時,會有兩個View匹配menu_search id - 一個是圖標,另一個是文本框。我們想要點擊那個可見的。
onView(withId(R.id.search_src_text)).perform(typeText("riggaroo"), pressKey(KeyEvent.KEYCODE_ENTER));
//Then
onView(withText("Start typing to search")).check(matches(not(isDisplayed())));
onView(withText("riggaroo - Rebecca Franks")).check(matches(isDisplayed()));
onView(withText("Android Dev")).check(matches(isDisplayed()));
onView(withText("A unicorn")).check(matches(isDisplayed()));
onView(withText("riggaroo2 - Rebecca's Alter Ego")).check(matches(isDisplayed()));
}
在輸入文本到SearchView
之後,點擊enter,我們斷定假數據會顯示在UI上。
4 . 我們已經給正面的場景寫了測試。現在我們應該爲負面的情況寫測試。我們需要調整MockGithubUserRestServiceImpl
,讓它可以返回定製的error observable。
private static Observable dummyGithubSearchResult = null;
public static void setDummySearchGithubCallResult(Observable result) {
dummyGithubSearchResult = result;
}
@Override
public Observable<UsersList> searchGithubUsers(final String searchTerm) {
if (dummyGithubSearchResult != null) {
return dummyGithubSearchResult;
}
return Observable.just(new UsersList(usersList));
}
在上面的代碼中,新建了一個可以設置假搜索結果的方法。當調用searchGithubUsers()
時,如果那個Observable不爲null,將返回它。
5 . 現在我們創建一個測試,檢查錯誤信息是否顯示在UI上。
@Test
public void searchText_ServiceCallFails_DisplayError() {
String errorMsg = "Server Error";
MockGithubUserRestServiceImpl.setDummySearchGithubCallResult(Observable.error(new Exception(errorMsg)));
onView(allOf(withId(R.id.menu_search), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))).perform(
click()); // 當使用SearchView時,會有兩個View匹配menu_search id - 一個是圖標,另一個是文本框。我們想要點擊那個可見的。
onView(withId(R.id.search_src_text)).perform(typeText("riggaroo"), pressKey(KeyEvent.KEYCODE_ENTER));
onView(withText(errorMsg)).check(matches(isDisplayed()));
}
在這個測試裏,我們先確保service返回一個異常,然後我們斷定錯誤信息被顯示到UI上。
6 . 讓我們運行這些測試:
全部通過了!
Android代碼的覆蓋率
爲了知道你寫的測試的有效性,進行代碼覆蓋率度量是很好的做法。
1 . 爲了讓UI測試的代碼覆蓋率功能可用,添加testCoverageEnabled = true
到build.gradle.
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
testCoverageEnabled = true
}
}
2 . 代碼覆蓋率功能目前並不兼容Jack編譯器。我們需要切換成 Retrolambda 來獲得代碼覆蓋率報告。相關的分支地址在 這裏 。
在app的目錄下的build.gradle添加以下代碼啓用Retrolambda。
apply plugin: 'me.tatarka.retrolambda'
/*jackOptions {
enabled true
}*/
在根目錄下的build.gradle添加相關的資源
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'me.tatarka:gradle-retrolambda:3.3.0-beta4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
3 . 在Terminal中運行任務:createMockDebugCoverageReport。你將在這個目錄找到HTML報告: app/build/reports/coverage/mock/debug/index.html.
Windows下的命令爲.\gradlew createMockDebugCoverageReport
Yay! - 我們的Mock UI測試有了82%的覆蓋率。加上我們在Part4中看到的覆蓋率報告,這給了我們一個很不錯的基於整個APP的測試評價。現在我們可以重複之前的方式,努力提高我們的代碼的測試覆蓋率。
結語
哇!我們通過6篇博客完成了測試編寫!很明顯,還可以爲這個APP編寫更多的測試。非功能性的測試,例如測試你的APP在低內存的設備上的表現,或者不穩定的網絡狀態下的表現。
這個系列到這裏就結束了,希望你享受到測試的快樂。如果覺得寫得好,請推薦給你的朋友,歡迎訂閱博客推送。
原作者Riggaroo博客地址:https://riggaroo.co.za/
譯者結語:英文有點菜,翻譯得磕磕絆絆的,大家見諒了。本人失業中,有廣州的Android工程師招聘的話,麻煩推薦一下我哈。陳捷尉 13580579413 [email protected]