Kotlin Multiplatform单元测试-mockk工具

2023-11-22  本文已影响0人  BlueSocks

技术目标

MockK是一款功能强大、易于使用的Kotlin mocking框架。它具有简洁的语法和强大的功能,能够过帮助开发者轻松的进行单元测试、集成测试。MockK提供了一套丰富灵活的API,可以轻松地创建模拟对象并进行相关的操作,来验证方法调用和预期的返回值。另外,它还提供了Mockito、PowerMock等不具备的高级功能,例如mock静态类、final类等。本文将介绍MockK在KMP中的基本使用方法,并深入探讨一些额外的高级特性。

前期分析

由于Mockito、PowerMockito主要针对Java语言进行的设计,因此在处理kotlin语言上存在缺陷。MockK是从零开始专门为Kotlin构建,它能够针对Kotlin实现更强大和高级的功能。

Mockito存在的问题

PowerMockito存在的问题

MockK的优势

  1. 强大的mock能力:MockK支持final class、匿名内部类以及基本类型的mock,同时支持静态、final方法的mock。

  2. 简化测试代码:MockK提供了简洁而直观的 API,使得创建和管理模拟对象变得容易。它的语法清晰简洁,可以快速定义模拟对象的行为和预期结果,从而减少冗余的测试代码。

  3. 模拟复杂场景:MockK不仅可以模拟普通的对象行为,还可以处理更复杂的场景,如模拟 lambda 表达式、捕获函数调用参数等。这使得在测试中处理回调函数、异步操作或依赖其他组件的情况变得更加容易。

  4. 支持依赖注入框架:MockK可以与常见的依赖注入框架(如Koin、Dagger)集成,使得在单元测试中模拟依赖项变得更加便捷。通过模拟依赖项,我们可以更好地隔离被测试单元的功能,并提供更可靠的测试环境。

使用教程

引入Mockk

首先mockk仅支持JVM平台,如果在KMP中编写了通用的commonMain代码,那么它将无法工作。由于项目中KMP支持的平台有Android、iOS、PC Mac、Pc windows,而没有支持专门JVM平台,因此考虑将mockk放置到Android平台的androidUnitTest中,同时让Android的单元测试运行在JVM平台上。

image.png

在androidUnitTest源码集合添加依赖

val androidUnitTest by getting {
    dependencies {
        implementation("io.mockk:mockk:1.12.0")
    }
}

备注:不要使用官网提示的testImplementation,KMP中没有这个方法支持

  1. 基本使用

class Kid(private val mother: Mother) {
    var money = 0
        private set

    fun wantMoney() {
        money += mother.giveMoney()
    }
}

class Mother {
    fun giveMoney(): Int {
        return 100
    }

package com.subscribe.kmpproject.unittest
import com.subscribe.kmpproject.unit.Kid
import com.subscribe.kmpproject.unit.Mother
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import junit.framework.TestCase.assertEquals
import kotlin.test.Test

class CommonGreetingTest {

    @Test
    fun testExample() {
        // 准备阶段
        val mother = mockk<Mother>()
        val kid = Kid(mother)
        every { mother.giveMoney() } returns 30

        // 执行阶段
        kid.wantMoney()

        // 校验阶段
        verify {
            kid.wantMoney()
        }
        assertEquals(30, kid.money)
    }

}

备注:其中every定义了mock对象mother的行为,当调用giveMoney时,返回值为30; verify用于校验kid对象的wantMoney是否被调用过。

  1. 参数匹配

every { 
    mockObj.someMethod(any()) 
} returns "Mocked Result"

备注:在定义mock对象行为时,可以进行参数匹配,此处使用了any()表明可以匹配任意的输入参数。

  1. 函数验证

verify { 
    mockObj.someMethod() 
}

备注:校验函数是否被调用过,前面的例子中已经写了。更高级的,还可以校验函数的调用次数、顺序、参数匹配等等。

verify(exactly = 10) { 
    mockObj.someMethod() 
}

备注:校验方法必须精确被调用10次

verify { 
    mockObj.firstMethod() 
    mockObj.secondMethod() 
}

备注:校验调用顺序,firstMethod必须在secondMethod之前进行调用,否则验证不通过

  1. 偏函数模拟

every { 
    mockObject.someMethod(any()) 
} answers { 
    originalCall(it.invocation.args.first()) 
}

备注:对于某些方法调用,我们并不想完全使用模拟的值,而是想使用特定的函数调用过程,那么可以使用originalCall来实现对实际函数的调用。

  1. 构造函数

mockkConstructor(MyClass::class)
every { 
    anyConstructed<MyClass>().someMethod() 
} returns "Mocked Result"// 执行测试代码
unmockkConstructor(MyClass::class)

备注:使用mockkConstructor方法mock构造函数,并通过anyConstructed进行类的构造,最后通过 unmockkConstructor取消构造函数的mock。

