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 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"))}
}
测试用例编写完成,然后就是运行测试用例,有几种方法:
- 运行单个测试方法:选中@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 {@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
- 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 {@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())}}
}
- 模拟 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'//对于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 相同格式的测试报告。在这篇文章中,我们主要关注如何生成本地单元测试覆盖率报告。
- 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单元测试相关推荐
- Android单元测试全解
自动化测试麻烦吗?说实在,麻烦!有一定的学习成本.但是,自动化测试有以下优点: 节省时间:可以指定测试某一个activity,不需要一个个自己点 单元测试:既然Java可以进行单元测试,Andro ...
- Android 单元测试学习计划
网上查了一下Android单元测试相关的知识点,总结了一个学习步骤: 1. 什么是单元测试 2. 单元测试正反面: 2.1. 重要性 2.2. 缺陷 2.3. 策略 3. 单元测试的基础知识: 3.1 ...
- Android单元测试 - 几个重要问题
前言 已经一个月没写文章了,由于9月份在plan国庆旅行计划,国庆前前后后去了14天旅行,所以没时间写,哈哈. 言归正传,上一篇文章<Android单元测试 - 如何开始?>介绍了几款单元 ...
- Android单元测试框架Robolectric3.0介绍(二)
文章中的所有代码在此:https://github.com/geniusmart/LoveUT ,由于 Robolectric 3.0 和 3.1 版本(包括后续3.x版本)差异不小,该工程中包含这两 ...
- Android单元测试研究与实践
处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单元测试的用武之地.单元测试周期性对项目进行函数级别的测试,在良好的覆盖率下,能够持续维护代码逻辑,从而支持项目从容 ...
- Android单元测试 - Sqlite、SharedPreference、Assets、文件操作 怎么测?
前言 上篇<Android单元测试 - 几个重要问题> 讲解了"何解决Android依赖.隔离Native方法.静态方法.RxJava异步转同步"这几个Presente ...
- android单元测试作用,Android单元测试源码解读
Android手机操作系统是一个开源的操作系统.程序员们可以在模拟器的帮助下对其进行修改,来实现各种功能需求,满足用户的应用.在这里我们先来了解一下Android单元测试的相关内容. 在网络上找了半天 ...
- android单元测试android环境,基于Robolectric的Android单元测试 —环境搭建与部署运行...
移动端的测试中,因为回归一些逻辑分支比较多的功能时工作量比较大,且不太适合用UI完成,尝试通过单元测试来完成.几经波折终于完成了一个功能的UT用例并在CI上部署运行,现总结如下: 一.Robolect ...
- Android单元测试 Instrumentation
开发中我们需要对部分功能进行单元测试,启动Activity来测试部分小功能,有点小题大作,杀鸡用牛刀. 我们可以用Android单元测试 Instrumentation 本篇只是入门,起到抛砖的效果 ...
- (转)Android单元测试
关键字: camera unit test android源代码中每个app下中都自带了一个test用例,下面主要介绍下camra单元测试用例 在AndroidManifest.xml中标明了测试用例 ...
最新文章
- java中实现具有传递性吗_Java中volatile关键字详解,jvm内存模型,原子性、可见性、有序性...
- 收藏 | Tensorflow实现的深度NLP模型集锦(附资源)
- Adam又要“退休”了?耶鲁大学团队提出AdaBelief
- 你连原理都还没弄明白?java的基本单位
- cocos2d-x游戏开发(七)对象释放时机
- “*** IS NOT TRANSLATED IN …….. 解决办法
- gitlab bash_如何编写Bash一线式以克隆和管理GitHub和GitLab存储库
- LeetCode 2075. 解码斜向换位密码(模拟)
- 基础算法 —— 高精度计算 —— 高精度乘法
- 执行计划级别mysql 2ef,Mysql 层级、执行顺序、执行计划分析
- 大数据_MapperReduce_Hbase的优化_存数据_自动计算分区号 自动计算分区键---Hbase工作笔记0027
- 像玩乐高一样玩 simpletun
- Flutter InkWell 动画浅析
- 微软Windows7将捆绑杀软 众厂商面临生死抉择
- python中对象的定义_全面了解python中的类,对象,方法,属性
- 锻造成形与计算机技术,铸造成型及控制工程
- 整流八--电网不平衡状态下三相PWM整流器的控制策略
- 【web素材】04—40款个人主页简历网页模板及企业单页
- tc android开发工具,TC5.0 (一个脚本开发工具)其底层实现原理分析与推测(半成品)...
- idea Push Tags选All还是Current Branch?
热门文章
- Win7激活工具的原理是什么?
- 巨蟒python全栈开发-第5天 字典集合
- 关于中国移动宽带(中国铁通)比较卡
- pl sql面试题_PL SQL面试问答
- 安卓 多条通知_【安卓+苹果】石头阅读,全网小说、漫画免费看,最好用的追书神器!...
- C++ 一维高斯积分的实现
- 关于DTC诊断故障码的获取与清除(ISO14229系列之14、19服务)
- GD32 startup.s
- lintcode 丢鸡蛋
- 2.4 数值分析: Doolittle直接三角分解法