淺談測試之Espresso

Espresso的簡介

Espresso官網指南java

Google推行的測試庫,用於編寫簡潔、漂亮、可靠的Android UI測試。缺點是須要真機或模擬器配合測試,比較慢。android

Espresso的集成

1.app的build.gradle下添加依賴

androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0'
androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation 'androidx.test:core:1.1.0'
複製代碼

2.一樣的build.gradle文件下的android.defaultConfig裏添加一行

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
複製代碼

Espresso的使用

1.Espresso示例

1)用Espresso寫的測試代碼是放置項目自動生成的src/androidTest/java文件夾裏的。git

2)模板代碼github

@RunWith(AndroidJUnit4.class)
@LargeTest
public class EspressoTest {

    @Rule
    public ActivityTestRule<MainActivity> activityRule =
            new ActivityTestRule<>(MainActivity.class);

    @Test
    public void testEspresso() {
        ...
    }
}
複製代碼

注意:web

1.ActivityTestRule會當即初始化MainActivity,執行onCreate()、onResume()方法。數據庫

2.ActivityTestRule它是運行在@Before以前的。若是你不想當即初始化MainActivity,而且傳遞一些參數給MainActivity,可使用ActivityTestRule另外一個構造方法:ActivityTestRule( Class<T> activityClass, boolean initialTouchMode, boolean launchActivity)json

@RunWith(AndroidJUnit4.class)
@LargeTest
public class EspressoTest {

    @Rule
    public ActivityTestRule<MainActivity> activityRule =
            new ActivityTestRule<>(MainActivity.class,true,false);
            
    @Before
    public void setup(){
        Intent intent = new Intent(ApplicationProvider.getApplicationContext(),
                            MainActivity.class);
        intent.putExtra("hello","nsnmn");
        intentsTestRule.launchActivity(intent);
    }
}
複製代碼

2.Context

在測試裏,能夠經過ApplicationProvider.getApplicationContext()得到Application的Context。api

3.API

Espresso的核心類有4個。都是提供一系列靜態方法的工具類:bash

1)Espresso:提供幾個靜態方法,如onView()或onData(),方便定位到相應的UI控件。還有幾個不必定綁定到任何視圖的api,好比pressBack()、closeSoftKeyboard()。服務器

2)ViewMatchers:提供的靜態方法,好比ViewAssertions.withId()、ViewAssertions.withText(),均會返回一個實現了Matcher<? super View>接口的類實例。你能夠將一個或多個此類實例,做爲參數傳遞給onView()方法,以便定位到相應的控件。

Espresso.onView(ViewMatchers.withId(R.id.my_view))
即:
onView(withId(R.id.my_view))
複製代碼

3)ViewActions:提供的靜態方法,好比,ViewActions.click()、ViewActions.closeSoftKeyboard(),均會返回一個實現了ViewAction接口的類實例。你能夠將一個或者多個此類實例,做爲參數傳遞給ViewInteraction.perform()方法。

//onView方法,會返回ViewInteraction的實例
onView(withId(R.id.my_view)).perform(click(),closeSoftKeyboard())
複製代碼

4)ViewAssertions:提供的靜態方法,均會返回一個實現了ViewAssertion接口的類實例。你能夠將該實例,做爲參數傳遞給ViewInteraction.check()方法。大多數狀況下,咱們使用ViewAssertions.matches()斷言,斷言當前選定控件的狀態。

onView(withId(R.id.show_text_view)).check(matches(withText("text")))
複製代碼

4.定位一個View

最簡單的是經過id來定位:

onView(withId(R.id.my_view))
複製代碼

或者經過特有的特徵,好比文本:

onView(withText("Hello!"))
複製代碼

但有時候,使用withId()來定位一個控件,你可能會獲得AmbiguousViewMatcherException異常。咱們知道,R.id的值是可能被多個界面的控件共享的。因此,僅靠withId()來定位是不夠的,必須加上額外的限制條件。好比:

onView(allOf(withId(R.id.my_view), withText("Hello!")));
又或者:
onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));
複製代碼

5.操做一個控件

最簡單的就是點擊一個控件:

onView(...).perform(click());
複製代碼

也能夠對一個控件連續進行多個操做:

//輸入文字,而後進行點擊
onView(...).perform(typeText("Hello"), click());
//若是控件在ScrollView裏面,能夠先滑動,直到顯示該控件,而後進行點擊
onView(...).perform(scrollTo(), click());
複製代碼

