概述

新建一个 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 等没有太大关系。一般适合进行本地单元测试的代码就是:

  1. MVP 结构中的 Presenter 或者 MVVM 结构中的 ViewModel
  2. Helper 或者 Utils 工具类
  3. 公共基础模块,比如网络库、数据库等

我们一直强调本地单元测试和 Android 框架没有关系,但是有时候还是不可避免地会依赖到 Android 框架,比如某些 Utils 工具类需要 Context,针对这种情况,我们只能使用模拟对象的框架了,1,如果使用 Java 语言开发推荐使用 Mocktio,如果使用 Kotlin 语言开发推荐使用 MockK;2,如果使用 Java 语言开发推荐使用 Mocktio,如果使用 Kotlin 语言开发推荐使用 MockK;3,如果使用 Java 语言开发推荐使用 Mocktio,如果使用 Kotlin 语言开发推荐使用 MockK。(重要的事情说三遍,都是血泪的经验)

dependencies {// Required -- JUnit 4 frameworktestImplementation '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.utilimport java.util.regex.Patternobject Utils {/*** 是否有效的邮箱* */fun isValidEmail(email: String?): Boolean {if (email == null)return falseval 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 falsereturn 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.utilimport org.junit.Testimport org.junit.Assert.*class UtilsTest {@Testfun isValidEmail() {assertEquals(false, Utils.isValidEmail("test"))assertEquals(true, Utils.isValidEmail("test@qq.com"))}@Testfun isValidPhoneNumber() {assertEquals(false, Utils.isValidPhoneNumber("123"))assertEquals(true, Utils.isValidPhoneNumber("12345678911"))}
}

测试用例编写完成,然后就是运行测试用例,有几种方法:

  1. 运行单个测试方法:选中@Test注解或者方法名,右键选择 Run
  2. 运行一个测试类中的所有测试方法:打开类文件,在类的范围内右键选择 Run 或者直接选择类文件直接右键 Run
  3. 运行一个目录下的所有测试类:选择这个目录,右键 Run
  4. 使用 gradle 命令:./gradlew :mylibrary:test ,然后在 mylibrary/build/reports/tests 目录下查看测试的结果
  5. 使用 AS 快捷键,打开右上角的 Gradle Tab,mylibrary -> Tasks-> verification->点击 test

现在我们在 Utils 公共类增加一个“getMyString() ”的方法,这个方法需要一个 Context 对象:

 Utils 类/*** 获取 string* */fun getMyString(context: Context): String {return context.getString(R.string.mylibrary)}

这时候就轮到 Mocktio 出场:

  1. 在 mylibrary 的 build.gradle 文件中添加 Mockito 库的依赖
  2. 在单元测试类定义 UtilsTest 的开头,添加 @RunWith(MockitoJUnitRunner::class) 注释
  3. 要为 Android 依赖项创建模拟对象,在要模拟的对象前添加 @Mock 注释
  4. 使用 Mockito 的 when() 和 thenReturn() 方法指定条件并在满足条件时返回期望的值
  5. 调用 Utils.getMyString() 方法,看看它返回的值和我们期望的值是否一样

注意点:mock 出来的对象是一个虚假的对象,在测试环境中,用来替换掉真实的对象,以达到验证对象方法调用情况,或是指定这个对象的某些方法返回特定的值等。

@RunWith(MockitoJUnitRunner::class)
class UtilsTest {@Mocklateinit var mContext: Contextprivate val FAKE_STRING = "Hello"@Testfun 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

  1. verify().method Call,用来验证 mock 对象的方法是否被调用
  2. when(…​.).thenReturn(…​.),用来定义当条件满足时函数的返回值;对于无返回值的函数,我们可以使用 doReturn(…​).when(…​).method Call 来获得类似的效果
  3. doAnswer(…​).when(…​).method Call,用于有回调的函数,我们可以在 Answer 对象中拿到回调的对象,然后执行回调对象的方法
  4. 还有 doThrow() | doNothing() 等方法,可以参考 Mockito 的官方文档

缺陷

