原文来自:https://fernandocejas.com/2017/02/03/android-testing-with-kotlin/
事实上,这也不是一个新话题,因为 Kotlin 在编程语言世界里攻城略地,尤其在 Android 中。我不会深入去讲 Kotlin 能给给我们什么,因为有很多人已经做的很出色(特别是我的朋友 Antonio Leiva)。
开始之前,我会给这个后起之秀的主要好处来个快速总结:
我们有一个使用 Java 写的 Android 代码库,我们想逐步引入这门帅气的语言,所以为啥不从测试开始呢?这样子我们就可以在任何环境下都不影响主应用的情况下放心地参试 Kotlin,体验把这门现代接近成熟的语言带来的激情,而且也可以为我们和我们的团队做一些迎接大改变的准备。听上去是不是很牛逼?那么让我们先写些代码...
大致上,我想展示我们怎么使用 Kotlin 测试 Android 应用的。所以先迈出第一步,准备好我们的环境配置,把 Kotlin 的依赖加入到我们的 build.gradle 文件中去:
buildscript {
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.5-2'
}
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
...
dependencies {
...
compile "org.jetbrains.kotlin:kotlin-stdlib:1.0.6"
...
testCompile 'org.jetbrains.kotlin:kotlin-stdlib:1.0.6'
testCompile 'org.jetbrains.kotlin:kotlin-test-junit:1.0.6'
testCompile "com.nhaarman:mockito-kotlin:1.1.0"
testCompile 'org.amshove.kluent:kluent:1.14'
}
接着我们需要设置专门放 Kotlin 写的用例的文件夹,通过修改 sourceSets 部分完成:
android {
...
sourceSets {
test.java.srcDirs += 'src/test/kotlin'
androidTest.java.srcDirs += 'src/androidTest/kotlin'
}
...
}
第三步,我们想确保没有一行 Kotlin 的代码会跑到生产环境上去,我们加一些东西来防御下:
afterEvaluate {
android.sourceSets.all { sourceSet ->
if (!sourceSet.name.startsWith('test') || !sourceSet.name.startsWith('androidTest')) {
sourceSet.kotlin.setSrcDirs([])
}
}
}
完整的文件 可以在 sample project on Github 找到。现在我们可以像用 Java 那样写测试用例啦!
这里我只需要 JUnit, Mockito-kotlin 和 Kluent(一个非常 cool 的断言语义库)。让我们看看 GetUserDetails.java 的测试代码,我的应用里的一个用户用例 (from a Clean Architecture approach) 。
class GetUserDetailsTest {
private val USER_ID = 123
private lateinit var getUserDetails: GetUserDetails
private val userRepository: UserRepository = mock()
private val threadExecutor: ThreadExecutor = mock()
private val postExecutionThread: PostExecutionThread = mock()
@Before
fun setUp() {
getUserDetails = GetUserDetails(userRepository, threadExecutor, postExecutionThread)
}
@Test
fun shouldGetUserDetails() {
getUserDetails.buildUseCaseObservable(GetUserDetails.Params.forUser(USER_ID));
verify(userRepository).user(USER_ID)
verifyNoMoreInteractions(userRepository)
verifyZeroInteractions(postExecutionThread)
verifyZeroInteractions(threadExecutor)
}
}
Something to pay attention is that when we need to construct our subject under test (in the setup method), 这里需要注意的是当我们需要构建测试对象的时候(在 setup 方法里),我们必须用 “lateinit“来声明,否则编译器会报错,因为属性必须初始化或者抽象。这里是另外一个测试 Serializer.java 类的用例,你可以看到上面说到的断言:
class SerializerTest {
private val JSON_RESPONSE = "{\n \"id\": 1,\n " +
"\"cover_url\": \"http://www.android10.org/myapi/cover_1.jpg\",\n " +
"\"full_name\": \"Simon Hill\",\n " +
"\"description\": \"Curabitur gravida nisi at nibh. In hac habitasse " +
"platea dictumst. Aliquam augue quam, sollicitudin vitae, consectetuer " +
"eget, rutrum at, lorem.\\n\\nInteger tincidunt ante vel ipsum. " +
"Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo " +
"placerat.\\n\\nPraesent blandit. Nam nulla. Integer pede justo, " +
"lacinia eget, tincidunt eget, tempus vel, pede.\",\n " +
"\"followers\": 7484,\n " +
"\"email\": \"jcooper@babbleset.edu\"\n}"
private var serializer = Serializer()
@Test
fun shouldSerialize() {
val userEntityOne = serializer.deserialize(JSON_RESPONSE, UserEntity::class.java)
val jsonString = serializer.serialize(userEntityOne, UserEntity::class.java)
val userEntityTwo = serializer.deserialize(jsonString, UserEntity::class.java)
userEntityOne.userId shouldEqual userEntityTwo.userId
userEntityOne.fullname shouldEqual userEntityTwo.fullname
userEntityOne.followers shouldEqual userEntityTwo.followers
}
@Test
fun shouldDesearialize() {
val userEntity = serializer.deserialize(JSON_RESPONSE, UserEntity::class.java)
userEntity.userId shouldEqual 1
userEntity.fullname shouldEqual "Simon Hill"
userEntity.followers shouldEqual 7484
}
}
我创建了一个测试父类(每个测试用例都会用到)为了封装所有 Robolectic 相关的东西。所以,我的测试不需要直接依赖 Robolectric 框架。把任何通用功能或者帮助方法封装在父类里是我学到的一个教训,以前我在每个测试类里都用了 Robolectric 类,当我想迁移到一个不向后兼容的版本的时候就非常痛苦。
/**
* Base class for Robolectric data layer tests.
* Inherit from this class to create a test.
*/
@RunWith(RobolectricTestRunner::class)
@Config(constants = BuildConfig::class,
application = AndroidTest.ApplicationStub::class,
sdk = intArrayOf(21))
abstract class AndroidTest {
fun context(): Context {
return RuntimeEnvironment.application
}
fun cacheDir(): File {
return context().cacheDir
}
internal class ApplicationStub : Application()
}
下面是一个集成测试用例,我们通过 AndroidTest.kt 和 Android 框架沟通。
class FileManagerTest : AndroidTest() {
private var fileManager = FileManager()
@After
fun tearDown() {
fileManager.clearDirectory(cacheDir())
}
@Test
fun shouldWriteToFile() {
val fileToWrite = createDummyFile()
val fileContent = "content"
fileManager.writeToFile(fileToWrite, fileContent)
fileToWrite.exists() shouldEqualTo true
}
@Test
fun shouldHaveCorrectFileContent() {
val fileToWrite = createDummyFile()
val fileContent = "content\n"
fileManager.writeToFile(fileToWrite, fileContent)
val expectedContent = fileManager.readFileContent(fileToWrite)
expectedContent shouldEqualTo fileContent
}
private fun createDummyFile(): File {
val dummyFilePath = cacheDir().path + File.separator + "dummyFile"
return File(dummyFilePath)
}
}
我认为当下最稳定的集成测试框架非谷歌出品的 Espresso 莫属,所以我选择了它。和前面的 Robolectric 一样,我决定在顶上创建个小框架,让我们看看它是怎么工作的。我所有的测试用例都基于 AcceptanceTest.kt 类:
@LargeTest
@RunWith(AndroidJUnit4::class)
abstract class AcceptanceTest<T : Activity>(clazz: Class<T>) {
@Rule @JvmField
val testRule: ActivityTestRule<T> = IntentsTestRule(clazz)
val checkThat: Matchers = Matchers()
val events: Events = Events()
}
这里需要关注的点是:
class Matchers {
fun <T : Activity> nextOpenActivityIs(clazz: Class<T>) {
intended(IntentMatchers.hasComponent(clazz.name))
}
fun viewIsVisibleAndContainsText(@StringRes stringResource: Int) {
onView(withText(stringResource)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
fun viewContainsText(@IdRes viewId: Int, @StringRes stringResource: Int) {
onView(withId(viewId)).check(matches(withText(stringResource)))
}
}
class Events {
fun clickOnView(@IdRes viewId: Int) {
onView(withId(viewId)).perform(click())
}
}
最后说下,该项目的主 activity 就展示了一个 view,上面有个按钮,点了按钮就可以打开另外一个 view。代码很简单,如果你想更好的理解的话,可以看这里: you can browse the sample code on Github。
class MainActivityTest : AcceptanceTest<MainActivity>(MainActivity::class.java) {
@Test
fun shouldOpenHelloWorldScreen() {
events.clickOnView(R.id.btn_hello_world)
checkThat.nextOpenActivityIs(HelloWorldActivity::class.java)
}
@Test
fun shouldDisplayAction() {
events.clickOnView(R.id.fab)
checkThat.viewIsVisibleAndContainsText(R.string.action)
}
}
There are neither problems nor especial configuration to run our tests from 在 Android Studio/Intellij 里运行我们测试没有什么问题,也无需特别的配置。同时,我也在我的根 build.gradle 文件里添加了几个 Gradle 任务:
task runUnitTests(dependsOn: [':app:testDebugUnitTest']) {
description 'Run all unit tests'
}
task runAcceptanceTests(dependsOn: [':app:connectedAndroidTest']) {
description 'Run all acceptance tests.'
}
如果要运行的话,只要在终端目录下运行:
./gradlew runUnitTests
./gradlew runAcceptanceTests
如果某个时刻,你开始考虑 Kotlin,那就不要有任何借口不用到产品中去。测试是一个好的切入点,而且还能尝尝新语言的味道。当然,你还可以额外得到所有本文提到的好处。一个好的培训可以让你和你的团队为这个大改变做好准备。