如何正确使用Espresso来测试你的Android程序
UI测试在Android平台上一直都是一个令人头痛的事情, 由于大家平时用的很少, 加之很多文档的缺失, 如果很多东西从头摸索,势必踩坑无数.
自Android24正式淘汰掉了InstrumentationTestCase(位于android.test包), 推出Espresso(位于android.support.test包), Google一直致力于降低UI测试的门槛.
了解测试金字塔的同学可能知道,UI测试属于功能测试(Functional Test), 或者按照其他的划分也属于集成测试(Integration Test), Google推出了UIAutomator与Espresso来分别处理跨App间的测试(黑盒测试)以及App内的测试(白盒测试).
测试步骤类似,分为:
- 查找元素
- 触发行为
- 检测结果
本文分为三部分, 第一部分简单介绍如何使用Espresso, 第二部分分析如何处理诸如异步, 依赖注入, 程序结构对UI测试的影响以及提供解决办法, 第三部分提供源码以及一些Reference的地址.
Part I
如何配置
1.需要在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'
2.在gradle的android.defaultConfig里指定TestRunner
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
3.书写测试文件,通过AndroidJUnit4来跑即可,使用Activity Rule来启动你的Activity.
@Rule
@JvmField
var activityTestRule: ActivityTestRule<MainActivity> = ActivityTestRule<MainActivity>()
4.添加测试.
onView(withText("Hello world!")).check(matches(isDisplayed()));
5.运行
./gradlew connectedAndroidTest
或在IDE中进行运行.
以上步骤写的比较简略, 如果第一次使用, 可参考官方文档.
Part II
貌似已经会了, 打钩[x]?
对于简单的UI其实上面的5步已经完全足够, 这也是Espresso好用的地方, 将UI测试写的跟普通的Unit Test一样简单.
但是随着你的UI变得复杂, 很多问题接踵而至.
其根本原因在于, Espresso系统在处理内置UI渲染(包括WebView)的异步操作都没有问题, 它会等待页面的渲染与加载, 而你自己如果有异步逻辑, 可能测试进程不会等待其完成而结束, 导致测试失败.
而采用Unit Test将无论是RxJava的Scheduler或者是Excutor替换成同一个线程的方法没法在UI Test中使用. 原因是UI操作只能在创建它的线程使用(UI 线程), 而如果你用了网络或者Room之类的数据库, 它又无法在UI线程使用, 相互矛盾, 进退两难.
所以这个时候就需要使用Espresso提供的IdleResource, 来通知系统是否Idle或者Busy.
什么时候该使用IdleResource
其实IdleResource的官方文档里面有指出, 如果你的测试里有使用:
- Thread.sleep()
- Retry
- CountDown ...
来保证你的测试工作正常, 那么意味着你应该使用IdleResource了.
或许刚刚接触Espresso的你可能还没有意识到问题所在, 还没有使用Work Around的方法来解决问题, 换个角度来说可能更好理解.
如果你所测试程序里有使用:
- Databinding
- LiveData
- 通过非AsyncTask实现的异步操作
- Fragment跳转
- 等等...
那么就意味着你需要使用IdleResource来保证你的测试能顺利进行, 否则Test Case可能在程序异步操作未执行时就已经关闭了.
如何使用IdleResource
IdleResource的三个关键接口都非常Straigtforward.
1:
fun getName(): String
每一个IdleResource都应该有唯一的Name来注册到系统里, 不能重复.
2:
fun isIdleNow(): Boolean
Espresso会从UI线程调用, 通过这个方法来获得是否进入Idle状态.
3:
fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback)
当该IdleResource被使用时, Espresso会注册该callback, 当background job执行完毕后, 需要调用callback.onTransitionToIdle()通知(如果已经是Idle状态, 调用也不影响, 所以很多简单的实现都是将这个调用放在isIdleNow中, 判断已经idle就调用, 虽然google的best practice里说不要这样), 该调用会通知UI线程, 并可以在任何线程调用.
在使用IdleResource的时候, 通常是通过注册Rule来驱动的, 这个就需要继承TestWatcher.
复写它的starting与finished方法, 通过IdlingRegistry.getInstance().register与IdlingRegistry.getInstance().unregister来注册/反注册IdleReource, 当然可能需要在finished的时候drain掉所有在运行的Task.
给一个简单的例子把.
class SampleIdleResourceRule : TestWatcher() {
private val idlingResource: IdlingResource = xxx
override fun starting(description: Description?) {
IdlingRegistry.getInstance().register(idlingResource)
super.starting(description)
}
override fun finished(description: Description?) {
//drain all the pending task here if needed.
IdlingRegistry.getInstance().unregister(idlingResource)
super.finished(description)
}
}
举个IdleResource的例子吧.
1.使用LiveData等Archtecture Component组件
我们知道LiveData是一个订阅系统, 是必涉及后台线程, 比较方便的是它自己内部已经调用了IdleResource来增加/减少后台job, 所以直接使用系统提供的CountingTaskExecutorRule.
由于Resource name不能重复, 所以为了绕过这个检测, 需要继承CountingTaskExecutorRule来复写getName.
具体可以参考google的TaskExecutorWithIdlingResourceRule.
Google还提供了Databinding的Rule, 可以参考.
2.等待弹框结束
一般情况下我们使用DialogFragment来弹框, 如果我们去check一些text被dialog遮挡, 就必须等待其消失后在进行检查.
这时我们可以通过findFragmentByTag来检测该弹框是否dismiss.
class DialogIdlingResource(
private val manager: FragmentManager,
private val tag: String) : IdlingResource {
private var resourceCallback: IdlingResource.ResourceCallback? = null
override fun getName(): String = "xxx"
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
resourceCallback = callback
}
override fun isIdleNow(): Boolean {
val idle = manager.findFragmentByTag(tag) == null
if (idle) {
resourceCallback?.onTransitionToIdle()
}
return idle
}
}
3.Delegate Executors/Scheduler
如果有异步处理逻辑, 大多都位于Repository/ViewModel层, 这部分会被Mock, 但也有一些UI逻辑可能会用到Excecutor. 如RecyclerView的DiffUtil, 需要传入一个Executor来做异步Diff, 这时我们就需要一个Excecutor的IdlingResource, 并把它里面的Delegate赋值给UI.
这部分可以参考Google GithubBrowser Sample的CountingAppExecutorsRule.
应该怎么测试, 需要测试什么?
虽然Espresso测试是集成测试, 但是由于涉及到异步逻辑导致Test Case无法按照预期进行的问题时而存在, 且有时候无法通过IdlingResource来解决.
比如涉及到多个Fragment的跳转, 就会发生在Fragment未打开时Test Case就挂掉的情况.
再比如使用RxJava, 在Espresso3.x + RxJava2.x的情况下, 即便将Scheduler代理给IdlingResource也无法保证整个业务流程完整走下来, 异步操作仍无法完整运行, 具体问题可参考Jake大神RxIdler的Issue.
所以测试起来就有一些原则需要遵守, 才能保证整个流程的可测性.
- 最好对每一个Fragment进行单独测试, Mock所依赖的部分, 如网络, 数据模块, 如果涉及Fragment跳转逻辑, 通过继承来复写进行测试.
- 如果使用了RxJava, 需要将其封装在Repository或者Presenter/ViewModel中进行整体的Mock.
- 如果使用了Dagger2.android进行自动注入, 最好对测试部分自定义TestRunner提供一个空的Application来Disable注入, 对所测试Fragment注入对象进行手动赋值.
- 如果Activity有注入逻辑, 最好将其解耦到Fragment, 因为Espresso的Activity是通过ActivityRule来启动, 无法进行直接手动注入.
- 如果无法Move到Fragment, 或者不想... 那就需要在测试里构建自己的Dagger Component, 对于使用Dagger2.android自动注入的, 还需要手动创建Fake的DispatchingAndroidInjector完成手动注入.
- 如果未使用Dagger2.android, 通过AndroidInjector来注入的, 可以忽略与注入相关的item.
能再讲的仔细一些吗?
1.单独测试Fragment的好处是可以解耦Fragment之间的跳转, 往往Fragment都是UI流程中的一个环节, 当逻辑完成时会跳向下一Fragment. 可以创建一个空Activity来专门用于显示该Fragment, 并且在测试的setUp里commit该Fragment.
class TestActivity {
fun showFragment()
}
@RunWith(AndroidJUnit4::class)
class XXXFragmentTest {
@Rule
@JvmField
val activityRule = ActivityTestRule(XXXFragment::class.java)
@Before
fun init() {
//1. init fragment
//2. assign mock data
activityRule.showFragment(xxx)
}
@Test
fun testXXX() {
xxx
}
}
2.由于常常会需要继承需要测试的Fragment来复写一些类, 对于使用Dagger.android自动注入的, 该子Fragment又未通过@ContributesAndroidInjector进行注册, 往往需要自定义TestRunner, 然后手动注入Fragment.
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(...) {
return ...TestApp:class...
}
}
android {
defaultConfig {
testInstrumentationRunner "xxx.CustomTestRunner"
}
}
class TestApp : Application() {}
@RunWith(AndroidJUnit4::class)
class XXXFragmentTest {
//activity rule
...
val testFragment = TestFragment()
@Before
fun init() {
testFragment.xxx = mockXXX
...
activityRule.activity.showFragment(testFragment)
}
@Test
fun testXXX() {
onView...check(...)
assertTrue(testFragment.isXXXShow)
}
class TestFragment : XXXFragment() {
var isXXXShow = false
override fun showXXX() {
isXXXShow = true
}
}
}
3.如果Activity有注入逻辑与业务逻辑, 并且不想抽到Fragment中去, 则需要创建Fake的Injector保证可以完成注入,
fun createFakeInjector(block: T.() -> Unit): DispatchingAndroidInjector<Activity> {
...
}
@RunWith(AndroidJUnit4::class)
class XXXActivityTest {
@Rule
@JvmField
var activityRule = object : ActivityTestRule<XXX>(XXX::class.java) {
val app = ...get application
app.dispatchingAndroidInjector = createFakeInjector<XXX>() {
//手动注入
xxx = mockXXX
`when`(xxx).thenReturn(xxx)
}
}
}
4.为了支持需要通过继承Fragment来完成测试的Case, 还需要对测试模块创建自己的Component来注册从而进行Fake Injector的创建 (类似3, 只是Application/Activity可能为Test版本).
Grale
dependencies {
kaptAndroidTest 'com.google.dagger:dagger-android-processor:2.X'
}
@Component(modules = [
AndroidInjectionModule::class,
AndroidSupportInjectionModule::class,
...主App所注册的所有Module,
TestActivityModule::class])
interface TestCompnent {
fun inject(xxx: XXX)
...
}
@Module
abstract class TestActivityModule {
//通过`ContributesAndroidInjector`注册你的TestActivity, 以及TestFragment
}
class TestApp : Application(), HasActivityInjector {
@Inject
lateinit var injector: DispatchingAndroidInjector<Activity>
}
class TestActivity : Activity(), HasSupportFragmentInjector {
@Inject
lateinit var injector: DispatchingAndroidInjector<Fragment>
}
Part III
如果还不是很明白可以查看代码
Disable注入的在这里:
Google的Demo GithubBrowser
跟注入相关的在这里:
自己的Demo
Reference
- https://proandroiddev.com/activity-espresso-test-with-daggers-android-injector-82f3ee564aa4
- https://github.com/SabagRonen/dagger-activity-test-sample
- https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample
- https://developer.android.com/training/testing/espresso/idling-resource
- http://blog.sqisland.com/2015/07/espresso-wait-for-dialog-to-dismiss.html
欢迎关注微信公众号: 圣骑士Wind