  1. Mockito cannot mock/spy because : — final class : Mockito 预设是无法 Mock final class,而在 Kotlin 里任何 Class预设都是 final(除非使用 open 关键字)
  2. java.lang.IllegalStateException: anyObject() must not be null :Mockito 的 any() 、eq()等方法都是可能回传 null 的,而 Kotlin 是“空安全”的,显然它不能接受这些方法的
  3. Mockito 的 when()方法要加上反引号才能使用,这是因为 when 在 Kotlin 中是保留字
  4. 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 {@MockKprivate lateinit var context: Contextprivate val FAKE_STRING = "Hello"@Beforefun setup() {MockKAnnotations.init(this)//另外一种 mock 对象的方法//context = mockk()}@Testfun getMyString() {every {context.getString(any())}.returns(FAKE_STRING)assertEquals(FAKE_STRING, Utils.getMyString(context))verify {context.getString(any())}}
}
  1. 模拟 Context 对象,有两种方式@MockK 注解和 mockk() 方法,使用注解则必须在 @Before 方法中调用MockKAnnotations.init() 方法
  2. 使用 every(…).returns(…) 方法,定义当条件满足时函数的返回值,这个方法类似于 Mockito 的 when(…​.).thenReturn(…​.) 方法
  3. 调用 Utils.getMyString(context) 方法
  4. 使用 verify(…) 方法验证 Context 对象的 getString() 方法是否被调用

常用API

  1. verify(…)、coVerify(…),验证 mock 对象的方法是否被调用
  2. every(…)、coEvery(…),定义当条件满足时函数的返回值,后面可以跟 returns(…) answers(…) throws(…) 等方法,可以去参考文档
  3. 以 co 开头的方法是配合 Kotlin 协程使用的,suspend 函数可以在方法的闭包内使用
  4. 推荐 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'//对于InstantTaskExecutorRuletestImplementation '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.mylibraryimport 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:Ruleval instantTaskExecutorRule = InstantTaskExecutorRule()@ExperimentalCoroutinesApi@get:Ruleval mainCoroutineRule = MainCoroutineRule()private lateinit var params: Paramsprivate lateinit var repository: ProductRepositoryprivate lateinit var viewModel: ProductViewModel@Beforefun setup() {repository = mockk()params = mockk()viewModel = ProductViewModel(repository)}@ExperimentalCoroutinesApi@Testfun getList_SuccessTest() {// 注意这里使用 runBlockingTestmainCoroutineRule.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 相同格式的测试报告。在这篇文章中,我们主要关注如何生成本地单元测试覆盖率报告。

  1. 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 = truehtml.enabled = truecsv.enabled = false}//设置需要检测覆盖率的目录def mainSrc = "${projectDir}/src/main/java"sourceDirectories.from = files([mainSrc])// exclude auto-generated classes and testsdef 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单元测试相关推荐

  1. Android单元测试全解

      自动化测试麻烦吗?说实在,麻烦!有一定的学习成本.但是,自动化测试有以下优点: 节省时间:可以指定测试某一个activity,不需要一个个自己点 单元测试:既然Java可以进行单元测试,Andro ...

  2. Android 单元测试学习计划

    网上查了一下Android单元测试相关的知识点,总结了一个学习步骤: 1. 什么是单元测试 2. 单元测试正反面: 2.1. 重要性 2.2. 缺陷 2.3. 策略 3. 单元测试的基础知识: 3.1 ...

  3. Android单元测试 - 几个重要问题

    前言 已经一个月没写文章了,由于9月份在plan国庆旅行计划,国庆前前后后去了14天旅行,所以没时间写,哈哈. 言归正传,上一篇文章<Android单元测试 - 如何开始?>介绍了几款单元 ...

  4. Android单元测试框架Robolectric3.0介绍(二)

    文章中的所有代码在此:https://github.com/geniusmart/LoveUT ,由于 Robolectric 3.0 和 3.1 版本(包括后续3.x版本)差异不小,该工程中包含这两 ...

  5. Android单元测试研究与实践

    处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单元测试的用武之地.单元测试周期性对项目进行函数级别的测试,在良好的覆盖率下,能够持续维护代码逻辑,从而支持项目从容 ...

  6. Android单元测试 - Sqlite、SharedPreference、Assets、文件操作 怎么测?

    前言 上篇<Android单元测试 - 几个重要问题> 讲解了"何解决Android依赖.隔离Native方法.静态方法.RxJava异步转同步"这几个Presente ...

  7. android单元测试作用,Android单元测试源码解读

    Android手机操作系统是一个开源的操作系统.程序员们可以在模拟器的帮助下对其进行修改,来实现各种功能需求,满足用户的应用.在这里我们先来了解一下Android单元测试的相关内容. 在网络上找了半天 ...

  8. android单元测试android环境,基于Robolectric的Android单元测试 —环境搭建与部署运行...

    移动端的测试中,因为回归一些逻辑分支比较多的功能时工作量比较大,且不太适合用UI完成,尝试通过单元测试来完成.几经波折终于完成了一个功能的UT用例并在CI上部署运行,现总结如下: 一.Robolect ...

  9. Android单元测试 Instrumentation

    开发中我们需要对部分功能进行单元测试,启动Activity来测试部分小功能,有点小题大作,杀鸡用牛刀. 我们可以用Android单元测试 Instrumentation 本篇只是入门,起到抛砖的效果 ...

  10. (转)Android单元测试

    关键字: camera unit test android源代码中每个app下中都自带了一个test用例,下面主要介绍下camra单元测试用例 在AndroidManifest.xml中标明了测试用例 ...

最新文章

  1. java中实现具有传递性吗_Java中volatile关键字详解,jvm内存模型,原子性、可见性、有序性...
  2. 收藏 | Tensorflow实现的深度NLP模型集锦(附资源)
  3. Adam又要“退休”了?耶鲁大学团队提出AdaBelief
  4. 你连原理都还没弄明白?java的基本单位
  5. cocos2d-x游戏开发(七)对象释放时机
  6. “*** IS NOT TRANSLATED IN …….. 解决办法
  7. gitlab bash_如何编写Bash一线式以克隆和管理GitHub和GitLab存储库
  8. LeetCode 2075. 解码斜向换位密码(模拟)
  9. 基础算法 —— 高精度计算 —— 高精度乘法
  10. 执行计划级别mysql 2ef,Mysql 层级、执行顺序、执行计划分析
  11. 大数据_MapperReduce_Hbase的优化_存数据_自动计算分区号 自动计算分区键---Hbase工作笔记0027
  12. 像玩乐高一样玩 simpletun
  13. Flutter InkWell 动画浅析
  14. 微软Windows7将捆绑杀软 众厂商面临生死抉择
  15. python中对象的定义_全面了解python中的类,对象,方法,属性
  16. 锻造成形与计算机技术,铸造成型及控制工程
  17. 整流八--电网不平衡状态下三相PWM整流器的控制策略
  18. 【web素材】04—40款个人主页简历网页模板及企业单页
  19. tc android开发工具,TC5.0 (一个脚本开发工具)其底层实现原理分析与推测(半成品)...
  20. idea Push Tags选All还是Current Branch?

热门文章

  1. Win7激活工具的原理是什么?
  2. 巨蟒python全栈开发-第5天 字典集合
  3. 关于中国移动宽带(中国铁通)比较卡
  4. pl sql面试题_PL SQL面试问答
  5. 安卓 多条通知_【安卓+苹果】石头阅读,全网小说、漫画免费看,最好用的追书神器!...
  6. C++ 一维高斯积分的实现
  7. 关于DTC诊断故障码的获取与清除(ISO14229系列之14、19服务)
  8. GD32 startup.s
  9. lintcode 丢鸡蛋
  10. 2.4 数值分析: Doolittle直接三角分解法