Espresso官网指南
Google推行的测试库,用于编写简洁、漂亮、可靠的Android UI测试。缺点是需要真机或模拟器配合测试,比较慢。
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文件夹里的。
2)模板代码
@RunWith(AndroidJUnit4.class) @LargeTest public class EspressoTest { @Rule public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(MainActivity.class); @Test public void testEspresso() { ... } } 复制代码
注意:
1.ActivityTestRule会立即初始化MainActivity,执行onCreate()、onResume()方法。
2.ActivityTestRule它是运行在@Before之前的。如果你不想立即初始化MainActivity,并且传递一些参数给MainActivity,可以使用ActivityTestRule另一个构造方法:ActivityTestRule( Class <T>
activityClass, boolean initialTouchMode, boolean launchActivity)
@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。
Espresso的核心类有4个。都是提供一系列静态方法的工具类:
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