介绍
在过去的一年里,MockK 在 Kotlin 世界中迅速崛起。用户通过提交问题和建议改进,积极帮助改进它。在最近的几个月里,MockK 引入了许多强大的功能。本文将介绍这些新可能性,帮助你提升测试水平。
分层 Mock
在 1.9.1 版本中,MockK 支持所谓的分层 Mock。例如,假设有以下相互依赖的接口:
interface AddressBook {
val contacts: List<Contact>
}
interface Contact {
val name: String
val telephone: String
val address: Address
}
interface Address {
val city: String
val zip: String
}
需要模拟整个通讯录,可以通过 mockk
函数的初始化块在其中指定行为,并通过链在 every
的返回值中放置其他 Mock。这导致了一个漂亮的 DSL 用于定义依赖对象:
val addressBook = mockk<AddressBook> {
every { contacts } returns listOf(
mockk {
every { name } returns "John"
every { telephone } returns "123-456-789"
every { address.city } returns "New-York"
every { address.zip } returns "123-45"
},
mockk {
every { name } returns "Alex"
every { telephone } returns "789-456-123"
every { address } returns mockk {
every { city } returns "Wroclaw"
every { zip } returns "543-21"
}
}
)
}
可以注意到,这里通过链调用模拟了 address.city
和 address.zip
。这是一种替代方法,实际层次结构当然会包含真实对象和 Mock 的混合。
val serviceLocator = mockk<ServiceLocator> {
every { transactionRepository } returns mockk {
coEvery { getTransactions() } returns Result.build {
listOf(
NoteTransaction("28/10/2018", "Content of note1.", TransactionType.DELETE),
NoteTransaction("28/10/2018", "Content of note2.", TransactionType.DELETE),
NoteTransaction("28/10/2018", "Content of note3.", TransactionType.DELETE)
)
}
coEvery { deleteTransactions() } returns Result.build { Unit }
}
every { remoteRepository } returns mockk {
coEvery { getNotes() } returns Result.build {
listOf(
Note("28/10/2018", "Content of note1.", 0, "", User("8675309", "Ajahn Chah", "")),
Note("28/10/2018", "Content of note2.", 0, "", User("8675309", "Ajahn Chah", "")),
Note("28/10/2018", "Content of note3.", 0, "", User("8675309", "Ajahn Chah", ""))
)
}
coEvery { synchronizeTransactions(any()) } returns Result.build { Unit }
}
}
协程
MockK 从一开始就支持协程,但在过去的一年里,内部引擎变得更加先进,同时还增加了一些语法条款。所有与协程一起使用的 MockK 函数都构建在常规函数+前缀 co
上。例如 every
变为 coEvery
,verify
变为 coVerify
,answers
变为 coAnswers
。这些函数接受挂起的 lambda 而不是常规 lambda,并允许调用 suspend
函数。
coEvery { mock.divide(capture(slot), any()) } coAnswers { slot.captured * 11 }
从表面上看,一切都与常规模拟非常相似。但内部并不简单。
第一个问题是,当调用 divide
函数时,在字节码层面上有隐含的 continuation 参数作为最后一个参数。这个 continuation 是在结果准备好后使用的回调。这意味着库需要将这个 continuation 传递给 coAnswers
条款,所以这不仅仅是 runBlocking
挂起的lambda。
第二个区别是 divide
悬挂函数可能决定暂停。这意味着内部它会返回特殊的暂停标记。当计算恢复时,divide
被再次调用。因此,每次这种恢复调用将计为 divide
函数的额外调用。这意味着在验证期间需要注意这一点。
验证超时
在处理协程时,人们经常忘记 launch
运行一个并行执行的任务,为了验证这种过程的结果,需要一个进程等待另一个进程。这可以通过 join
来实现。但需要显式地到处传递 Job
的引用。
为简化编写此类测试,MockK 支持带超时的验证:
mockk<MockCls> {
coEvery { sum(1, 2) } returns 4
launch {
delay(2000)
sum(1, 2)
}
verify(timeout = 3000) { sum(1, 2) }
}
简单地说,verify
条款将在验证条件满足时退出,或者在超时时抛出异常。这样就不需要显式等待 launch
中计算完成的时刻。
验证确认和排除记录
verifyAll
和 verifySequence
是两种验证模式,用于独占性地验证一组调用。没有可能出错并被多次调用。
另一方面,verifyOrder
和简化版的 verify
不是这样工作的,但它们提供了更大的灵活性。为了解决这种差异,你可以在所有验证块之后使用特别的验证确认调用。
confirmVerified(object1, object2)
这将确保 object1
和 object2
的所有调用都被 verify
条款覆盖。对于一些不重要的调用,可以使用 excludeRecords
结构将它们从确认验证中排除。
excludeRecords { object1.someCall(andArguments) }
可以使用参数匹配器排除一系列调用。
可变参数
从早期的 MockK 版本开始,简单地处理变长参数就已经存在,但从 1.9.1 版本开始,添加了更多的匹配器。简单示例如下:
interface VarargExample {
fun example(vararg sequence: Int): Int
}
val mock = mockk<VarargExample>()
every { mock.example(1, 2, 3, more(4), 5) } returns 6
此外,MockK 还增强了三个匹配器:anyVararg
、varargAll
和 varargAny
every { mock.example(*anyVararg()) } returns 1
every { mock.example(1, 2, *anyVararg(), 3) } returns 4
every { mock.example(1, 2, *varargAll { it > 5 }, 9) } returns 10
拓展函数和顶层函数
要成功模拟拓展和顶层函数,需要理解其工作原理。
顶层函数
Kotlin 将这些函数转换为特殊类的静态方法。例如,Code.kt
源文件中的 lowercase
转换为 CodeKt
类中的静态方法。因此,需要通过字符串参数 mockkStatic
来告诉 MockK 这个类名。
mockkStatic("pkg.CodeKt")
every { lowercase("A") } returns "lowercase-abc"
拓展函数绑定到类或对象
要模拟附加到类或对象的拓展函数,需要了解 dispatch receiver
和 extension receiver
的概念及其在 JVM 字节码中的相应位置。
class ExtensionExample {
fun String.concat(other: String): String
}
val mock = mockk<ExtensionExample>()
with (mock) {
every { any<String>().concat(any<String>()) } returns "result"
}
对象和枚举模拟
要模拟对象,传递对象给 mockkObject
即可,该对象将成为一个 spy
,允许依旧使用它作为原始对象,但可以存根、记录和验证行为。
object ExampleObject {
fun sum(a: Int, b: Int): Int
}
mockkObject(ExampleObject)
every { ExampleObject.sum(5, 7) } returns 10
模拟返回 Unit
的函数
默认情况下,在 MockK 中轻松处理返回 Unit
的函数,将降低样板代码,使其更简洁。
val mock = mockk<ExampleClass>(relaxUnitFun = true)
模拟返回 Nothing
的函数
对于返回 Nothing
的函数,唯一选择是抛出异常,以不滥用类型系统和运行时。
every { quit(1) } throws Exception("this is a test")
构造函数模拟
为了将刚创建(通过构造函数初始化)的对象转换为对象 mocks,可以使用所谓的构造函数 mocks。
mockkConstructor(MockCls::class)
every { anyConstructed<MockCls>().add(1, 2) } returns 4
MockCls().add(1, 2) // 返回 4
模拟私有函数
在罕见情况下,可能需要模拟私有函数。这个过程较为复杂,因为不能直接调用此类函数。
val mock = spyk(ExampleClass(), recordPrivateCalls = true)
every { mock["sum"](any<Int>(), 5) } returns 25
或使用:
every { mock invoke "openDoor" withArguments listOf("left", "rear") } returns "OK"
模拟属性
通常可以像模拟 get/set
函数或字段访问一样模拟属性。对于更多场景,可以使用其他方法。
every { mock getProperty "speed" } returns 33
every { mock setProperty "acceleration" value less(5) } just Runs
verify { mock getProperty "speed" }
verify { mock setProperty "acceleration" value less(5) }
类模拟
需要创建指定类的 mock,可以使用 mockkClass
函数。
val mock = mockkClass(ExampleClass::class)
配置
MockK 支持一些全局开关。这些开关可以放入 io/mockk/settings.properties
中。
清理
正确的清理是重要的话题。clear
函数删除与 mocks 关联的对象的内部状态,而 unmock
函数则恢复类的转换。
clearAllMocks() // 清除所有 mocks
clearMocks(...) // 清除指定 mocks
unmockkObject(...) // 恢复对象的转换
unmockkAll() // 恢复所有转换
MockK 还有带 lambda 参数的版本,最后会执行相应的 unmockk
操作。
总结
MockK 提供了相当广泛的高级功能集,包括:分层模拟、协程、验证超时、验证确认和记录排除、可变参数、拓展和顶层函数、对象和枚举模拟、模拟返回 Nothing
、构造函数模拟、私有函数模拟、属性模拟、类模拟、配置和清理。