README.md 10.3 KB
Newer Older
颯沓如流星's avatar
颯沓如流星 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# 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)
}
```


颯沓如流星's avatar
颯沓如流星 已提交
25 26
[`MockK` 是一个用 Kotlin 写的 Mocking 框架](https://mockk.io/),它解决了所有上述提到的 Mockito 中存在的问题。

颯沓如流星's avatar
颯沓如流星 已提交
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256

## 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<String>()
    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<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 秒后检查该方法是否被调用。

```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() }
    }

}
```

颯沓如流星's avatar
颯沓如流星 已提交
257
## 添加依赖
颯沓如流星's avatar
颯沓如流星 已提交
258 259 260 261 262 263 264 265 266 267 268
```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'
颯沓如流星's avatar
颯沓如流星 已提交
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
```

## Top level functions

|Function|Description|
|--|--|
|mockk<T>(...)	|builds a regular mock|
|spyk<T>()	|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|