6.斷言

//斷言控件可見
onView(...).check(matches(isDisplayed()))
//斷言控件不可見
onView(...).check(matches(not(isDisplayed())))
//斷言控件不存在
onView(...).check(doesNotExist())
複製代碼

7.列表中的定位

1)AdapterView

在AdapterView(好比ListView, GridView等)裏面,多個條目複用同一個佈局,onView()是不起做用的。這時候,要使用onData()。

好比,假設這樣一個ListView。

它的adapter的數據類是Map<String,Integer>。 如:

{"STR" : "item: 0", "LEN": 7}
複製代碼

定位到該條目,並點擊它:

//定位符合條件的item,若是不在屏幕上,Espresso會滑動屏幕,使其顯示出來
onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50"))))
    .perform(click());
複製代碼

若是是要定位到該條目中的某個子控件,好比,item右邊的TextView:

onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50"))))
    .onChildView(withId(R.id.item_size))
    .perform(click());
複製代碼

示例源碼:android-test裏面的AdapterViewTest

2)RecyclerView

RecyclerView跟AdapterView是不一樣的,onData()對它並不起做用。須要espresso-contrib包裏的工具類RecyclerViewActions幫助咱們。它爲咱們提供了幾個有用的靜態方法: 滾動到匹配的視圖。

scrollToHolder(Matcher<VH>)——滾動到匹配的ViewHolder。

scrollToPosition(int)——滾動到特定位置。

actionOnHolderItem(Matcher<VH>,ViewAction)——在匹配的ViewHolder上執行View操做。

actionOnItem(Matcher<View>,ViewAction)——對匹配的View執行View操做。

actionOnItemAtPosition(int,ViewAction)——對特定位置的View執行View操做。

下面是使用scrollToHolder(Matcher<VH>)方法,定位RecyclerView的中間條目:

1)先自定義一個匹配器Matcher

private static Matcher<CustomAdapter.ViewHolder> isInTheMiddle() {
        //ViewMatchers裏不少方法,其實就是自定義一個匹配器進行校驗,好比isDisplayed()
        return new TypeSafeMatcher<CustomAdapter.ViewHolder>() {
            @Override
            protected boolean matchesSafely(CustomAdapter.ViewHolder customHolder) {
                //檢驗item是不是中間的item
                return customHolder.getIsInTheMiddle();
            }

            /**
            * 生成一段對該對象的描述
            */
            @Override
            public void describeTo(Description description) {
                description.appendText("item in the middle");
            }
        };
}
複製代碼

2)定位RecyclerView的中間條目

//使用scrollToHolder(Matcher<VH>)方法,定位RecyclerView的中間條目
onView(ViewMatchers.withId(R.id.recyclerView))
                .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()));

//確認該條目有特定的文本描述
String middleElementText = "This is the middle!";
onView(withText(middleElementText)).check(matches(isDisplayed()));
複製代碼

示例源碼:RecyclerViewSample裏面的RecyclerViewSampleTest

參考資料:Espresso lists

8.Intent

Espresso提供了驗證跳轉其餘界面的Intent的Api。

1)使用IntentsTestRule替代ActivityTestRule

@Rule
public IntentsTestRule<DialerActivity> mActivityRule = new IntentsTestRule<>(
            DialerActivity.class);
複製代碼

另外,若是是跳轉到系統界面,好比撥打電話等,一般須要動態申請權限,而權限申請彈窗,會干擾測試,讓咱們失去對UI的控制。因此,須要使用GrantPermissionRule默認贊成權限。

@Rule
public GrantPermissionRule grantPermissionRule = GrantPermissionRule
                                                .grant("android.permission.CALL_PHONE");
複製代碼

2)使用intended()和intending()進行驗證。

intented()方法至關因而Mockito.verify()。 而intending()方法跟Mockito.when()相似,你能夠提供一個本身設定的響應給startActivityForResult()。

@Test
public void typeNumber_ValidInput_InitiatesCall() {
    //輸入一串有效的電話號碼
    onView(withId(R.id.edit_text_caller_number))
            .perform(typeText(VALID_PHONE_NUMBER), closeSoftKeyboard());
    //點擊跳轉到撥打電話界面。會真的跳轉。
    onView(withId(R.id.button_call_number)).perform(click());
    //驗證跳轉的Intent
    intended(allOf(
                hasAction(Intent.ACTION_CALL),
                hasData(INTENT_DATA_PHONE_NUMBER)));
}