如何正确使用Espresso来测试你的Android程序的更多相关文章
- [译] 使用 Espresso 隔离测试视图
原文地址:Testing Views in Isolation with Espresso 原文作者:Ataul Munim 译文出自:掘金翻译计划 译者:yazhi1992 校对者:lovexiao ...
- 使用calabash测试开源中国Android客户端
Calabash-android是支持android的UI自动化测试框架,前面已经介绍过<中文Win7下成功安装calabash-android步骤>,这篇博文尝试测试一个真实应用:开源中 ...
- Android训练课程(Android Training) - 测试你的Android Activity
你应该开始编写和启动测试作为你的android程序开发周期的一部分.写的好的测试能够帮助你更早的发现bug和使你对你的代码有信心. 一个测试用例定义了一些对象和方法的集合,用于启动多样的彼此独立的测试 ...
- Android程序测试
一.建立测试环境 安装了Android Developer Tools (ADT) 插件的Eclipse将为你创建,构建,以及运行Android程序提供一个基于图形界面的集成开发环境.Eclipse的 ...
- 手机测试Android程序
手机测试Android程序 上传者:sanpi329 我也要“分享赚钱” 2014/7/9 关注(23) 评论(0) 声明:此内容仅代表网友个人经验或观点,不代表本网站立场和观点. ...
- JustMock .NET单元测试利器(三)用JustMock测试你的应用程序
用JustMock测试你的应用程序 本主题将指导您通过几个简单的步骤来使用Telerik®JustMock轻松测试您的应用程序.您将理解一个简单的原理,称为Arrange / Act / Assert ...
- 20162311 编写Android程序测试查找排序算法
20162311 编写Android程序测试查找排序算法 一.设置图形界面 因为是测试查找和排序算法,所以先要有一个目标数组.为了得到一个目标数组,我设置一个EditText和一个Button来添加数 ...
- ESP8266 SDK开发: 测试下诱人的程序
前言 这一节测试一下诱人的程序 实现的功能,APP通过SmartConfig给Wi-Fi模块配网并绑定设备,然后通过MQTT远程控制开发板的继电器, APP显示ESP8266采集的温湿度数据. 简而言 ...
- ESP8266 LUA脚本语言开发: 测试下诱人的程序
前言 这一节测试一下诱人的程序 实现的功能,APP通过SmartConfig给Wi-Fi模块配网并绑定设备,然后通过MQTT远程控制开发板的继电器, APP显示ESP8266采集的温湿度数据. 简而言 ...
随机推荐
- [Swift]LeetCode320. 通用简写 $ Generalized Abbreviation
Write a function to generate the generalized abbreviations of a word. Example: Given word = "wo ...
- [Swift]LeetCode421. 数组中两个数的最大异或值 | Maximum XOR of Two Numbers in an Array
Given a non-empty array of numbers, a0, a1, a2, … , an-1, where 0 ≤ ai < 231. Find the maximum re ...
- [Swift]LeetCode770. 基本计算器 IV | Basic Calculator IV
Given an expression such as expression = "e + 8 - a + 5" and an evaluation map such as {&q ...
- iFace安全专家揭秘:存放在区块链钱包中的比特币,其实已经早就不属于你……
自MoreToken钱包跑路之后,2019年3月以来陆续多个钱包.交易所跑路,造成了大量用户账户被盗,仅MoreToken钱包用户损失总价值就达12.2亿人民币,用户损失惨重.为什么这么多钱包.交易所 ...
- 英语笔记3(git)
备注 一: Staging Modified Files Let’s change a file that was already tracked. (tracked 表示该文件已经被git管理过,再 ...
- 在react中使用vis.js
import React from 'react'; import {message} from 'antd' import vis from 'vis'; import {api as http ...
- Python内置函数(41)——max
英文文档: max(iterable, *[, key, default]) max(arg1, arg2, *args[, key]) Return the largest item in an i ...
- Redis学习——Linux环境下Redis的安装(一)
一.关于Redis Redis最为一款开源的key-value存储系统,自推出到现在一直受到编程人员的喜爱.它支持存储多种value类型,String .List .Set .Zset .Hash.这 ...
- java基础(五)-----关键字static
在Java中并不存在全局变量的概念,但是我们可以通过static来实现一个“伪全局”的概念,在Java中static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,当然也可以修饰代码块. ...
- Python爬虫入门教程 16-100 500px摄影师社区抓取摄影师数据
写在前面 今天要抓取的网站为 https://500px.me/ ,这是一个摄影社区,在一个摄影社区里面本来应该爬取的是图片信息,可是我发现好像也没啥有意思的,忽然觉得爬取一下这个网站的摄影师更好玩一 ...