Android UI 测试框架,在真机运行,相比手动测试,相当于把流程自动化了,并且自动监测结果。
这篇文章主要是阅读官方文档的结果,这渣英文,不敢说翻译。若有理解错误,望指正。
有些感觉用不着的就舍弃了没有看,当然整篇通读下来,感觉真的开发过程也不会去写这个测试吧,好像学了点用不着的屠龙术。不比单元测试,依然要编译运行到真机上,没敢用公司项目测,只是建了个最简单的 Demo,就感觉好慢,测试一次好慢。要是真的去写这测试,还得写许多代码,考虑许多过程,然后再编译,我怎么觉得,还不如 Instant Run 加自己手动操作测试来得快呢。
当然 Android 工程创建完就自动引入了这个框架,说明肯定是有作用的,大概是自己程度不够,没察觉它能提高多少效率。
设置
测试环境准备
开发者选项中关掉动画:
- Window animation scale 窗口动画缩放
- Transition animation scale 过渡动画缩放
- Animator duration scale 动画程序时长缩放
Gradle 配置
Module 的 gradle 文件中配置
android {
...
defaultConfig {
...
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
基本使用
在 src/androidTest
创建文件。
@RunWith(AndroidJUnit4.class)
@LargeTest
public class HelloWorldEspressoTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule =
new ActivityTestRule(MainActivity.class);
@Test
public void listGoesOverTheFold() {
onView(withText("Hello world!")).check(matches(isDisplayed()));
}
}
onView(withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher
.perform(click()) // click() is a ViewAction
.check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
- ViewMatchers – 当前 View 层级上匹配一个 View
- ViewActions – 对 View 执行某种行为,如点击
- ViewAssertions – 检查 View 的状态,类似单元测试中的断言
找到 View
有时候 View 可能没有对应的 R.id,或者虽然有但是不唯一。假设多个 View 共用 R.id.my_view
,onView(withId(R.id.my_view))
会报错,要通过额外内容进行过滤。
onView(allOf(withId(R.id.my_view), withText("Hello!")))
onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))
ViewMatchers 提供了若干过滤方法,具体参见 https://developer.android.com/reference/android/support/test/espresso/matcher/ViewMatchers
- 页面上任何可与用户交互的 View 都应该有 text 或 content description,如果通过
withText()
或withContentDescription()
不能获取到 View,说明代码写的不好,补上 text 或 content description。 - 用最少的过滤方法寻找 View,过滤方法越多,框架做的事情越重,比如能通过 withId 获取到唯一的 View,就不要再 withText 了。
- 如果 View 在 AdapterView 里,比如 ListView、GridView、Spinner,
onView()
方法可能无效,要用onData()
替换。
View 上执行操作
// 执行点击
onView(...).perform(click());
// 执行多个操作
onView(...).perform(typeText("Hello"), click());
// 如果在 ScrollView 里,要先滚动使 View 在当前页面显示出来,然后再执行其它动作
// 如果 View 本身就在页面中显示,srollTo 不起作用
onView(...).perform(scrollTo(), click());
可执行的操作参见 https://developer.android.com/reference/android/support/test/espresso/action/ViewActions
检查状态
主要通过 .check(matches())
方法,matches 里是寻找 View 的那些过滤方法,
// 断言 View 没有显示
onView(withId(R.id.bottom_left)).check(matches(not(isDisplayed())));
// 断言 View 不存在了,比如去了另一个 Activity
onView(withId(R.id.bottom_left)).check(doesNotExist());
Adapter View
比如 ListView、GridView、Spinner,有个 Adapter,它有好多个 Item,要寻找内容是 Americano 字符串的 Item
onData(allOf(is(instanceOf(String.class)), is("Americano")));
检查某个数据 Item 没有被加到一个 AdapterView 里,就是说还没有加载到它,Adapter 还没持有这个数据。先自定义一个 Matcher 类
private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
return new TypeSafeMatcher<View>() {
// 描述自己
@Override
public void describeTo(Description description) {
description.appendText("with class name: ");
dataMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
if (!(view instanceof AdapterView)) {
return false;
}
@SuppressWarnings("rawtypes")
Adapter adapter = ((AdapterView) view).getAdapter();
// 遍历当前 Adapter 持有的数据
for (int i = 0; i < adapter.getCount(); i++) {
// 和参数传进来 datamatcher 进行匹配
if (dataMatcher.matches(adapter.getItem(i))) {
return true;
}
}
return false;
}
};
}
然后检查
@SuppressWarnings("unchecked")
public void testDataItemNotInAdapter(){
onView(withId(R.id.list)) // 获取这个列表
.check(matches(not(withAdaptedData(withItemContent("item: 168")))));
}
}
list-showing-all-rows.png
假设 ListView,每个 Item 都是一个 Map,如 {"STR" : "item: 0", "LEN": 7}
,找到内容为 "item: 50" 的并点击
onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50"))).perform(click());
框架会自动滚动以显示 Item 并点击。
为了寻找 "item: 50" 的 Item,先寻找 AdapterView 用 Map 类型数据填充的,然后再寻找内容,可以定义出一个 Matcher,
return new BoundedMatcher<Object, Map>(Map.class) {
@Override
public boolean matchesSafely(Map map) {
return hasEntry(equalTo("STR"), itemTextMatcher).matches(map);
}
// 描述自己这个 Matcher
@Override
public void describeTo(Description description) {
description.appendText("with item content: ");
itemTextMatcher.describeTo(description);
}
};
定义了 BoundedMatcher 作为 Matcher,便可以使用 withItemContent(equalTo("foo"))
方法,为了方便可以将这个方法再封装
public static Matcher<Object> withItemContent(String expectedText) {
checkNotNull(expectedText);
return withItemContent(equalTo(expectedText));
}
然后再使用
onData(withItemContent("item: 50")).perform(click());
找到列表的 Item,还想找到 Item 里面的 View,使用 onChildView()
,比如
onData(withItemContent("item: 60")) // 找到 Item
.onChildView(withId(R.id.item_size)) // 找到 ItemView 里 id 为 R.id.item_size 的 View
.perform(click());
Recycler View
RecyclerView 的机制和过去的 ListView 这种不同,所以 onData()
方法也不适用了。它需要使用 RecyclerViewActions,有如下可执行的动作:
- scrollTo() - Scrolls to the matched View.
- scrollToHolder() - Scrolls to the matched View Holder.
- scrollToPosition() - Scrolls to a specific position.
- actionOnHolderItem() - Performs a View Action on a matched View Holder.
- actionOnItem() - Performs a View Action on a matched View.
- actionOnItemAtPosition() - Performs a ViewAction on a view at a specific position.
最后来张图
espresso-cheatsheet.png
网友评论