@Test
public void pickContactButton_click_SelectsPhoneNumber() {
    //設定響應
    intending(hasComponent(hasShortClassName(".ContactsActivity")))
            .respondWith(new ActivityResult(Activity.RESULT_OK,
                    ContactsActivity.createResultData(VALID_PHONE_NUMBER)));
    //點擊跳轉到ContactsActivity,但前面有設定了響應,因此不會真的跳轉。
    onView(withId(R.id.button_pick_contact)).perform(click());
    //驗證響應結果
    onView(withId(R.id.edit_text_caller_number))
            .check(matches(withText(VALID_PHONE_NUMBER)));
}
複製代碼

3)防止界面跳轉

Espresso寫的測試代碼是要運行在真機或者虛擬機上面的,點擊跳轉界面時,會真的發生跳轉。若是你以爲這會干擾你的測試。能夠經過下面的設定,避免這種狀況。

@Before
public void stubAllExternalIntents() {
    //全部Intent都將被阻止
    intending(not(isInternal())).respondWith(new ActivityResult(Activity.RESULT_OK, null));
}
複製代碼

資料來源:Espresso-Intents

示例源碼:IntentsBasicSampleIntentsAdvancedSample

9.測試異步代碼

異步代碼測試,會存在一個問題:異步代碼一般比較耗時,可能它尚未執行完,相關的測試代碼已經執行完了。這樣,即便你的異步代碼有誤,但測試代碼顯示的結果永遠都是正常的。

Espresso爲咱們提供了一套機制:Idling resources。使用方法:

1)app的build.gradle下添加依賴

//注意,不是androidTestImplementation
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.0'
複製代碼

2)調整異步代碼

//異步任務開始以前的地方,添加該代碼
EspressoIdlingResource.increment();

//異步任務結束以後的地方,添加該代碼
if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
    EspressoIdlingResource.decrement();
}
複製代碼

EspressoIdlingResource是一個實現了IdlingResource接口的類。

3)在須要以前註冊空閒資源

@Before
public void registerIdlingResource() {
    IdlingRegistry.getInstance().register(EspressoIdlingResource.getIdlingResource());
}
複製代碼

4)完成使用後取消註冊閒置資源

@After
public void unregisterIdlingResource() {
    IdlingRegistry.getInstance().unregister(EspressoIdlingResource.getIdlingResource());
}
複製代碼

5)將異步代碼視爲同步代碼,放心寫測試代碼便可

擴展:

若是是你的異步代碼是RxJava寫的,能夠考慮下列的方法:

@Before
public void setup() {
    asyncToSync();
}

public static void asyncToSync() {
    RxJavaPlugins.reset();
    RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
    RxAndroidPlugins.reset();
    RxAndroidPlugins.setInitMainThreadSchedulerHandler(
            schedulerCallable -> Schedulers.trampoline());
}
複製代碼

上面的設置,會利用RxJavaPlugins將io線程轉換爲trampoline,異步代碼轉換爲同步代碼。好處是不用像Espresso同樣,入侵代碼。壞處是,異步操做切換成同步,可能會致使ANR。

資料來源:Idling resource

示例源碼:android-architectureIdlingResourceSample

10.Mock數據層

若是使用Espresso測試Activity,這已經算是一個端對端測試了。這時候,咱們該考慮mock數據層了。由於Model層可能會經過請求網絡等途徑,去獲取數據。而網絡的不穩定性、不固定的網絡請求結果,都會致使測試程序的不穩定性。

這裏提供兩種方案:

1)flavor

在gradle裏面配置不一樣的flavor:mock和prod。這時候,項目源碼的結構以下圖。

這時候,經過Build Variants,咱們就能夠構建不一樣的包。以下圖。

mock包使用的源碼是main和mock裏面的FakeTasksRemoteDataSource,顧名思義,model層使用的是假數據。而prod包使用的源碼是main和prod裏面TasksRemoteDataSource,是正式包,model層是從網絡、數據庫等處獲取真實數據。

這樣,咱們build一個mock包,就可使用假數據跑測試了。

Flavor的配置:Configure build variants

示例源碼:android-architecture

2)MockWebServer

MockWebServer是跟隨okhttp一塊兒發佈,咱們能夠用它來Mock服務器行爲。

1.集成 app的build.gradle下添加依賴:

testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
複製代碼

2.本地提供json數據

在src/test目錄下,新建resources文件夾,而後新建json文件夾,把響應的json放進裏面。以下:

3.建立MockWebServer

public class NetworkMockTest {

    private GithubRepository mGithubRepository;
    //使用@Rule標註一下。
    @Rule
    public MockWebServer server = new MockWebServer();

    @Before
    public void setup() throws IOException {
        //重設BASE_URL。不要使用真實的URL,否則會直接請求真實網絡。
        NetConstants.BASE_URL = server.url("/").toString();

        HttpService httpService = RetrofitFactory.createHttpService();
        mGithubRepository = new GithubRepository(httpService);
    }
}
複製代碼

4.模擬成功的網絡請求

@Test
public void getUserOnSuccess() throws IOException {
    InputStream inputStream = getClass().getClassLoader().getResourceAsStream("json/user.json");
    String json = Okio.buffer(Okio.source(inputStream)).readString(StandardCharsets.UTF_8);
    server.enqueue(new MockResponse().setBody(json));
    mGithubRepository.getUser()
            .test()
            .assertNoErrors()
            .assertComplete()
            .assertValue(userBean ->
                    userBean.getLogin().equals("TuFei"));
    }
複製代碼

5.模擬失敗的網絡請求

@Test
public void getUserOnError() {
    server.enqueue(new MockResponse().setResponseCode(404));
    mGithubRepository.getUser()
            .test()
            .assertError(HttpException.class)
            .assertErrorMessage("HTTP 404 Client Error");
}
複製代碼

6.模擬弱網下的網絡請求

@Test
public void getUserOnConnectTimeOut() throws IOException {
    InputStream inputStream = getClass().getClassLoader().getResourceAsStream("json/user.json");
    String json = Okio.buffer(Okio.source(inputStream)).readString(StandardCharsets.UTF_8);
    server.enqueue(new MockResponse()
            .setBody(json)
            .setResponseCode(504)
            //設置的響應超時時間是5秒
            //這裏模擬弱弱弱網,每10秒傳輸1kb
            .throttleBody(1024, 10, TimeUnit.SECONDS));
    mGithubRepository.getUser()
            .test()
            .assertNotComplete()
            .assertError(SocketTimeoutException.class);
}
複製代碼

注意:

1)這裏只是經過簡單的單元測試例子,介紹一下MockWebServer的使用。固然,若是是在src/androidTest下寫集成測試、端對端測試的時候要用,也須要經過androidTestImplementation引入依賴。

2)建立MockWebServer時,須要使用@Rule標註一下。不標註也能夠,但你就得在測試開始前手動調用MockWebServer.start()啓動服務器,測試結束後手動調用MockWebServer.shutdown()關閉服務器。MockWebServer本質就是TestRule,它幫咱們封裝了這些操做而已。(自定義TestRule,請參考淺談測試之JUnit。)

3)示例使用的是Retrofit請求網絡。因此,測試開啓前要重設baseUrl,不要使用真實的Url去調用,否則會走真實網絡。

示例源碼:UnitTest

MockWebServer更多使用技巧,建議參考:okhttp源碼

11.其餘相關資料

1)測試Activity:Test your app's activities

2)測試Fragment:Test your app's fragments

3)測試WebView:Web

4)Espresso API備忘圖:Espresso cheat sheet

後記

Espresso主要是用來寫集成測試、端對端測試,也就是測試UI交互。咱們只須要考慮對異步代碼的處理,以及對數據層的mock。由於是在真機或者模擬器上運行的,不須要像在單元測試裏面,忌憚Android類帶來的影響。也由於集成測試、端對端測試,是在一個更大的範圍內進行測試,因此舊代碼的設計問題,好比,Presenter/Model層不是依賴注入、Presenter/Model層摻雜了過多的Android代碼等等,都不影響你愉快地寫測試代碼。相比之下,單元測試,就痛苦得多了。

上述資料,源碼大部分來自android-testing下的子module。知識點整理自Espresso官網指南,並結合了一些官網不涉及的資料。

推薦閱讀官方的測試教程:Test apps on Android。它不只包含了Espresso教程,還包括一些不須要使用到Espresso,但一樣很重要的測試。下面列舉一二:

測試服務:Test your service

測試內容提供者:Test your content provider

測試跨應用UI:UI Automator

相關文章
相關標籤/搜索