Android 进阶之自动化测试Espreso
Espresso 文档
Espresso 环境搭建
在Android Studio 2.2中Expresso是默认引入的,你会发现在新创建的Android studio项目的build.gradle中已经帮我们添加好了如下的依赖,
并且已经帮我们创建好了androidTest目录,之前是没有这个待遇的,所以如果你还在为搭建环境困扰,不要纠结了升级你的Android Studio吧。
// Android JUnit Runner |
当然如果你们公司不能上网那么只能下载如下的jar包存在对应的libs中:
自动化测试静态jar包
一定要注意需要添加 defaultConfig 节点添加
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" |
否则会遇到明明编写了测试用例,但是还是报没有测试项的错误:
Espresso 组成
Expresso 是由三大部分组成的:
ViewMachers:寻找用来测试的View。 |
上述的操作可以简单归结为定位到某个控件,对某个空间执行某项操作,检查操作结束后的结果。
下面是Espresso的CheetSheet
ActivityTestRule
在开始自动化测试之前都会定义一个ActivityTestRule 它用于在测试的时候launch待测试的activity。Rules会在每个测试方法执行的时候执行,并且优先于 @Before方法。使用ActivityTestRule还能通过ActivityTestRule#getActivity()方法来获取某个activity的引用。
查找待操作的View
从上面的Espresso CheetSheet中可以看出查找视图可以用onView 和 onData两个方法,整体来说,onView()比较适用于UI比较简单的情况,一般我们获取这些简单的View可以通过id,text,Discription,Hint等属性来获取。但是需要注意的是上面的onView所找到的应该只能有一项,不能有多个View满足给出的条件,否则会报出AmbiguousViewMatcherException,如果遇到这种情况可以有两种方式一种是使用allof来添加多个条件来重新定位,另一个是找到具体的那个父View然后再定位。
但是,对于类似ListView, GridView, Spinner这种有UI复用的元素来说,,可能只有一部分显示在了屏幕上,对于没有显示在屏幕上的那部分数据,我们通过onView()是没有办法找到的。onView就不能胜任了只能借助onData来完成。
我们首先对比下onView和onData两个方法:
从返回值角度来看:
- onView返回的是ViewInteraction,它是一个符合匹配条件的唯一目标控件
- onData返回的是DataInteraction,它关注于数据。
从传入参数角度来看: - onView 传入的是Matcher
它是一个View匹配的匹配规则 - onData 传入的是Matcher<? extends Object> 它是针对数据匹配的匹配规则
也就是说onView偏向于直接匹配视图,而onData由于有些视图不能完全显示所以偏向于从数据入手。也就是Adapter对应的Data入手
在AdapterView中点击某项的例子:
先上代码:
public class ItemMatcher { |
在上面我们使用了onData来在数据集中查找,这里最重要的就是自定义的ItemMatcher,通过它可以找到name为”Jimmy 999”的那一项,在有多个ListView的时候可以使用inAdapterView来指定要查找的listView。找到之后会自动滚动到哪一项,执行点击操作。
public void testClickSpecialItem() { |
当然并不是所有的AdapterView都像上面那么复杂比如点击某项Spinner可以使用如下方式:
onData(allOf(is(instanceOf(String.class)),is("xxxx"))).perform(click()); |
在googlesample中给出的一个测试SimpleAdapter的例子如下:
@RunWith(AndroidJUnit4.class) |
RecyclerView 的用法:
当要测试RecyclerView的时候需要添加如下依赖:
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource |
这个不是默认添加的所以需要自己添加。并且这个库的某些依赖和support有冲突需要使用移除部分,也可以考虑上面说的离线的方式添加对应的jar包。
@Test |
hasSibling的用法
Sibling 的意思是 兄弟,姐妹;[生]同科,同属;[人]氏族成员
比如我们有一个ListView每一项都有一个显示“70”的TextView,这些TextView的text属性和id都是相同的,但是每一个TextView左边都放置有不同的TextView。如下图所示,此时可以通过如下的代码出目标项:
onView(allOf(withText("70"), hasSibling(withText("Jimmy: 12")))) |
这里需要注意的是这些项必须是在屏幕上可见的。
inRoot 验证Toast && AutoComplete
这个很简单就是判断Toast时候在Root之上。
onView(withText("Position = 99")) |
自定义ViewAction
一般我们要执行某个操作都是针对某个匹配的View的所以我们一般作为参数的都是Matcher,里面使用Matcher找到空间并添加ViewAction 在perform方法中会传入对应的View可以根据这个传入的View对其进行操作
下面是两个自定义的ViewAction:getText 会从View中获取文本,changeTextColor会改变文本的颜色。
public String getText (Matcher<View> matcher) { |
public void changeTextColor(Matcher<View> matcher) { |
要输入中文可以使用如下的方式:
onView(withId(R.id.imputString)).perform(replaceText(“各种格式文件”));
对于一般的View我们可以使用onView来定位,但是对于AdapterView(ListView GridView Spinner都是属于AdapterView)就需要onData来进行定位。
自定义Idling Resource
Idling Resource 用于需要等待异步计算或I/O操作完成的情况。使用它Espresso会等待app处于idle状态,才会执行下个动作和检查下个断言。
为了实现IdlingResource,需要重写3个函数:getName(),registerIdleTransitionCallback(),isIdleNow()。
* getName():必须返回代表idling resource的非空字符串,这个一般个人不做过多的纠结直接通过XXXX.class.getName() |
下面是一个在网上找的比较典型的例子,就是主程序使用IntentService处理一个耗时操作,等到耗时操作结束的时候进行后续的测试
|
注册和注销idling resource
为了让Espresso等待自定义的idling resource,你需要注册它。在测试代码的@Before方法中执行注册,在@After中执行注销。
@Before |
|
Espresso Intent 测试
要测试Intent就必须使用到如下的库:
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2' |
这个库在上面介绍的离线库中也可以找到。
在使用Intent进行测试的时候需要首先明确两个方法:
intending and intended
intending 相当于 when ,respondWith 相当于 thenReturn。intending还有个用法是用于检查返回的Intent携带的参数
intended 用于验证某个Intent是否被发送。
在使用这两个方法的时候必须先初始化Intents。用完后必须调用release释放。
- 检测某个Intent是否发送成功
private Matcher<Intent> expectedIntent; |
- 检测某个Intent 发送后的响应是否正确:
有的时候我们会按下某个按钮发送一个Intent来获取某个结果,这个结果一般在onActivityResult中返回。如果要测试返回某个结果时候的情况可以使用如下的代码来验证
public void testLoginPass() { |
验证发送出去的数据
Intent 一般会携带一些数据我们可以通过hasExtra来检查是否发送带有特定键值的数据,@Test
public void testLoginSuccess() throws InterruptedException {
onView(withId(R.id.email)).perform(typeText(TEST_USER));
onView(withId(R.id.password)).perform(typeText(TEST_PASS));
closeSoftKeyboard();
Intents.init();
onView(withId(R.id.email_sign_in_button)).perform(click());
//等待20秒
TimeUnit.SECONDS.sleep(20);
//检验返回的数据
intending(hasExtra(MainActivity.LOGIN_NAME, TEST_USER));
Intents.release();
}Espresso中提供了许多方法用于检测Intent的各个部分,下面是每个字段的对应关系
Intent.setData <–> hasData
Intent.setAction <–> hasAction
Intent.setFlag <–> hasFlag
Intent.setComponent <–> hasComponent发送一个带有数据的Intent检验返回的结果
@Before
public void setupImageUri() {
Resources resources = InstrumentationRegistry.getTargetContext().getResources();
Uri imageUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + resources
.getResourcePackageName(
R.mipmap.ic_launcher) + '/' + resources.getResourceTypeName(
R.mipmap.ic_launcher) + '/' + resources.getResourceEntryName(
R.mipmap.ic_launcher));
Intent resultData = new Intent();
resultData.setData(imageUri);
//创建一个Intent响应结果
mActivityResult = new Instrumentation.ActivityResult(
Activity.RESULT_OK, resultData);
}
@Test
public void testSelectImage() {
//Check the image is not displayed
onView(withId(R.id.imageView)).check(matches(not(hasDrawable())));
//Setup the intent
Intents.init();
//将响应结果绑定到具有如下条件的Intent上
Matcher<Intent> expectedIntent = allOf(hasAction(Intent.ACTION_PICK),
hasData(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI));
intending(expectedIntent).respondWith(mActivityResult);
//执行操作触发满足上述条件的Intent
onView(withId(R.id.fab_image)).perform(click());
//检验是否发送成功
intended(expectedIntent);
Intents.release();
//Check the image is displayed
onView(withId(R.id.imageView)).check(matches(hasDrawable()));
}测试启动Activity的Intent:
首先要设置不启动Activity@Rule
public final ActivityTestRule<TextHashActivity> rule =
new ActivityTestRule<>(TextHashActivity.class, false,
false); // not launch the activity@Test
public void testCorrectIntent() throws InterruptedException {
//设置启动Activity的Intent
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType(TextHashActivity.SUPPORT_TYPE);
intent.putExtra(Intent.EXTRA_TEXT, TEST_INPUT);
//启动Activity
rule.launchActivity(intent);
TimeUnit.SECONDS.sleep(1);
onView(withId(R.id.textview_sha1))
.check(matches(withText(TEST_SHA1)));
}另一种方式测试:
@Test |
想让每个Activity启动的时候都收到某个Intent
public class MainActivityTest {
public ActivityTestRule<MainActivity> mActivityRule =
new ActivityTestRule<MainActivity>(MainActivity.class) {
protected Intent getActivityIntent() {
Context targetContext = InstrumentationRegistry.getInstrumentation()
.getTargetContext();
Intent result = new Intent(targetContext, MainActivity.class);
result.putExtra("Name", "Value");
return result;
}
};
}排除外部Intent的影响
intending(not(isInternal())).(new ActivityResult(Activity.RESULT_OK, null)); |
Espresso 测试用例中使用源码中的数据
要想在测试用例中使用源码中的数据可以使用VisibleForTesting 这个注释符。
|
参考文章
http://michaelevans.org/blog/2015/09/15/testing-intents-with-espresso-intents/
https://github.com/pengj/Intent-Test
http://collectiveidea.com/blog/archives/2015/06/11/testing-for-android-intents-using-espresso/
https://segmentfault.com/a/1190000004338384
guides.codepath.com/android/UI-Testing-with-Espresso
http://www.jianshu.com/p/ad8b3514b852
https://segmentfault.com/a/1190000004355178
http://blog.csdn.net/qq744746842/article/details/51005604
http://stackoverflow.com/questions/32142463/how-to-stub-select-images-intent-using-espresso-intents
http://pengj.me/android/test/2015/10/17/expresso-test-intent.html