  1. Lambada表达式

val lambdaMock: () -> Unit = mockk()
every { 
    lambdaMock.invoke() 
} just Runs

  1. 使用注解进行mock

class Car {
    fun getName(): String {
        return "NewCar"
    }
}

class AnnotationTest {

    @MockK
    lateinit var car: Car

    @Before
    fun setUp() {
        MockKAnnotations.init(this)
    }

    @Test
    fun getName() {
        every { car.getName() } returns "MyCar"
        val name = car.getName()
        assertEquals("MyCar", name)
    }
}

备注:使用@MockK可以mock并注入一个对象,同时需要在@Before初始化函数中调用注入方法MockKAnnotations.init(this)

  1. 所有方法跳过准备

class Car {
    fun getName(): String {
        return "NewCar"
    }
}

class AnnotationTest {

    @MockK
    lateinit var car: Car

    @Before
    fun setUp() {
        MockKAnnotations.init(this)
    }

    @Test
    fun getName() {
        val name = car.getName()
    }
}

对于mock对象而言,其方法的调用都需要预设行为,否则会报错:io.mockk.MockKException: no answer found for: Car(car#1).getName()。如果我们不想对每个方法都预设,比如一个对象的方法实在太多了有上千个,那么我们可以使用以下三种方案,来取消对象的方法预设:

@Test
fun getName() {
    val car = mock<Car>(relaxed = true)
}

@RelaxedMockK
lateinit var car: Car

@MockK
lateinit var car: Car

@Before
fun setUp() {
    MockKAnnotations.init(this, relaxed = true)
}

  1. Unit方法跳过准备

返回值为Unit类型的跳过校验,而非Unit的方法不跳过校验

@Test
fun getName() {
    val car = mock<Car>(relaxUnitFun = true)
}

  1. 抓取参数

class Mother {
    fun inform(money: Int) {
        println("Mother.inform $money 元")
    }

    fun giveMoney(): Int {
        return 100
    }
}

class CaptureTest {

    @Test
    fun getName() {
        // 准备
        var mother: Mother = mockk<Mother>()
        val slot = slot<Int>()
        every { mother.inform(capture(slot)) } just Runs

        // 执行
        mother.inform(0)

        // 校验
        assertEquals(0, slot.captured)
    }
}

备注:首先在准备阶段创建了一个slot槽位,接着配合capture函数定义slot可以捕获到值。在执行阶段inform传入的参数,可以被slot捕获到,并存储在slot.captured的变量中。

  1. 静态方法

object UtilKotlin {
    @JvmStatic
    fun method(): String {
        return "UtilKotlin.ok()"
    }
}

class Utils {
    fun method() {
        UtilKotlin.method()
    }
}

class StaticClassTest {

    @Test
    fun testMethod() {
        // 准备
        val utils = Utils()
        mockkStatic(UtilKotlin::class)
        every { UtilKotlin.method() } returns "MockResult"

        // 执行
        utils.method()

        // 校验
        verify { UtilKotlin.method() }
        assertEquals("MockResult", UtilKotlin.method())
    }
}

备注:实际上Java的类的静态方法也可以模拟,不过咱这里在KMP环境中只针对kotlin

  1. 静态对象

class UtilKotlinX {
    companion object {
        @JvmStatic
        fun method(): String {
            return "UtilKotlinX.ok()"
        }
    }
}
class UtilsX {
    fun method() {
        UtilKotlinX.method()
    }
}

class ObjectTest {

    @Test
    fun testMethod() {
        // Given
        val utilsX = UtilsX()
        mockkObject(UtilKotlinX)
        mockkObject(UtilKotlinX.Companion)

        every { UtilKotlinX.method() } returns "Test"

        // When
        utilsX.method()

        // Then
        verify { UtilKotlinX.method() }
        assertEquals("Test", UtilKotlinX.method())
    }
}

备注:模拟的如果是静态方法,那么参考13;模拟的如果是一个对象,那么使用mockObject即可

总结

MockK是一款功能强大、易于使用的Kotlin mocking框架,由于专门针对Kotlin进行设计,可以轻松的支持static方法、static类、final类mock。在Kotlin Multiplatform项目中,由于MockK不支持跨平台只支持JVM平台,因此需要将commonMain的测试代码,放置在可以运行于JVM虚拟机的源码集中。MockK的使用也比较简单,会使用Mokito的话很容易上手。下一篇将记录在KMP中的使用情况,以及自动构建需要如何配置。

上一篇下一篇

猜你喜欢

热点阅读