前言
最近小编在探索端对端测试相关的topic,在Android端的自动化测试上,可供我们选择的库并不是很多,而其中小编使用最多的两个库分别是Espresso和UIAutomator。尽管两者都可以达成我们的最终目的,但实现的过程还是有所区别的:
为了进行充分的端对端测试,我们便需要利用好两者的优势,以实现在合适的地方对程序进行合适的自动化测试。然而,如果我们想设计一套自顶向下,设备、接口、代码层级均可自动化执行且有一定校验的框架或系统时,就会发现这两个完全不同语法的库融合一起后,可读性和可维护性几乎等于零。
因此,本文提出了一种基于Kotlin DSL写法的Espresso和UIAutomator融合方案,解决在不同库下的客户端自动化框架、用例的可读性、可维护性问题。
Espresso
在Espresso中,我们一般会处理三种类型的对象:匹配器、ViewAction和ViewAssertions。按照语法,结合这三种对象,我们可以实现如以下click这一类的操作,如下所示:
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进行交互了
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语法更合逻辑且更易于掌握,因此历史代码可以更轻松地移交给其他同事。
click on button(R.id.activityLoginBtnLogin)
上面是基于Kotlin DSL实现的一个例子,是不是很清晰易懂呢?以下是融合UIAutomator和Espresso语法的一个实例:
Espresso语法:
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语法:
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))
}
}
融合语法:
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() }
}
}
后续优化思考
@Test
fun shouldLoginToTheApp() {
withLoginRobot {
login("john_smith", "p@$$w0rd")
} andThen {
acceptTermsOfUse()
} andThenWithPermissionRobot {
acceptAllPermissions()
} andVerifyThat {
userIsLoggedIn()
}
}