赞
踩
新建一个 module 的时候,Android Studio 自动帮我们生成了 test 和 androidTest 两个 sourceSet。这两个 sourceSet 对应了不同的单元测试类型,同时两个 sourceSet 声明依赖的命令也有区别,前者是 testImplementation 后者是 androidTestImplementation,在这篇文章中,我们主要讲本地单元测试。
app/src
├── androidTestjava (Instrument单元测试、UI测试)
├── main/java (业务代码)
└── test/java (本地单元测试)
顾名思义和 Android 无关,这种测试是和原生的 Java 测试一样,不依赖 Android 框架或者只有非常少的依赖,直接运行在你本地的JVM上,而不需要运行在一个 Android 设备或者 Android 模拟器上,所以这种测试方式是非常高效的,因此我们建议如果可以,就是用这种方法测试,比如业务逻辑代码,它们可能和 Android Activity 等没有太大关系。一般适合进行本地单元测试的代码就是:
我们一直强调本地单元测试和 Android 框架没有关系,但是有时候还是不可避免地会依赖到 Android 框架,比如某些 Utils 工具类需要 Context,针对这种情况,我们只能使用模拟对象的框架了,1,如果使用 Java 语言开发推荐使用 Mocktio,如果使用 Kotlin 语言开发推荐使用 MockK;2,如果使用 Java 语言开发推荐使用 Mocktio,如果使用 Kotlin 语言开发推荐使用 MockK;3,如果使用 Java 语言开发推荐使用 Mocktio,如果使用 Kotlin 语言开发推荐使用 MockK。(重要的事情说三遍,都是血泪的经验)
dependencies {
// Required -- JUnit 4 framework
testImplementation 'junit:junit:4.12'
// Optional -- Mockito framework(可选,用于模拟一些依赖对象,以达到隔离依赖的效果)
testImplementation "org.mockito:mockito-core:1.10.19"
}
下面看例子,新建一个名为 mylibrary 的Android Module,Android Studio 会自动帮我们在 src 目录下创建 test、androidTest、main 三个目录,该 module 的 build.gradle 默认配置如下,这里我们使用的是本地测试单元,所以先把 androidTestImplementation 的依赖注释掉:
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
testImplementation 'junit:junit:4.12'
//androidTestImplementation 'androidx.test.ext:junit:1.1.3'
//androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
然后在 main 目录下 java 中定义一个 Utils 工具类,这个类有两个方法:
package com.jdd.smart.mylibrary.util import java.util.regex.Pattern object Utils { /** * 是否有效的邮箱 * */ fun isValidEmail(email: String?): Boolean { if (email == null) return false val regEx1 = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$" val p = Pattern.compile(regEx1) val m = p.matcher(email) return m.matches() } /** * 是否有效的手机号,只判断位数 * */ fun isValidPhoneNumber(phone: String?): Boolean { if (phone == null) return false return phone.length == 11 } }
现在我们编写一个 Utils 类单元测试用例,这里可以使用AS的快捷键,选择对应的类->将光标停留在类上->按下右键>在弹出的弹窗中选择Generate->选择Test:
Testing library 选择 JUnit4,勾选 setUp/@Before 会生成一个带 @Before 注解的 空方法,tearDown/@After 则会生成一个带 @After 注解的空方法,点击 OK:
选择测试用例保存的路径,我们现在使用本地单元测试,所以放到 src/test/java 目录下,点击 OK ,然后测试用例就创建完成,UtilsTest 类中的方法一开始都是空方法,我们编写自己的测试代码:
package com.jdd.smart.mylibrary.util import org.junit.Test import org.junit.Assert.* class UtilsTest { @Test fun isValidEmail() { assertEquals(false, Utils.isValidEmail("test")) assertEquals(true, Utils.isValidEmail("test@qq.com")) } @Test fun isValidPhoneNumber() { assertEquals(false, Utils.isValidPhoneNumber("123")) assertEquals(true, Utils.isValidPhoneNumber("12345678911")) } }
测试用例编写完成,然后就是运行测试用例,有几种方法:
现在我们在 Utils 公共类增加一个“getMyString() ”的方法,这个方法需要一个 Context 对象:
Utils 类
/**
* 获取 string
* */
fun getMyString(context: Context): String {
return context.getString(R.string.mylibrary)
}
这时候就轮到 Mocktio 出场:
注意点:mock 出来的对象是一个虚假的对象,在测试环境中,用来替换掉真实的对象,以达到验证对象方法调用情况,或是指定这个对象的某些方法返回特定的值等。
@RunWith(MockitoJUnitRunner::class)
class UtilsTest {
@Mock
lateinit var mContext: Context
private val FAKE_STRING = "Hello"
@Test
fun getMyString() {
Mockito.`when`(mContext.getString(R.string.mylibrary)).thenReturn(FAKE_STRING)
val myString = Utils.getMyString(mContext)
assertEquals(FAKE_STRING, myString)
}
}
我们注意到,在上面的测试用例 UtilsTest 中,我们使用了 when(….).thenReturn(….) API ,来定义当条件满足时函数的返回值,其实 Mockito 还提供了很多其他 API,接下来,我们介绍下Mockito。
第一条,可以依赖 mockito-inline 解决;第二条,可以依赖 mockito-kotlin 解决;第三条,只是语法问题还能接受;最后一条,要老命了,因为我们项目中大量使用了 Kotlin 的协程,Mockito 不能很好的支持挂起函数,那么项目中的异步操作就无法进行单元测试,怎么办,这就轮到另一款模拟框架 MockK 闪亮登场了。
MockK(mocking library for Kotlin),专为 Kotlin 而生 ,官方文档。MockK 其实跟 Mockito 的思路很像,只是语法稍有不同而已。
我们还是用上面的 Utils 公共类举例,首先,依赖 MockK 库
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation "io.mockk:mockk:1.12.1"
}
然后,编写 getMyString() 方法的测试用例
class UtilsTest { @MockK private lateinit var context: Context private val FAKE_STRING = "Hello" @Before fun setup() { MockKAnnotations.init(this) //另外一种 mock 对象的方法 //context = mockk() } @Test fun getMyString() { every { context.getString(any()) }.returns(FAKE_STRING) assertEquals(FAKE_STRING, Utils.getMyString(context)) verify { context.getString(any()) } } }
下面开始重头戏,项目实战走起,推荐一个很好的讲解 MockK 的系列。
我们项目使用的 Kotlin 协程 + MVVM,上面有提到,适合用本地单元测试的代码是 MVVM 结构中的 ViewModel,那么现在我们就为 ViewModel 编写测试用例。
首先,我们要 在 build.gradle 中,添加单元测试需要的依赖:
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation "io.mockk:mockk:1.12.1"
//对于runBlockingTest, CoroutineDispatcher等
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
//对于InstantTaskExecutorRule
testImplementation 'androidx.arch.core:core-testing:2.1.0'
}
//org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2 是用来测试 Kotlin 协程的
//androidx.arch.core:core-testing:2.1.0 是用来测试 LiveData 的
然后在 test/java 目录下,新增一个类,这个类很重要(Replace Dispatcher.Main with TestCoroutineDispatcher),为什么这么做?参考 Kotlin 的文章
package com.jdd.smart.mylibrary import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.rules.TestWatcher import org.junit.runner.Description @ExperimentalCoroutinesApi class MainCoroutineRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()): TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(dispatcher) } override fun finished(description: Description?) { super.finished(description) cleanupTestCoroutines() Dispatchers.resetMain() } }
最后编写测试用例:
class ProductViewModelTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @ExperimentalCoroutinesApi @get:Rule val mainCoroutineRule = MainCoroutineRule() private lateinit var params: Params private lateinit var repository: ProductRepository private lateinit var viewModel: ProductViewModel @Before fun setup() { repository = mockk() params = mockk() viewModel = ProductViewModel(repository) } @ExperimentalCoroutinesApi @Test fun getList_SuccessTest() { // 注意这里使用 runBlockingTest mainCoroutineRule.runBlockingTest { val result = Result.Success("hhhh") //定义条件和满足条件的返回值 coEvery { // getList 是挂起函数,返回值是 Result<String> repository.getList(any()) }.returns(result) viewModel.getList(params) //验证函数是否被调用 coVerify { // getList 是挂起函数 repository.getList(any()) } //liveData 是 MutableLiveData ,验证 liveData 是否赋值成功 Assert.assertEquals("hhhh", viewModel.liveData.value) } } }
上面的例子是 MVVM 架构的项目,这篇文章是 MVP 架构的项目。
Android Studio 支持的 Code Coverage Tool : jacoco、IntelliJ IDEA。上面有提到,当新建一个 module 时,Android Studio 自动帮我们生成了 test 和 androidTest 两个 sourceSet,在Android Studio中,在 androidTest 包下的单元测试代码,默认使用 jacoco 插件生成包含代码覆盖率的测试报告;而 test 包下的单元测试代码,则直接使用 IntelliJ IDEA 生成覆盖率报告,也可以通过自定义 gradle task 使用 jacoco 插件生成与 androidTest 相同格式的测试报告。在这篇文章中,我们主要关注如何生成本地单元测试覆盖率报告。
参考上面讲的 “运行测试用例” 的几种方法,在 Run 命令下面,有一个 Run xxx with Coverage 命令,点击这个 Coverage 命令,就会生成覆盖率报告。
2. jacoco
需要自定义 gradle task 。
首先,新建一个 jacoco.gradle 文件,内容如下:
apply plugin: 'jacoco' jacoco { toolVersion = "0.8.6" //指定jacoco的版本 } //依赖于testDebugUnitTest任务 task jacocoTestReport(type: JacocoReport, dependsOn: "testDebugUnitTest") { group = "reporting"指定task的分组 description = "Generate Jacoco coverage reports"指定task的描述 reports { xml.enabled = true html.enabled = true csv.enabled = false } //设置需要检测覆盖率的目录 def mainSrc = "${projectDir}/src/main/java" sourceDirectories.from = files([mainSrc]) // exclude auto-generated classes and tests def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', 'android/**/*.*'] //定义检测覆盖率的class所在目录,注意:不同 gradle 版本可能不一样,需要自行替换 def debugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug", excludes: fileFilter) classDirectories.from = files([debugTree]) executionData.from = fileTree(dir: project.projectDir, includes: ['**/*.exec', '**/*.ec']) }
注意:debugTree 配置不同 gradle 版本可能不一样
然后,在 module 的 build.gradle 文件里依赖 jacoco.gradle 即可:
apply from: 'jacoco.gradle'
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
Syns 完成后,在右上角的 Gradle Tab 会生成一个 task ,mylibrary -> Tasks-> reporting -> jacocoTestReport ,点击执行,就会生成覆盖率报告。
感谢大家的阅读,我这里只是分享了一些自己踩过的坑。
路漫漫其修远兮,吾将上下而求索,希望大家能共同探索、一起进步。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。