前言
最近小编在探索端对端测试相关的topic,在Android端的自动化测试上,可供我们选择的库并不是很多,而其中小编使用最多的两个库分别是Espresso和UIAutomator。尽管两者都可以达成我们的最终目的,但实现的过程还是有所区别的:
- Espresso是用于Android测试的白盒解决方案,以沙盒化的形式测试当前应用程序。
- UIAutomator是一个常用的Android端黑盒测试解决方案,它在设备维度上运行,故而提供了应用程序及程序之外的操作及测试方法。
为了进行充分的端对端测试,我们便需要利用好两者的优势,以实现在合适的地方对程序进行合适的自动化测试。然而,如果我们想设计一套自顶向下,设备、接口、代码层级均可自动化执行且有一定校验的框架或系统时,就会发现这两个完全不同语法的库融合一起后,可读性和可维护性几乎等于零。
因此,本文提出了一种基于Kotlin DSL写法的Espresso和UIAutomator融合方案,解决在不同库下的客户端自动化框架、用例的可读性、可维护性问题。
Espresso
在Espresso中,我们一般会处理三种类型的对象:匹配器、ViewAction和ViewAssertions。按照语法,结合这三种对象,我们可以实现如以下click这一类的操作,如下所示:
代码语言:javascript复制Espresso.onView(Matchers.withId(R.id.activityLoginBtnSubmit)).perform(ViewActions.click())
UIAutomator
相较于Espresso,黑盒的UIAutomator使用要复杂得多。比如我们要查询UI层次结构中的特定对象,就需要设定好一些先决条件:
1、从InstrumentationRegistry获取上下文
2、将资源ID转换为资源名称
3、创建UIDevice对象,它在UIAutomator中属于God对象,即每次调用都会需要用到UIDevice实例
4、定义UISelector,UISelector的作用是可以通过资源ID查询想要的UI组件,但是UIAutomator中没有这种方法,所以我们需要用到步骤2中的资源名称,通过资源名称查询UI组件,进而实现UISelector
5、通过使用UIDevice和UISelector实例化UIObject。实例化完成后,我们就可以和UIComponent进行交互了
代码语言:javascript复制val instrumentation = InstrumentationRegistry.getInstrumentation()
val uiDevice = UiDevice.getInstance(instrumentation)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val loginButtonSelector = UiSelector().resourceId(appContext.resources.getResourceName(
R.id.activityLoginBtnSubmit
)
)
val loginButton = uiDevice.findObject(loginButtonSelector)
loginButton.click()
现在,若我们将Espresso和UIAutomator结合起来,通过UI组件的动作来检查层次结构深处的某些View,那么就需要同时使用Espresso对象和UIAutomator对象(其中还包含了UIAutomator资源初始化等工作)。假设这一条case的编写、改进、维护成本在一个季度内评估为30min,那么1000条case维护起来的工作量可想而知。
Kotlin DSL带来的新思路
还好小编在调研阶段就意识到了这个问题,因此决定使用Kotlin的功能编写DSL以统一两个库的语法。DSL(domain specific language),即领域专用语言:专门解决某一特定问题的计算机语言,比如大家耳熟能详的 SQL 和正则表达式就属于DSL。而在Kotlin中,DSL 则是对 Kotlin 所有语法糖的一个大融合,它的代码结构通常是链式调用、lambda 嵌套,并且接近于日常使用的英语句子,我们可以愉悦的使用 DSL 风格的 API,同时,由于DSL语法更合逻辑且更易于掌握,因此历史代码可以更轻松地移交给其他同事。
代码语言:javascript复制click on button(R.id.activityLoginBtnLogin)
上面是基于Kotlin DSL实现的一个例子,是不是很清晰易懂呢?以下是融合UIAutomator和Espresso语法的一个实例:
Espresso语法:
代码语言:javascript复制class MainActivityTest {
@Test
fun shouldLoginDemoUser(){
onView(withId(R.id.activityLoginEditTextUsername)).perform(typeText("dummyUsername"))
onView(withId(R.id.activityLoginEditTextPassword)).perform(typeText("dummyPassword"))
onView(withId(R.id.activityLoginBtnLogin)).perform(click())
Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.name))
}
}
UIAutomator语法:
代码语言:javascript复制class MainActivityTest {
@Test
fun shouldLoginDemoUser(){
val instrumentation = InstrumentationRegistry.getInstrumentation()
val uiDevice = UiDevice.getInstance(instrumentation)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val usernameSelector = UiSelector().resourceId(appContext.resources.getResourceName(
R.id.activityLoginEditTextUsername
)
)
val usernameTextField = uiDevice.findObject(usernameSelector)
usernameTextField.text = "dummyUsername"
val passwordSelector = UiSelector().resourceId(appContext.resources.getResourceName(
R.id.activityLoginEditTextPassword
)
)
val passwordTextField = uiDevice.findObject(passwordSelector)
passwordTextField.text = "dummyPassword"
val loginButtonSelector = UiSelector().resourceId(appContext.resources.getResourceName(
R.id.activityLoginBtnLogin
)
)
val loginButton = uiDevice.findObject(loginButtonSelector)
loginButton.click()
Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.name))
}
}
融合语法:
代码语言:javascript复制class MainActivityTest {
@Test
fun shouldLoginDemoUser(){
typeText("dummyUsername") into text(R.id.activityLoginEditTextUsername)
typeText("dummyPassword") into text(R.id.activityLoginEditTextPassword)
click on button(R.id.activityLoginBtnLogin)
MainActivity::class verifyThat { itIsDisplayed() }
}
}
后续优化思考
- 在后续项目发展过程中,我们肯定会在UI组件上使用越来越多的操作和断言,因此DSL的量级会随着时间不断增长。在项目成熟度发展到某一节点时,维护功能集合会变得很困难,因此我们必须对其进行整理集合,使其独立于我们正在测试的程序。当前Github上已有Android Test KTX可供大家使用。
- 尽管UIAutomator对我们来说效果很好,但这也是造成大多数麻烦的原因。我们如果要自行更新或增加Kotlin DSL库的内容,可以将UIAutomator和Espresso相同的操作通过Espresso实现,并集合在库中。
- 可以考虑将DSL结合Kotlin的Robot模式使用,进一步提升测试case的可读性:
@Test
fun shouldLoginToTheApp() {
withLoginRobot {
login("john_smith", "p@$$w0rd")
} andThen {
acceptTermsOfUse()
} andThenWithPermissionRobot {
acceptAllPermissions()
} andVerifyThat {
userIsLoggedIn()
}
}