基于Kotlin DSL的Espresso和UIAutomator的融合客户端自动化

2020-05-08 16:04:21 浏览数 (1)

前言

最近小编在探索端对端测试相关的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的可读性:
代码语言:javascript复制
@Test
fun shouldLoginToTheApp() {
  withLoginRobot {
    login("john_smith", "p@$$w0rd")
  } andThen {
    acceptTermsOfUse()
  } andThenWithPermissionRobot {
    acceptAllPermissions()
  } andVerifyThat {
    userIsLoggedIn()
  }
}

0 人点赞