README.md

    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 中指定 exatclyatLeastatMost 属性,比如下面这样的。

    @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

    项目简介

    🎭mockk 模拟单元测试中需要用到的对象和方法,MockK 是一个用 Kotlin 写的 Mocking 框架

    发行版本

    当前项目没有发行版本

    贡献者 1

    开发语言