什么是单元测试?

一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,然后对这个单元的单个最终结果的某些假设进行检验。

单元测试思路:
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)