Android单元测试
Posted sl851938874
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android单元测试相关的知识,希望对你有一定的参考价值。
概述
新建一个 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 等没有太大关系。一般适合进行本地单元测试的代码就是:
- MVP 结构中的 Presenter 或者 MVVM 结构中的 ViewModel
- Helper 或者 Utils 工具类
- 公共基础模块,比如网络库、数据库等
我们一直强调本地单元测试和 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"))
测试用例编写完成,然后就是运行测试用例,有几种方法:
- 运行单个测试方法:选中@Test注解或者方法名,右键选择 Run
- 运行一个测试类中的所有测试方法:打开类文件,在类的范围内右键选择 Run 或者直接选择类文件直接右键 Run
- 运行一个目录下的所有测试类:选择这个目录,右键 Run
- 使用 gradle 命令:./gradlew :mylibrary:test ,然后在 mylibrary/build/reports/tests 目录下查看测试的结果
- 使用 AS 快捷键,打开右上角的 Gradle Tab,mylibrary -> Tasks-> verification->点击 test
现在我们在 Utils 公共类增加一个“getMyString() ”的方法,这个方法需要一个 Context 对象:
Utils 类
/**
* 获取 string
* */
fun getMyString(context: Context): String
return context.getString(R.string.mylibrary)
这时候就轮到 Mocktio 出场:
- 在 mylibrary 的 build.gradle 文件中添加 Mockito 库的依赖
- 在单元测试类定义 UtilsTest 的开头,添加 @RunWith(MockitoJUnitRunner::class) 注释
- 要为 Android 依赖项创建模拟对象,在要模拟的对象前添加 @Mock 注释
- 使用 Mockito 的 when() 和 thenReturn() 方法指定条件并在满足条件时返回期望的值
- 调用 Utils.getMyString() 方法,看看它返回的值和我们期望的值是否一样
注意点: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
常用API
- verify().method Call,用来验证 mock 对象的方法是否被调用
- when(….).thenReturn(….),用来定义当条件满足时函数的返回值;对于无返回值的函数,我们可以使用 doReturn(…).when(…).method Call 来获得类似的效果
- doAnswer(…).when(…).method Call,用于有回调的函数,我们可以在 Answer 对象中拿到回调的对象,然后执行回调对象的方法
- 还有 doThrow() | doNothing() 等方法,可以参考 Mockito 的官方文档
缺陷
- Mockito cannot mock/spy because : — final class : Mockito 预设是无法 Mock final class,而在 Kotlin 里任何 Class预设都是 final(除非使用 open 关键字)
- java.lang.IllegalStateException: anyObject() must not be null :Mockito 的 any() 、eq()等方法都是可能回传 null 的,而 Kotlin 是“空安全”的,显然它不能接受这些方法的
- Mockito 的 when()方法要加上反引号才能使用,这是因为 when 在 Kotlin 中是保留字
- Argument(s) are different! Wanted:Mockito 不能很好的支持 Kotlin 的 suspend functions
第一条,可以依赖 mockito-inline 解决;第二条,可以依赖 mockito-kotlin 解决;第三条,只是语法问题还能接受;最后一条,要老命了,因为我们项目中大量使用了 Kotlin 的协程,Mockito 不能很好的支持挂起函数,那么项目中的异步操作就无法进行单元测试,怎么办,这就轮到另一款模拟框架 MockK 闪亮登场了。
三,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())
- 模拟 Context 对象,有两种方式@MockK 注解和 mockk() 方法,使用注解则必须在 @Before 方法中调用MockKAnnotations.init() 方法
- 使用 every(…).returns(…) 方法,定义当条件满足时函数的返回值,这个方法类似于 Mockito 的 when(….).thenReturn(….) 方法
- 调用 Utils.getMyString(context) 方法
- 使用 verify(…) 方法验证 Context 对象的 getString() 方法是否被调用
常用API
- verify(…)、coVerify(…),验证 mock 对象的方法是否被调用
- every(…)、coEvery(…),定义当条件满足时函数的返回值,后面可以跟 returns(…) answers(…) throws(…) 等方法,可以去参考文档
- 以 co 开头的方法是配合 Kotlin 协程使用的,suspend 函数可以在方法的闭包内使用
- 推荐 API 文章 Kotlin 测试利器—MockK
下面开始重头戏,项目实战走起,推荐一个很好的讲解 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 相同格式的测试报告。在这篇文章中,我们主要关注如何生成本地单元测试覆盖率报告。
- IntelliJ IDEA
参考上面讲的 “运行测试用例” 的几种方法,在 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 ,点击执行,就会生成覆盖率报告。
结束语
感谢大家的阅读,我这里只是分享了一些自己踩过的坑。
路漫漫其修远兮,吾将上下而求索,希望大家能共同探索、一起进步。
以上是关于Android单元测试的主要内容,如果未能解决你的问题,请参考以下文章