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中的使用情况,以及自动构建需要如何配置。

参考资料

相关推荐
AntDreamer5 小时前
在实际开发中,如何根据项目需求调整 RecyclerView 的缓存策略?
android·java·缓存·面试·性能优化·kotlin
极客先躯1 天前
java和kotlin 可以同时运行吗
android·java·开发语言·kotlin·同时运行
滴水成冰-2 天前
Kotlin-Flow学习笔记
笔记·学习·kotlin
_Shirley3 天前
android.view.InflateException: Binary XML file line #7: Error inflating class
android·xml·java·ide·kotlin·android studio
ChinaDragonDreamer3 天前
Kotlin:1.9.0 的新特性
android·开发语言·kotlin
帅次5 天前
Android Studio:驱动高效开发的全方位智能平台
android·ide·flutter·kotlin·gradle·android studio·android jetpack
深海呐5 天前
Android 用线程池实现一个简单的任务队列(Kotlin)
android·kotlin·线程池·延时任务队列·线程池延时任务
我命由我123456 天前
Kotlin 极简小抄 P2(插值表达式、运算符、选择结构赋值)
android·java·开发语言·后端·kotlin·安卓
宝杰X76 天前
Compose Multiplatform+kotlin Multiplatfrom第三弹
android·开发语言·kotlin
jiet_h7 天前
Kotlin 中的 `flatMap` 方法详解
开发语言·微信·kotlin