Mock
Mockito
Mockito
是一个用 Java 写的 Mocking(模拟)框架
Mockito 存在的问题
- 类型: Mockito 不支持对 final class、匿名内部类以及基本类型(如 int)的 mock。
- 方法: Mockito 不支持对静态方法、 final 方法、私有方法、equals() 和 hashCode() 方法进行 mock。
在 Kotlin 写的测试中用 Mockito 会用得很不顺手,之所以不顺手有两点。
- 第一点是上面说到的 Mockito 不支持对 final 类和 final 方法进行 mock,而在 Kotlin 中类和方法默认都是 final 的,也就是当你使用 Mockito 模拟 Kotlin 的类和方法时,你要为它们加上 open 关键字,如果你的项目中的类都是用 Kotlin 写的,那这一点会让你非常头疼。
- 第二点是 Mockito 的 when 方法与 Kotlin 的关键字冲突了,当 when 的数量比较多时,写出来的代码看上去会比较别扭,比如下面这样的。
@Test
fun testAdd() {
`when`(calculator!!.add(1, 1)).thenReturn(2)
assertEquals(calculator!!.add(1, 1), 2)
}
MockK
是一个用 Kotlin 写的 Mocking 框架,它解决了所有上述提到的 Mockito 中存在的问题。
MockK
快速入门
使用 MockK 测试 Calculator
@Test
fun testAdd() {
// 每一次 add(1, 1) 被调用,都返回 2
// 相当于是 Mockito 中的 when(…).thenReturns(…)
every { calculator.add(1, 1) } returns 2
assertEquals(calculator.add(1, 1), 2)
}
class GoodsPresenterTest {
private var presenter: GoodsPresenter? = null
// @MockK(relaxed = true)
@RelaxedMockK
lateinit var view: GoodsContract.View
@Before
fun setUp() {
MockKAnnotations.init(this)
presenter = GoodsPresenter()
presenter!!.attachView(view)
}
@Test
fun testGetGoods() {
val goods = presenter!!.getGoods(1)
assertEquals(goods.name, "纸巾")
}
}
在 MockK 中,如果你模拟的对象的方法是没有返回值
的,并且你也不想要指定该方法的行为,你可以指定 relaxed = true
,也可以使用 @RelaxedMockK
注解,这样 MockK 就会为它指定一个默认行为,否则的话会报 MockKException 异常。
为无返回值的方法分配默认行为
把 every {…}
后面的 Returns 换成 just Runs
,就可以让 MockK 为这个没有返回值的方法分配一个默认行为。
@Test
fun testGetGoods() {
val goods = presenter!!.getGoods(1)
every { view.showLoading() } just Runs
verify { view.showLoading() }
assertEquals(goods.name, "纸巾")
}
为所有模拟对象的方法分配默认行为
如果测试中有多个模拟对象,且你想为它们的全部方法
都分配默认行为,那你可以在初始化 MockK 的时候指定 relaxed 为 true,比如下面这样。
@Before
fun setUp() {
MockKAnnotations.init(this, relaxed = true)
}
使用这种方式我们就不需要
使用 @RelaxedMockK
注解了,直接使用 @MockK
注解即可。
验证多个方法被调用
在 GoodsPresenter 的 getGoods() 方法中调用了 View 的 showLoading() 和 hideLoading() 方法,如果我们想验证这两个方法执行了的话,我们可以把两个方法都放在 verify {…} 中进行验证。
@Test
fun testGetGoods() {
val goods = presenter!!.getGoods(1)
verify {
view.hideLoading()
view.showLoading()
}
assertEquals(goods.name, "纸巾")
}
验证方法被调用的次数
如果你不仅想验证方法被调用,而且想验证该方法被调用的次数
,你可以在 verify
中指定 exatcly
、atLeast
和 atMost
属性,比如下面这样的。
@Test
fun testGetGoods() {
val goods = presenter!!.getGoods(1)
// 验证调用了两次
verify(exactly = 2) { view.showToast("请耐心等待") }
// 验证调用了最少一次
// verify(atLeast = 1) { view.showToast("请耐心等待") }
// 验证最多调用了两次
// verify(atMost = 1) { view.showToast("请耐心等待") }
assertEquals(goods.name, "纸巾")
}
之所把 atLeast 和 atMost 注释掉,是因为这种类型的验证只能进行其中一种,而不能多种同时验证。
验证 Mock 方法都被调用了
Mock 方法指的是,我们当前调用的方法中,调用了的模拟对象的方法。
@Test
fun testGetGoods() {
val goods = presenter!!.getGoods(1)
verifyAll {
view.showToast("请耐心等待")
view.showToast("请耐心等待")
view.showLoading()
view.hideLoading()
}
assertEquals(goods.name, "纸巾")
}
验证 Mock 方法的调用顺序
@Test
fun testGetGoods() {
val goods = presenter!!.getGoods(1)
verifyOrder {
view.showLoading()
view.hideLoading()
}
assertEquals(goods.name, "纸巾")
}
验证全部的 Mock 方法都按特定顺序被调用了
如果你不仅想测试好几个方法被调用了,而且想确保它们是按固定顺序
被调用的,你可以使用 verifySequence {…}
,比如下面这样的。
@Test
fun testGetGoods() {
val goods = presenter!!.getGoods(1)
verifySequence {
view.showLoading()
view.showToast("请耐心等待")
view.showToast("请耐心等待")
view.hideLoading()
}
assertEquals(goods.name, "纸巾")
}
确认所有 Mock 方法都进行了验证
把我们的模拟对象传入 confirmVerified()
方法中,就可以确认是否验证了模拟对象的每一个方法。
@Test
fun testGetGoods() {
val goods = presenter!!.getGoods(1)
verify {
view.showLoading()
view.showToast("请耐心等待")
view.showToast("请耐心等待")
view.hideLoading()
}
confirmVerified(view)
assertEquals(goods.name, "纸巾")
}
验证 Mock 方法接收到的单个参数
如果我们想验证方法接收到的参数是预期的参数
,那我们可以用 capture(slot)
进行验证,比如下面这样的。
@Test
fun testCaptureSlot() {
val slot = slot<String>()
every { view.showToast(capture(slot)) } returns Unit
val goods = presenter!!.getGoods(1)
assertEquals(slot.captured, "请耐心等待")
}
验证 Mock 方法每一次被调用接收到参数
如果一个方法被调用了多次,可以使用 capture(mutableList)
将每一次被调用时获取到的参数记录下来, 并在后面进行验证,比如下面这样。
@Test
fun testCaptureList() {
val list = mutableListOf<String>()
every { view.showToast(capture(list)) } returns Unit
val goods1 = presenter!!.getGoods(1)
assertEquals(list[0], "请耐心等待")
assertEquals(list[1], "请耐心等待")
}
验证使用 Kotlin 协程进行耗时操作
使用 Mockito 测试异步代码,只能通过 Thread.sleep()
阻塞当前线程,否则异步任务还没完成,当前测试就完成了,当前测试所对应的线程也就结束了,没有线程能处理回调中的结果。
当我们的协程涉及到线程切换时,我们需要在 setUp() 和 tearDown() 方法中设置和重置主线程的代理对象。
使用 verify(timeout) {…}
就可以实现延迟验证,比如下面代码中的 timeout = 2000 就表示在 2 秒后检查该方法是否被调用。
class GoodsPresenterTest {
private val mainThreadSurrogate = newSingleThreadContext("UI Thread")
private var presenter: GoodsPresenter? = null
@MockK
lateinit var view: GoodsContract.View
@Before
fun setUp() {
MockKAnnotations.init(this, relaxed = true)
presenter = GoodsPresenter()
presenter!!.attachView(view)
Dispatchers.setMain(mainThreadSurrogate)
}
@After
fun tearDown() {
Dispatchers.resetMain()
mainThreadSurrogate.close()
}
@Test
fun testBlockingTask() {
presenter!!.requestGoods(1)
verify(timeout = 2000) { view.hideLoading() }
}
}
添加依赖
// Unit tests
testImplementation "io.mockk:mockk:1.9.3"
// Instrumented tests
androidTestImplementation('io.mockk:mockk-android:1.9.3') { exclude module: 'objenesis' }
androidTestImplementation 'org.objenesis:objenesis:2.6'
// Coroutine tests
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-M2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.0-M2'
Top level functions
Function | Description |
---|---|
mockk(...) | builds a regular mock |
spyk() | builds a spy using the default constructor
|
spyk(obj) | builds a spy by copying from obj
|
slot | creates a capturing slot |
every | starts a stubbing block |
coEvery | starts a stubbing block for coroutines |
verify | starts a verification block |
coVerify | starts a verification block for coroutines |
verifyAll | starts a verification block that should include all calls |
coVerifyAll | starts a verification block that should include all calls for coroutines |
verifyOrder | starts a verification block that checks the order |
coVerifyOrder | starts a verification block that checks the order for coroutines |
verifySequence | starts a verification block that checks whether all calls were made in a specified sequence |
coVerifySequence | starts a verification block that checks whether all calls were made in a specified sequence for coroutines |
excludeRecords | exclude some calls from being recorded |
confirmVerified | confirms that all recorded calls were verified |
clearMocks | clears specified mocks |
registerInstanceFactory | allows you to redefine the way of instantiation for certain object |
mockkClass | builds a regular mock by passing the class as parameter |
mockkObject | makes an object an object mock or clears it if was already transformed |
unmockkObject | makes an object mock back to a regular object |
mockkStatic | makes a static mock out of a class or clears it if it was already transformed |
unmockkStatic | makes a static mock back to a regular class |
clearStaticMockk | clears a static mock |
mockkConstructor | makes a constructor mock out of a class or clears it if it was already transformed |
unmockkConstructor | makes a constructor mock back to a regular class |
clearConstructorMockk | clears the constructor mock |
unmockkAll | unmocks object, static and constructor mocks |
clearAllMocks | clears regular, object, static and constructor mocks |