【问题标题】:How to mock a Kotlin singleton object?如何模拟 Kotlin 单例对象?
【发布时间】:2016-10-24 22:39:00
【问题描述】:

给定一个 Kotlin 单例对象和一个有趣的方法

object SomeObject {
   fun someFun() {}
}

fun callerFun() {
   SomeObject.someFun()
}

有没有办法模拟呼叫SomeObject.someFun()?

【问题讨论】:

    标签: mocking mockito kotlin powermock powermockito


    【解决方案1】:

    有一个非常棒的 Kotlin 模拟库 - Mockk,它允许你模拟对象,就像你想要的一样。

    截至其文档:


    对象可以通过以下方式转换为模拟:

    object MockObj {
      fun add(a: Int, b: Int) = a + b
    }
    
    mockkObject(MockObj) // aplies mocking to an Object
    
    assertEquals(3, MockObj.add(1, 2))
    
    every { MockObj.add(1, 2) } returns 55
    
    assertEquals(55, MockObj.add(1, 2))
    

    要恢复使用 unmockkAll 或 unmockkObject:

    @Before
    fun beforeTests() {
        mockkObject(MockObj)
        every { MockObj.add(1,2) } returns 55
    }
    
    @Test
    fun willUseMockBehaviour() {
        assertEquals(55, MockObj.add(1,2))
    }
    
    @After
    fun afterTests() {
        unmockkAll()
        // or unmockkObject(MockObj)
    }
    

    尽管有 Kotlin 语言限制,但如果测试逻辑需要,您可以创建新的对象实例:

    val newObjectMock = mockk<MockObj>()
    

    【讨论】:

    • 另外,问题没有这么说,但在 android 仪器测试中,这不适用于低于 pie 的设备 api:mockk.io/ANDROID.html 因此 Ruslans 的答案使用起来更安全
    【解决方案2】:

    只需让您的对象实现一个接口,就可以使用任何模拟库来模拟您的对象。这里是 Junit + Mockito + Mockito-Kotlin 的例子:

    import com.nhaarman.mockito_kotlin.mock
    import com.nhaarman.mockito_kotlin.whenever
    import org.junit.Assert.assertEquals
    import org.junit.Test
    
    object SomeObject : SomeInterface {
        override fun someFun():String {
            return ""
        }
    }
    
    interface SomeInterface {
        fun someFun():String
    }
    
    class SampleTest {
    
        @Test
        fun test_with_mock() {
            val mock = mock<SomeInterface>()
    
            whenever(mock.someFun()).thenReturn("42")
    
            val answer = mock.someFun()
    
            assertEquals("42", answer)
        }
    }
    

    或者如果你想在 callerFun 中模拟 SomeObject:

    import com.nhaarman.mockito_kotlin.mock
    import com.nhaarman.mockito_kotlin.whenever
    import org.junit.Assert.assertEquals
    import org.junit.Test
    
    object SomeObject : SomeInterface {
        override fun someFun():String {
            return ""
        }
    }
    
    class Caller(val someInterface: SomeInterface) {
        fun callerFun():String {
            return "Test ${someInterface.someFun()}"
        }
    }
    
    // Example of use
    val test = Caller(SomeObject).callerFun()
    
    interface SomeInterface {
        fun someFun():String
    }
    
    class SampleTest {
    
        @Test
        fun test_with_mock() {
            val mock = mock<SomeInterface>()
            val caller = Caller(mock)
    
            whenever(mock.someFun()).thenReturn("42")
    
            val answer = caller.callerFun()
    
            assertEquals("Test 42", answer)
        }
    }
    

    【讨论】:

    • 这看起来像是一种反模式......不应该仅仅为了测试目的而在主代码中创建额外的类/接口!
    • 不是额外的类,只是接口。您应该这样做,因为这是在 JVM 上进行测试的最佳实践。
    • 通常 mock.someFun() 在被测试的类中调用,而不是在测试类中。由于您无法在实际中模拟该对象,因此它将无法正常工作。
    【解决方案3】:

    您可以通过使用类委托来模拟对象而无需任何额外的库。

    这是我的建议

    val someObjectDelegate : SomeInterface? = null
    
    object SomeObject: by someObjectDelegate ?: SomeObjectImpl
    
    object SomeObjectImpl : SomeInterface {
    
        fun someFun() {
            println("SomeObjectImpl someFun called")
        }
    }
    
    interface SomeInterface {
        fun someFun()
    }
    

    在您的测试中,您可以设置会改变行为的委托对象,否则它将使用它的真实实现。

    @Beofre
    fun setUp() {
      someObjectDelegate = object : SomeInterface {
          fun someFun() {
              println("Mocked function")
          }
      }
      // Will call method from your delegate
      SomeObject.someFun()
    }
    

    上面的名字当然不好,但为了举例说明目的。

    SomeObject 初始化后,委托将处理所有功能。
    更多内容请关注官方documentation

    【讨论】:

    • 理论上,当你这样做时,你还应该测试你的代表是否正常工作,并且为此编写测试也需要一个代表等等......使用模拟替代方案,我们相信t模拟库是自己测试的,我们不需要为这个概念添加额外的样板和额外的行
    • 肯定使用模拟库是更好的方法,但对于低于 android pie 的 android instrumentationtest 是不可能的。因此这个解决方案看起来不错!
    【解决方案4】:

    除了使用非常方便的Mockk 库之外,还可以简单地使用Mockito 和反射来模拟object。 Kotlin 对象只是具有私有构造函数和 INSTANCE 静态字段的常规 Java 类,通过反射可以将 INSTANCE 的值替换为模拟对象。测试后应恢复原件,以免更改影响其他测试

    使用 Mockito Kotlin(需要按照here 的描述添加扩展配置来模拟最终类):

    testCompile "com.nhaarman:mockito-kotlin:1.5.0"
    

    第一个乐趣可以替换object 类中的静态INSTANCE 字段的值并返回之前的值

    fun <T> replaceObjectInstance(clazz: Class<T>, newInstance: T): T {
    
        if (!clazz.declaredFields.any {
                    it.name == "INSTANCE" && it.type == clazz && Modifier.isStatic(it.modifiers)
                }) {
            throw InstantiationException("clazz ${clazz.canonicalName} does not have a static  " +
                    "INSTANCE field, is it really a Kotlin \"object\"?")
        }
    
        val instanceField = clazz.getDeclaredField("INSTANCE")
        val modifiersField = Field::class.java.getDeclaredField("modifiers")
        modifiersField.isAccessible = true
        modifiersField.setInt(instanceField, instanceField.modifiers and Modifier.FINAL.inv())
    
        instanceField.isAccessible = true
        val originalInstance = instanceField.get(null) as T
        instanceField.set(null, newInstance)
        return originalInstance
    }
    

    然后你可以创建一个 object 的模拟实例并用模拟的值替换原始值,返回原始值以便稍后重置

    fun <T> mockObject(clazz: Class<T>): T {
        val constructor = clazz.declaredConstructors.find { it.parameterCount == 0 }
                ?: throw InstantiationException("class ${clazz.canonicalName} has no empty constructor, " +
                        "is it really a Kotlin \"object\"?")
    
        constructor.isAccessible = true
    
        val mockedInstance = spy(constructor.newInstance() as T)
    
        return replaceObjectInstance(clazz, mockedInstance)
    }
    

    添加一些 Kotlin 糖

    class MockedScope<T : Any>(private val clazz: Class<T>) {
    
        fun test(block: () -> Unit) {
            val originalInstance = mockObject(clazz)
            block.invoke()
            replaceObjectInstance(clazz, originalInstance)
        }
    }
    
    fun <T : Any> withMockObject(clazz: Class<T>) = MockedScope(clazz)
    

    最后,给定一个object

    object Foo {
        fun bar(arg: String) = 0
    }
    

    你可以这样测试

    withMockObject(Foo.javaClass).test {
        doAnswer { 1 }.whenever(Foo).bar(any())
    
        Assert.assertEquals(1, Foo.bar(""))
    }
    
    Assert.assertEquals(0, Foo.bar(""))
    

    【讨论】:

    • 基本上,您依赖于 Kotlin 实现细节:只要 Kotile 设计人员决定更改 object 类的实现,您就应该更新所有测试。
    • 嗯,您不需要更改所有测试,只需更改模拟函数即可。但最重要的是,如果object 的实现发生了变化,您将需要重新编译旧代码。如果您今天编译Foo.bar(),编译结果也将使用INSTANCE 字段。
    • 这也适用于我!使用 mockito-kotlin 2.0.0-RC2,但尚未检查版本 2 是否提供更直接的内容。
    【解决方案5】:

    不操纵字节码答案是否定的,除非您愿意并且能够更改代码。模拟callerFun 对SomeObject.someFun() 的调用最直接的方法(也是我推荐的方法)是提供某种方法将其滑入模拟对象。

    例如

    object SomeObject {
        fun someFun() {}
    }
    
    fun callerFun() {
        _callerFun { SomeObject.someFun() }
    }
    
    internal inline fun _callerFun(caller: () -> Unit) {
        caller()
    }
    

    这里的想法是改变你愿意改变的东西。如果您确定需要一个单例和一个作用于该单例的顶级函数,那么如上所示,在不更改其公共签名的情况下使顶级函数可测试的一种方法是将其实现移动到 @987654324 @ 允许滑动模拟的函数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-08-22
      • 1970-01-01
      • 1970-01-01
      • 2019-01-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多