什么是单元测试?
一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,然后对这个单元的单个最终结果的某些假设进行检验。
单元测试思路:
1.确认待测试的方法或对象
2.为待测试的方法构造初始化条件
3.调用(运行)该测试方法
4.比较被测试方法的行为(结果)与预期的是否一致
设置测试环境
在 Android Studio 项目中,必须将本地单元测试的源文件存储在 module-name/src/test/java/ 中。当创建新项目时,此目录已存在。
在应用的顶级 build.gradle 文件中,将以下库指定为依赖项:
dependencies {
testImplementation ‘junit:junit:4.12’
testImplementation “io.mockk:mockk:1.9.3”
}
Mock 的定义
mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两个目的:
1.验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
2.指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作
为什么使用Mock测试
单元测试是为了验证代码运行正确性,注重的是代码的流程以及结果的正确与否。
对比真实运行代码,可能其中有一些外部依赖的构建步骤相对麻烦,如果按照真实代码的构建规则构造出外部依赖,会大大增加单元测试的工作,代码也会参杂太多非测试部分的内容,测试用例显得复杂难懂。
采用 Mock 框架,可以虚拟出一个外部依赖,只注重代码的流程与结果,真正地实现测试目的。
MockK 介绍
MockK 是一个用 Kotlin 写的 Mocking 框架。通过mockk<>(), mockkObject(), spyk()返回的对象就处于mock状态。只有处于这个状态的对象才能通过every{}对对象的行为进行Mock。mockk框架遵循 mock – 监听 – 执行 – 验证的流程。
testImplementation "io.mockk:mockk:1.9.3"
import io.mockk.*
注解
注解 | 描述 |
---|---|
@Test | 测试注解,标记一个方法可以作为一个测试用例 。 |
@Before | Before注解表示,该方法必须在类中的每个测试之前执行,以便执行某些必要的先决条件 |
@After | After注释表示,该方法在每项测试后执行(如执行每一个测试后重置某些变量,删除临时变量等)。 |
断言方法 | 作用 |
---|---|
assertTrue | 真值断言 |
assertFalse | 假植断言 |
assertEquals | 相等断言 |
assertNotEquals | 不等断言 |
assertNull | 空值断言 |
assertNotNull | 非空值断言 |
assertFail | 断言失败 |
Failure一般由单元测试使用断言方法判断失败所引起,表示测试点发现了问题,程序的输出结果与预期结果不符
举个例子
public class ServiceImplA implements Service {
@Override
public void doSomething1(String s) {
}
@Override
public String doSomething2(String s) {
return s;
}
private String name = null;
private String setName(String name){
this.name = name;
}
}
mock 普通对象
通过语句 mockk(…)来mock一个对象
这个方法返回T的实例,该实例所有函数都为待mock状态,这些待mock状态的函数都不能直接调用,需要结合every{}语句mock对应方法后才能调用
//返回ServiceImplA一个mock对象
val mockk = mockk<ServiceImplA>()
//mock指定方法
every { mockk.doSomething1(any()) } returns Unit
//调用被mock的方法
mockk.doSomething1("")
//该方法未通过every进行mock,会报错
mockk.doSomething2("")
every{…}语句 用来监听指定的代码语句,并做出接下来的动作,例如:
return value返回某个值
just Runs 继续执行(仅用于 Unit 方法)
answer {} 执行某个语句块
当在测试代码中执行到every{…}中的方法时并不会真的去执行,而是直接返回returns之后的对象。也可以不加returns而使用every { } just Runs跳过执行。
mockk Object类
将指定对象转为可mock状态
与mockk<>()的区别是返回的mock对象,允许mock行为跟真实行为并存,如果不主动mock,则执行真实行为。
val serviceImplA = ServiceImplA()
mockkObject(serviceImplA)
every { serviceImplA.doSomething1(any()) } returns Unit
//调用被mock方法
serviceImplA.doSomething1("")
//调用真实方法
serviceImplA.doSomething2("")
如果要验证、执行 object类里面的私有方法,需要在mock的时候指定一个值 recordPrivateCalls, 它默认是false:
mockkObject(serviceImplA, recordPrivateCalls = true)
给mock对象设置私有属性
而私有属性的设置需要通过反射来实现,在 mockk 中,需要使用 InternalPlatformDsl 这个类:
InternalPlatformDsl.dynamicSetField(serviceImplA, "name", "fyn")
执行 mock对象私有方法
对于私有方法,通过反射来实现,也需要调用 InternalPlatformDsl
这个类:
InternalPlatformDsl.dynamicCall(serviceImplA, "setName", arrayOf("new name"))
{mockk()}
mock 静态类
mockkStatic(serviceImplA::class)
spyk() & spyk(T obj)
返回T的spyk对象或者obj的spyk对象
与mockk<>()的区别是,spyk<>()返回的对象是允许真实行为跟mock行为共存的,其表现跟mockkObject()相似
//返回ServiceImplA的一个spyk对象
val spyk = spyk<ServiceImplA>()
every { spyk.doSomething1(any()) } returns Unit
//调用mock方法
spyk.doSomething1("123")
//调用真实方法
spyk.doSomething2("999")
val serviceImplA = ServiceImplA()
//返回serviceImplA对象被spyk后的对象,原对象不会改变
val spyk1 = spyk(serviceImplA)
//serviceImplA不是可mock状态,这里会报错
//every { serviceImplA.doSomething1(any()) } returns Unit
//mock
every { spyk1.doSomething1(any()) } returns Unit
//调用mock方法
spyk1.doSomething1("999")
//调用真实方法
spyk1.doSomething2("999")
returns
作用是定制mock行为的结果
val spyk = spyk<ServiceImplA>()
//mock doSomething2,无论什么输入都返回111
every { spyk.doSomething2(any()) } returns "111"
val input = "222"
//这里拿到的应该是111
val mockkResult = spyk.doSomething2(input)
println("mockk行为结果:$mockkResult")
val real = ServiceImplA()
//这里拿到的应该是222
val realResult = real.doSomething2(input)
println("mockk行为结果:$realResult")
验证多个方法被调用
如果想验证这两个方法执行了的话,可以把两个方法都放在 verify {…} 中进行验证。如果确认成功那么测试通过,否则报错。
@Test
fun test() {
//返回ServiceImplA一个mock对象
val mockk = mockk<ServiceImplA>()
//mock指定方法,设置监听
every { mockk.doSomething1(any()) } returns Unit
every { mockk.doSomething2(any()) } returns Unit
verify {
mockk.doSomething1("sfas")
mockk.doSomething2("sfas")
}
}
为无返回值的方法分配默认行为
把 every {…} 后面的 Returns 换成 just Runs ,就可以让 MockK 为这个没有返回值的方法分配一个默认行为。
@Test
fun test() {
val serviceImplA = mockk<ServiceImplA>()
every { serviceImplA.doSomething1(any()) } just Runs
verify { serviceImplA.doSomething1(any()) }
}
验证方法被调用的次数
如果不仅想验证方法被调用,而且想验证该方法被调用的次数,可以在 verify 中指定 exatcly、atLeast 和 atMost 属性。
// 验证调用了两次
verify(exactly = 2) { serviceImplA.doSomething1(any()) }
// 验证调用了最少一次
// verify(atLeast = 1) { serviceImplA.doSomething1(any()) }
// 验证最多调用了两次
// verify(atMost = 2) { serviceImplA.doSomething1(any()) }
验证 Mock 方法都被调用了
verifyAll {
serviceImplA.doSomething1(any())
serviceImplA.doSomething2(any())
serviceImplA.doSomething3(any())
serviceImplA.doSomething4(any())
}
验证全部的 Mock 方法都按特定顺序被调用了
如果不仅想测试好几个方法被调用了,而且想确保它们是按固定顺序被调用的,可以使用 verifySequence {…} 。
verifySequence {
serviceImplA.doSomething1(any())
serviceImplA.doSomething2(any())
serviceImplA.doSomething3(any())
serviceImplA.doSomething4(any())
}
验证mock对象私有方法
验证是放在 verify{…} 中的,也是通过反射的方式来验证:
verify{ mockClass["privateFunName"](arg1, arg2, ...) }
主要分成三个部分:
1.mock类
2.中括号,里面填入双引号+私有方法名
3.小括号,里面填入传参
注:mock的object类也需要设置 recordPrivateCalls 为true
延迟验证
使用 verify(timeout) {…} 就可以实现延迟验证,比如下面代码中的 timeout = 2000 就表示在 2 秒后检查该方法是否被调用。
verify(timeout = 2000) { serviceImplA.doSomething1(any()) }
assertEquals
判断effectedNum和预期值1是否相同,如果不同则测试fail
assertEquals(expected:1,effectedNum)
relaxed
有一个被测类 Car,它依赖于一个 Engine:
class Car(private val engine: Engine) {
fun getSpeed(): Int {
return engine.getSpeed()
}
}
class Engine {
fun getSpeed(): Int {
return calSpeed()
}
private fun calSpeed(): Int {
return 30
}
}
要测试 getSpeed(),它依赖于 Engine 里的方法,所以需要 mockk 一下 Engine,写下面的测试方法:
fun testCar() {
// mock engine对象
val engine = mockk<Engine>()
val car = Car(engine)
// 这里是私有方法设置监听的写法:
every { engine["calSpeed"]() } returns 30
val speed = car.getSpeed()
assertEquals(speed, 30)
}
但是这里报了一个错误: io.mockk.MockKException: no answer found for: Engine(#1).getSpeed()
这是因为mockk是严格去执行每个方法,而 Engine虽然mock了出来,但是mockk并不知道 Engine.getSpeed() 需不需要往下执行,所以它抛出了一个错误。
这个时候,你有三种解决方案。
方案一:将 Engine 的构建从 mock 改成 spy,因为spy可以真实模拟对象行为: engine = spyk()
方案二:抛弃 calSpeed 方法, 使用 every { engine.getSpeed() } returns 30
方案三:在 mock Engine 时, 将 relaxed 置为true, engine = mockk(relaxed = true)