what is Espresso
Espresso是Google官方提供的一个用于UI自动化测试的android框架。Google希望开发者在写完测试用例后,能一边自动跑着测试用例,一边享受着一杯浓厚香醇Espresso(浓咖啡)
使用群体
Google对该框架的使用人群描述为:
Espresso 的使用群体为坚信自动化测试是开发周期中必不可少的一部分的开发者。虽然它可被用来做黑盒测试,但 Espresso 会在对被测代码库熟悉的人手中火力全开。
why Espresso
市面上自动化测试的框架有很多,如:UIAutoMator,Robotium,Appium等,那为啥选择Espresso呢,主要考虑到:
- Espresso为Google官方出品以及力推的一款框架,在我们创建项目的时候就已经帮我们自动集成了相关sdk,说明了Google希望我们去用它。
- 相比于其他框架,Espresso规模更小,更简洁,API更加精确,编写测试用例比较简单,比较容易上手。
how to use
配置测试环境
为了避免花屏,官方强烈建议关闭模拟器或者真机的系统动画,在设备上的设置->开发者选项中禁用一下三项设置:
窗口动画缩放 过渡动画缩放 动画程序时长缩放
在app/build.gradle的dependencies节点添加:
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
implementation 'com.android.support.test.espresso:espresso-idling-resource:3.0.2'
在 android.defaultConfig 下添加下面的代码:
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
添加好以上依赖之后,我们需要创建一个测试配置,在android Studio中:
- 打开菜单 Run -> Edit Configurations
- 点击左上角的+号,选择Android Instrumented Tests
- 设置name,选择module,点击完成保存
然后编写好测试用例后,就可以选择这个配置执行了。
Espresso基础
Espresso主要组件有:
- Espresso - 与视图交互的切入点(参考 onView 和 onData)。也暴露了与任何视图都没有必然联系的 API(如 pressBack)。
- ViewMatchers - 实现了 Matcher<? super View> 接口的对象集合。你可以在 onView 方法中传入一个或多个此类对象来在当前的视图结构中定位一个视图。
- ViewActions - 可以作为参数传入 ViewInteraction.perform() 方法中的 ViewAction 的集合(如 click())。
- ViewAssertions - 可以作为参数传入 ViewInteraction.check() 方法中的 ViewAssertion 的集合。通常,你会使用带有视图匹配器的匹配断言来判断当前被选中视图的状态。
简单点说就是ViewMatchers找到你要操作的某个view,然后通过ViewActions对这个view进行操作,最后通过ViewAssertions来验证操作的结果。例如:
// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
.perform(click())
.check(matches(isDisplayed()))
onView
我们使用onView来查找我们要的视图,onView方法使用hamcrest匹配器在当前的视图结构中匹配到唯一的一个视图,一般是通过view的id来查:
onView(withId(R.id.my_view))
但是有时候可能某个id被多个视图所共用,此时如果你使用这个id将会抛出AmbiguousViewMatcherException的异常。这时候你可以通过比较两个视图,找出可以唯一确定的属性,如文本信息,可点击性,可见性等,如:
onView(allOf(withId(R.id.my_view), withText("Hello!")))
perform
当我们找到了指定的视图之后,就可以通过perform在该视图上执行ViewAciton,例如点击view:
onView(withId(R.id.my_view)).perform(click())
如果我们要操作的view的ScrollView中,并且未滑进屏幕,处于不可见。我们首先要通过scrollTo()方法使其显示,然后再执行对应操作:
onView(withId(R.id.my_view)).perform(scrollTo(),click())
注:如果视图已经是显示状态, scrollTo() 将不会对界面有影响。
check
通过check方法传递一个断言,来判断view状态是否符合我们预期,最常用的断言是matches(),它使用一个 ViewMatcher 来判断当前选中视图的状态。
例如,检查一个TextView的文本内容是否是“hello”:
onView(withId(R.id.textview)).check(matches(withText("hello")))
onData
在ListView或GridView等AdapterView中不能使用onView,因为AdapterView是动态加载view的方式,在某个时刻只加载了AdapterView的部分view,简单的 onview() 搜索不能找到当前没有被加载的视图。所以Espresso提供了onData来处理。
例如:
假设adapter的Item为:
public static class Item {
private final int value;
public Item(int value) {
this.value = value;
}
public String toString() {
return String.valueOf(value);
}
}
那么点击某个item的写法为:
@Test
public void clickItem() {
onData(withValue(27))
.inAdapterView(withId(R.id.list))
.perform(click());
//Do the assertion here.
}
public static Matcher<Object> withValue(final int value) {
return new BoundedMatcher<Object,
MainActivity.Item>(MainActivity.Item.class) {
@Override public void describeTo(Description description) {
description.appendText("has value " + value);
}
@Override public boolean matchesSafely(
MainActivity.Item item) {
return item.toString().equals(String.valueOf(value));
}
};
}
点击Item中的子view的方式为:
onData(withItemContent("xxx")).onChildView(withId(R.id.tst)).perform(click());
RecyclerView
onData不能用于RecyclerView的测试,原因是RecyclerView并不是继承于AdapterView。Espresso提供了专门用于RecyclerView使用的方案RecyclerViewActions
例如:
@Test
fun testList(){
onView(withId(R.id.rv_list)).perform(RecyclerViewActions.actionOnItemAtPosition<ListAdapter.ViewHolder>(7, click()))
}
如上为点击postion等于7的item。
异步-IdlingResource
我们目前的App会有大量的异步操作,如接口数据的获取,图片的加载等。Espresso并不知道你异步任务啥时候结束,如果我们直接按上述那些方式去操作,测试是过不了的。所以就引入了这里要讲的重点:IdlingResource
在src/main/java下创建IdlingResource的实现类:
class SimpleIdlingResource : IdlingResource {
private val counter: AtomicInteger by lazy { AtomicInteger(0) }
@Volatile
private var resourceCallback: IdlingResource.ResourceCallback? = null
override fun getName(): String {
return "SimpleIdlingResource"
}
/**
* 如果返回0,说明当前处于空闲状态
*/
override fun isIdleNow(): Boolean {
return counter.get() == 0
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.resourceCallback = callback
}
/**
* 增长
*/
fun increment() {
counter.getAndIncrement()
}
/**
* 减少
*/
fun decrement() {
val counterCount = counter.decrementAndGet()
if (counterCount == 0) {
//告诉Espresso,当前处于空闲状态
resourceCallback?.onTransitionToIdle()
}
if (counterCount < 0) {
throw IllegalArgumentException("Counter has been corrupted!")
}
}
}
接着在我们待测试的Activity中,在异步开始的时候添加SimpleIdlingResource.increment(),在异步结束的时候添加SimpleIdlingResource.decrement(), 并添加 getIdlingResource() 方法方便我们在测试方法中调用
class IdlingTestActivity:AppCompatActivity() {
private val mSimpleIdlingResource by lazy { SimpleIdlingResource() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_idling_test)
btn_idling_click.setOnClickListener {
mSimpleIdlingResource.increment()
handler.sendEmptyMessageDelayed(1,3000)
}
}
private var handler = object : Handler() {
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
tv_idling_word.text = "world"
if(!mSimpleIdlingResource.isIdleNow){
mSimpleIdlingResource.decrement()
}
}
}
@VisibleForTesting
fun getIdlingResource():SimpleIdlingResource{
return mSimpleIdlingResource
}
}
最后,编写我们的测试用例
@RunWith(AndroidJUnit4::class)
class IdlingTest {
private var mIdlingResource:IdlingResource?=null
@get:Rule
val mRuleActivity = ActivityTestRule<IdlingTestActivity>(IdlingTestActivity::class.java)
@Before
fun before(){
mIdlingResource = mRuleActivity.activity.getIdlingResource()
IdlingRegistry.getInstance().register(mIdlingResource)
}
@Test
@LargeTest
fun testIdling(){
onView(withId(R.id.btn_idling_click)).perform(click())
onView(withId(R.id.tv_idling_word)).check(matches(withText("world")))
}
@After
fun after(){
IdlingRegistry.getInstance().unregister(mIdlingResource)
}
}
另外,Espresso 提供了一个实现好的CountingIdlingResource类,所以如果没有特别需求的话,直接使用CountingIdlingResource即可。
注意事项
Getting Started With Espresso 2.0这个视频中提到了2个写测试用例时的注意项:
-
避免Activity的层级跳转,测试用例尽量只在单个Activity内完成。Activity层级跳转越多,越容易出错
-
强烈不推荐,直接获取View的对象,调用View的方法来模拟用户操作。应该统一使用Espresso提供的方法
测试用例,特别是UI自动化测试用例,应该尽量保持逻辑简单,覆盖关键路径就足矣。因为UI变动是很频繁的,越复杂,维护成本就越高,投入产出比就会自然降低了。