Instrumentation tests

Instrumentation tests有一点不同。它们通常被使用在UI交互上,我们需要一个应用程序实例跑的同时执行测试。达到这个,我们就需要在设备上部署并运行。

这类的测试必须要放在androidTest文件夹中,我们必须要修改Build Variants区域的Test ArtifactAndroid Instrumentation Tests。实现instrumentation的官方库是Espresso,它通过Actionsfilter以及检测结果的ViewMatchersMatchers可以帮助我们更简单地使用。

配置比之前更加难一点。我们需要下载额外的库和Gradle的配置。好事是Kotlin的测试不需要添加额外的东西,所以如果你已经知道怎么去配置Espresso,它将对你来说是很简单的。

首先,在defaultConfig中指定test runner

defaultConfig {
    ...
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

当你处理完该runner,然后增加其它的依赖,这次是用androidTestCompile。这种方式,这些库只会再编译运行instrumentation tests的时候才被增加:

dependencies {
    ...
    androidTestCompile "com.android.support:support-annotations:$support_version"
    androidTestCompile "com.android.support.test:runner:0.4.1"
    androidTestCompile "com.android.support.test:rules:0.4.1"
    androidTestCompile "com.android.support.test.espresso:espresso-core:2.2.1"
    androidTestCompile ("com.android.support.test.espresso:espresso-contrib:2.2.1"){
        exclude group: 'com.android.support', module: 'appcompat'
        exclude group: 'com.android.support', module: 'support-v4'
        exclude module: 'recyclerview-v7'
}

我不想花大量的时间去讲这些,但是这里有为什么需要这些库的简短原因:

  • support-annotations:其它库中需要使用到。
  • runner:这是test runner,就是我们再defaultConfig中指定的那个。
  • rules:包括一些测试inflate启动activity的规则。我们将会在我们的例子中使用这些规则。
  • espresso-coreEspresso的基本实现,它让instrument tests更加容易。
  • espresso-contrib:它增加了其它额外的功能,比如支持RecyclerView测试。我们不得不排除掉一些它的依赖,因为我们已经在这个项目中使用到了,否则测试会出错。

我们现在来创建一个简单的例子。测试将会点击forecast列表的第一行,然后它会判断是否能找到一个id为R.id.weatherDescription的view。这个view是在DetailActivity中的,这表示我们在测试在RecyclerView里面点击后是否可以成功地导航到详情页面。

class SimpleInstrumentationTest {

    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java)

    ...
}

首先我们需要指定它运行时使用AndroidJUnit4。然后,创建一个activity规则,它会实例化一个测试需要的activity。在Java中,你可以使用@Rule。但是如你所知,字段和属性是不一样的,所以如果你像那样去使用的话,执行会失败因为访问属性中的字段是不是public的。你需要加注解的是在getter上面。Kotlin允许指定get或者set在Rule的名字前面。在这个例子中,只要些@get:Rule

之后,我们已经准备好创建第一个测试了:

@Test fun itemClick_navigatesToDetail() {
    onView(withId(R.id.forecastList)).perform(
            RecyclerViewActions
                .actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()))
    onView(withId(R.id.weatherDescription))
           .check(matches(isAssignableFrom(TextView::class.java)))
}

函数加上了@Test注解,这根我们使用unit test的方式一样。我们可以开始在测试体中使用Espresso。它首先在RecyclerView的第一个position中执行了一个点击。然后它检测是否可以找到一个指定id的view且这个view是一个TextView

要运行这个测试,点击顶部的Run configurations下拉选择Edit Configurations...按下+图标,选择Android Tests,然后选择app模块。现在,在target device中选择你喜欢的target。点击OK然后运行。你应该可以看到App是怎样在你的设备中开始的,它会测试第一个position,打开详情页面然后再次关闭app。

现在我们要做一个更加复杂一点的事情。测试会从toolbar众打开一个溢出菜单,点击settings栏,改变城市的code,然后检测toolbar的标题是否改变成了对应的标题。

@Test fun modifyZipCode_changesToolbarTitle() {
    openActionBarOverflowOrOptionsMenu(activityRule.activity)
    onView(withText(R.string.settings)).perform(click())
    onView(withId(R.id.cityCode)).perform(replaceText("28830"))
    pressBack()
    onView(isAssignableFrom(Toolbar::class.java))
            .check(matches(
                withToolbarTitle(`is`("San Fernando de Henares (ES)"))))
}

这个测试实际做的事情:

  • 它首先使用openActionBarOverflowOrOptionsMenu打开溢出菜单。
  • 然后它根据Settings文本查找一个view,然后点击这个它。
  • 之后,设置界面就会被打开,所以它会查找一个EditText并且替换成一个新的code
  • 它会点击返回按钮。它会把新的值保存在preferences中,然后关闭Activity。
  • 因为MainActivityonResume会调用,请求会再调用一次。这时它会获取到新城市的forecast。
  • 最后一行将会检测Toolbar我们看到的title是否是新的城市的title。

这不是一个toolbar的title的默认匹配器,但是Espresso是很容易扩展的,所以我们可以创建一个新的matcher来实现检测:

private fun withToolbarTitle(textMatcher: Matcher<CharSequence>): Matcher<Any> =
        object : BoundedMatcher<Any, Toolbar>(Toolbar::class.java) {

    override fun matchesSafely(toolbar: Toolbar): Boolean {
        return textMatcher.matches(toolbar.title)
    }

    override fun describeTo(description: Description) {
        description.appendText("with toolbar title: ")
        textMatcher.describeTo(description)
    }                
}

matchSafely函数是我们检测的地方,而describeTo函数为matcher增加了一些新的信息。

这章特别有趣,因为我们看到了在Kotlin中怎么样去完美和谐地整合测试,它们可以没有任何问题地整合测试。查看代码然后你自己运行一下吧。