通用
assert:用于条件验证,verify:验证方法是否被调用、调用次数、顺序、参数等
answers:mock函数体,coAnswers:mock协程函数体
returns:mock函数的返回值,returnsMany:函数多次调用时按顺序返回多个不同值
throws:mock异常
mockk<T>():创建接口/类的 mock 实例
class Person() {
var name: String = ""
var age: Int = 0
}
// 1. relaxed = true 代表所有属性用默认值。否则,每个属性你都要手动mock。如果访问未mock的属性或函数时,会抛异常
val person = mockk<Person>(relaxed = true)
// 2. mock时自定义属性的值
val person = mockk<Person>(relaxed = true)
every { person.name } returns "hello"
every { person.age } returns 18
// 3. 也可以用下面这种写法
val person = mockk<Person>(relaxed = true) {
every { name } returns "hello"
every { age } returns 18
}
mockkObject:用于 Kotlin object 单例
假设我们有这样一个对象:
// File: Utils.kt
object Utils {
fun getCurrentTime(): Long = System.currentTimeMillis()
fun greet(name: String): String = "Hello, $name"
}
单元测试中 mock 它:
import io.mockk.*
import kotlin.test.*
class UtilsTest {
@BeforeTest
fun setUp() {
mockkObject(Utils) // ✅ mock 单例对象
}
@AfterTest
fun tearDown() {
unmockkObject(Utils) // ✅ 清理 mock,防止污染其他测试
}
@Test
fun testMockObjectFunction() {
every { Utils.getCurrentTime() } returns 123456789L
every { Utils.greet("Alice") } returns "Hi, Alice"
assertEquals(123456789L, Utils.getCurrentTime())
assertEquals("Hi, Alice", Utils.greet("Alice"))
verify {
Utils.getCurrentTime()
Utils.greet("Alice")
}
}
}
mockkStatic:针对顶层/静态方法进行 mock
kt文件-顶层函数
如果你要 mock 的是顶层函数,最简洁的方式是直接传入函数引用
假设在 com.example.HelloWorld.kt 文件中有:
package com.example
fun sayHello(): String = "Hello Kotlin!"
mock方式:
import com.example.sayHello // 导入顶层函数
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Test
import kotlin.test.assertEquals
class HelloWorldTest {
@Test
fun `mock sayHello via function reference`() {
// 使用函数引用
mockkStatic(::sayHello)
every { sayHello() } returns "Mocked Hello"
assertEquals("Mocked Hello", sayHello())
}
}
mockkStatic(::sayHello)
会把顶层函数sayHello
变为可mock的静态方法
后续再对同名函数的调用,均走 stub 逻辑,而不执行原始实现。
kt文件-顶层扩展函数
在 com/example/StringExt.kt 文件中, 定义了一个String的扩展函数
package com.example
fun String.starSurround(): String = "*$this*"
要 mock 顶层扩展函数,你不能这么写:
mockkStatic(::starSurround) // ❌ 不行!
正确写法:
mockkStatic("com.example.StringExtKt") // 传入编译后生成的 Java 类名(文件名 + Kt)
every { any<String>().starSurround() } returns "#hello#"
assertEquals("hello".startSurround(), "#hello#")
问题排查记录:
1.mock没有成功,要判断是不是代码中用到的对象和单元测试中的mock对象是否是同一个
遇到的坑!!!
坑一:多次调用 mockkStatic(...) 同一类,后写的会覆盖前面写的
// 工具类 Util.kt,定义了两个顶层函数
fun getProductList():{}
fun getTitle():{}
// 代码示例
mockkStatic("com.example.app.UtilKt")
every { Util.getProductList() } answers { println("被mock了") }
mockkStatic(::getTitle) // ❌ 这个mock函数会覆盖上面的mock,导致上面的mock失效
every { getTitle() } answers { println("被mock了") }
✅正确的写法是:如果一个类中有多个函数需要mock,应该用“包名”的写法
mockkStatic("com.example.app.UtilKt")
every { Util.getProductList() } answers { println("被mock了") }
every { Util.getTitle() } answers { println("被mock了") }
坑2:public属性应该直接赋值,不能用every方法mock
MockK的主要功能是拦截和控制模拟对象的方法调用以及属性的访问(即 getter 和 setter 的行为)。MockK 并没有为公共字段(public)的行为添加额外的拦截逻辑,而是保留了直接操作字段的能力
如果我们尝试用 MockK 的 every 语法来模拟公共字段,比如:
mockk<PackageInfo>(relaxed = true) {
every { packageName } returns appPkgName
}
会抛出异常(例如 “Missing mocked calls inside every { ... } block”)。这是因为 every 是设计用来模拟方法调用或属性 getter/setter 行为的,而 packageName 是一个字段,不是方法或属性。MockK 无法拦截对字段的直接访问,因此这种写法是无效的。
✅正确的写法应该直接赋值:
mockk<PackageInfo>(relaxed = true) {
packageName = appPkgName
}
坑3:TextUtils.equals("string","string"),即使一样也返回false
原因:在普通的 JVM 单元测试(runTest)里,你并不是真在 Android 设备/模拟器上跑代码,而是在本地用 Android SDK 里那个“空壳” android.jar(只包含接口和方法签名,不包含真正实现)来编译和运行。也就是说,android.text.TextUtils.equals(...) 在本地测试时是个空方法,返回的永远都是它的默认值——false。
直接用 == 运算符,它在幕后会调用 equals() 做“结构相等”(equals())比较,并且对 null 安全:
assertEquals("aaa","aaa")
坑4:Android 自带的 org.json 在 JVM 测试里并不一定可用
// 这段代码打印 null
@Test
fun test() = runTest {
val jsonObject = JSONObject()
println(jsonObject)
}
Android 的 org.json 并不是标准 Java 的实现,而是 Android SDK 内部提供的一个版本。这时,如果依赖 org.json,实际用到的是 stub(空实现),导致 JSONObject 等不可用,表现为构造对象返回null、方法调用无实际功能,甚至抛异常。
解决办法
办法一:在 build.gradle 文件中为 testImplementation 增加 org.json 的依赖。版本号可在maven仓库查看
testImplementation 'org.json:json:20250517'
办法二:或者,将相关测试迁移到 androidTest 目录