安卓自動化測試入門-6-Espresso Test

安卓自動化測試入門-6-Espresso Test

本文翻譯自Riggaroo的《 Introduction to Automated Android Testing – Part 6
注意:以下的測試特指“程序員編寫的自動化代碼測試”
水平有限,歡迎指教。如有錯漏,多多包涵。
作者的項目地址:
https://github.com/riggaroo/GithubUsersSearchApp
請注意:每個分支對應這一系列博客的每一篇文章。

在前面5篇博客中,我們覆蓋了從草稿建立一個Android App的各個方面的知識。我們專注於在這個過程中編寫單元測試。以下是前面幾篇博客的鏈接:

  • Part1 - 爲什麼我們應該編寫測試?
  • Part2 - 配置項目
  • Part3 - 網絡請求的單元測試
  • Part4 - Presenter的單元測試
  • Part5 - 創建UI

原文:

  • 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。

build_variants_mock_debug

2 . 在src目錄下新建一個mock文件夾,然後在裏面新建一個java文件夾,然後在裏面新建一個包,包名跟主包名相同(za.co.riggaroo.gus.data.remote)。新建一個類,名爲MockGithubUserRestServiceImpl。最後你的文件目錄應該像下圖所示:

resulting_file_structure

3 . 新建一個prod文件夾,移動之前定義的Injection類到這個文件夾(包也一樣)。我們將會創建另一個Injection類到mock文件夾裏面。這個類將會注入模擬出來的GitHub服務,而不是生產環境API。

prod_mock

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,我們應該看到不管我們搜索什麼,都會返回一樣的結果。

gif-dummydata

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。你的項目結構應該像下圖所示:

AndroidTestMock_folder

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()));
    }
}

@RuleActivityTestRule指明瞭這個測試要運行的是哪個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 . 讓我們運行這些測試:

Passing_UI_Tests

全部通過了!

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

mock_debug_android_test

Yay! - 我們的Mock UI測試有了82%的覆蓋率。加上我們在Part4中看到的覆蓋率報告,這給了我們一個很不錯的基於整個APP的測試評價。現在我們可以重複之前的方式,努力提高我們的代碼的測試覆蓋率。

結語

哇!我們通過6篇博客完成了測試編寫!很明顯,還可以爲這個APP編寫更多的測試。非功能性的測試,例如測試你的APP在低內存的設備上的表現,或者不穩定的網絡狀態下的表現。
這個系列到這裏就結束了,希望你享受到測試的快樂。如果覺得寫得好,請推薦給你的朋友,歡迎訂閱博客推送。

原作者Riggaroo博客地址:https://riggaroo.co.za/

譯者結語:英文有點菜,翻譯得磕磕絆絆的,大家見諒了。本人失業中,有廣州的Android工程師招聘的話,麻煩推薦一下我哈。陳捷尉 13580579413 [email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章