Espresso官網指南java
Google推行的測試庫,用於編寫簡潔、漂亮、可靠的Android UI測試。缺點是須要真機或模擬器配合測試,比較慢。android
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'
複製代碼
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
複製代碼
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);
}
}
複製代碼
在測試裏,能夠經過ApplicationProvider.getApplicationContext()得到Application的Context。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")))
複製代碼
最簡單的是經過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"))));
複製代碼
最簡單的就是點擊一個控件:
onView(...).perform(click());
複製代碼
也能夠對一個控件連續進行多個操做:
//輸入文字,而後進行點擊
onView(...).perform(typeText("Hello"), click());
//若是控件在ScrollView裏面,能夠先滑動,直到顯示該控件,而後進行點擊
onView(...).perform(scrollTo(), click());
複製代碼
//斷言控件可見
onView(...).check(matches(isDisplayed()))
//斷言控件不可見
onView(...).check(matches(not(isDisplayed())))
//斷言控件不存在
onView(...).check(doesNotExist())
複製代碼
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
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
示例源碼:IntentsBasicSample、IntentsAdvancedSample
異步代碼測試,會存在一個問題:異步代碼一般比較耗時,可能它尚未執行完,相關的測試代碼已經執行完了。這樣,即便你的異步代碼有誤,但測試代碼顯示的結果永遠都是正常的。
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-architecture、IdlingResourceSample
若是使用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源碼
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