Kotlin Multiplatform单元测试-mockk工具篇

写在前面

前言

最近项目中使用KMP开发了RTC音视频和IM数据多端同步两个模块,由于时间、多平台调试等原因项目模块匆忙上线,很遗憾没有完成单元测试。现在项目基本上稳定运行,有较多的时间可以补充遗留的单元测试内容,同时也有时间记录下KMP中单元测试工具的使用方法,以便其他开发者更优雅地编写KMP中的Kotlin单元测试。

技术目标

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

前期分析

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

Mockito存在的问题

  • 类型:Mockito不支持对 final class、匿名内部类以及基本类型(如 int)的 mock。
  • 方法:Mockito不支持对静态方法、 final 方法、私有方法、equals() 和 hashCode() 方法进行 mock。
  • 关键字:Mockito使用时when要加上反引号才能使用(与kotlin关键字when冲突),这种写法非常不友好。

PowerMockito存在的问题

  • 兼容性:在Android上编写单元测试,使用了某版本无法支持对静态方法与final类进行mock

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平台上。

在androidUnitTest源码集合添加依赖

kotlin 复制代码
val androidUnitTest by getting {
    dependencies {
        implementation("io.mockk:mockk:1.12.0")
    }
}

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

基本使用

  • 在commonMain中编写产品类
kotlin 复制代码
class Kid(private val mother: Mother) {
    var money = 0
        private set

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

class Mother {
    fun giveMoney(): Int {
        return 100
    }
  • 在androidUnitTest中编写测试类
kotlin 复制代码
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是否被调用过。

参数匹配

sql 复制代码
every { 
    mockObj.someMethod(any()) 
} returns "Mocked Result"

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

函数验证

markdown 复制代码
verify { 
    mockObj.someMethod() 
}

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

scss 复制代码
verify(exactly = 10) { 
    mockObj.someMethod() 
}

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

scss 复制代码
verify { 
    mockObj.firstMethod() 
    mockObj.secondMethod() 
}

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

偏函数模拟

scss 复制代码
every { 
    mockObject.someMethod(any()) 
} answers { 
    originalCall(it.invocation.args.first()) 
}

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

构造函数

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

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

Lambada表达式

css 复制代码
val lambdaMock: () -> Unit = mockk()
every { 
    lambdaMock.invoke() 
} just Runs

使用注解进行mock

kotlin 复制代码
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)

所有方法跳过准备

kotlin 复制代码
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()。如果我们不想对每个方法都预设,比如一个对象的方法实在太多了有上千个,那么我们可以使用以下三种方案,来取消对象的方法预设:

mock构造参数

kotlin 复制代码
@Test
fun getName() {
    val car = mock<Car>(relaxed = true)
}

mock注解指定

kotlin 复制代码
@RelaxedMockK
lateinit var car: Car

mock注解初始化

kotlin 复制代码
@MockK
lateinit var car: Car

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

Unit方法跳过准备

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

kotlin 复制代码
@Test
fun getName() {
    val car = mock<Car>(relaxUnitFun = true)
}
复制代码
### 抓取参数
kotlin 复制代码
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的变量中。

复制代码
### 静态方法
kotlin 复制代码
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

复制代码
### 静态对象
kotlin 复制代码
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即可

复制代码
### 其他功能

更多高级功能请参考:官网地址

源码参考

GitHub源码

总结

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

参考资料

相关推荐
Kapaseker18 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z3 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton3 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream4 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin
RdoZam4 天前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
Kapaseker4 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
糖猫猫cc5 天前
Kite:两种方式实现动态表名
java·kotlin·orm·kite
如此风景5 天前
kotlin协程学习小计
android·kotlin
Kapaseker5 天前
你搞得懂这 15 个 Android 架构问题吗
android·kotlin
zh_xuan5 天前
kotlin 高阶函数用法
开发语言·kotlin