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