# 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 的数量比较多时,写出来的代码看上去会比较别扭,比如下面这样的。 ```java @Test fun testAdd() { `when`(calculator!!.add(1, 1)).thenReturn(2) assertEquals(calculator!!.add(1, 1), 2) } ``` [`MockK` 是一个用 Kotlin 写的 Mocking 框架](https://mockk.io/),它解决了所有上述提到的 Mockito 中存在的问题。 ## MockK ### 快速入门 使用 MockK 测试 Calculator ```kotlin @Test fun testAdd() { // 每一次 add(1, 1) 被调用,都返回 2 // 相当于是 Mockito 中的 when(…).thenReturns(…) every { calculator.add(1, 1) } returns 2 assertEquals(calculator.add(1, 1), 2) } ``` ```kotlin 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 为这个**没有返回值的方法分配一个默认行为**。 ```kotlin @Test fun testGetGoods() { val goods = presenter!!.getGoods(1) every { view.showLoading() } just Runs verify { view.showLoading() } assertEquals(goods.name, "纸巾") } ``` ### 为所有模拟对象的方法分配默认行为 如果测试中有多个模拟对象,且你想为它们的`全部方法`都分配默认行为,那你可以在初始化 MockK 的时候指定 relaxed 为 true,比如下面这样。 ```kotlin @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) } ``` 使用这种方式我们就`不需要`使用 `@RelaxedMockK` 注解了,直接使用 `@MockK` 注解即可。 ### 验证多个方法被调用 在 GoodsPresenter 的 getGoods() 方法中调用了 View 的 showLoading() 和 hideLoading() 方法,如果我们想验证这两个方法执行了的话,我们可以把两个方法都放在 verify {…} 中进行验证。 ```kotlin @Test fun testGetGoods() { val goods = presenter!!.getGoods(1) verify { view.hideLoading() view.showLoading() } assertEquals(goods.name, "纸巾") } ``` ### 验证方法被调用的次数 如果你不仅想验证方法被调用,而且想验证该方法被`调用的次数`,你可以在 `verify` 中指定 `exatcly`、`atLeast` 和 `atMost` 属性,比如下面这样的。 ```kotlin @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 方法指的是,我们当前调用的方法中,调用了的模拟对象的方法。 ```kotlin @Test fun testGetGoods() { val goods = presenter!!.getGoods(1) verifyAll { view.showToast("请耐心等待") view.showToast("请耐心等待") view.showLoading() view.hideLoading() } assertEquals(goods.name, "纸巾") } ``` ### 验证 Mock 方法的调用顺序 ```kotlin @Test fun testGetGoods() { val goods = presenter!!.getGoods(1) verifyOrder { view.showLoading() view.hideLoading() } assertEquals(goods.name, "纸巾") } ``` ### 验证全部的 Mock 方法都按特定顺序被调用了 如果你不仅想测试好几个方法被调用了,而且想确保它们是按`固定顺序`被调用的,你可以使用 `verifySequence {…}` ,比如下面这样的。 ```kotlin @Test fun testGetGoods() { val goods = presenter!!.getGoods(1) verifySequence { view.showLoading() view.showToast("请耐心等待") view.showToast("请耐心等待") view.hideLoading() } assertEquals(goods.name, "纸巾") } ``` ### 确认所有 Mock 方法都进行了验证 把我们的模拟对象传入 `confirmVerified()` 方法中,就可以确认**是否验证了模拟对象的每一个方法**。 ```kotlin @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)` 进行验证,比如下面这样的。 ```kotlin @Test fun testCaptureSlot() { val slot = slot() every { view.showToast(capture(slot)) } returns Unit val goods = presenter!!.getGoods(1) assertEquals(slot.captured, "请耐心等待") } ``` ### 验证 Mock 方法每一次被调用接收到参数 如果一个方法被调用了多次,可以使用 `capture(mutableList)` 将每一次被调用时获取到的参数记录下来, 并在后面进行验证,比如下面这样。 ```kotlin @Test fun testCaptureList() { val list = mutableListOf() 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 秒后检查该方法是否被调用。 ```kotlin 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() } } } ``` ## 添加依赖 ```kotlin // 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|