如何模拟Kotlin单例对象?

use*_*037 24 mocking mockito powermock kotlin powermockito

给出一个Kotlin单例对象和一个称之为方法的乐趣

object SomeObject {
   fun someFun() {}
}

fun callerFun() {
   SomeObject.someFun()
}
Run Code Online (Sandbox Code Playgroud)

有没有办法模拟电话SomeObject.someFun()

Ker*_*ker 21

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))
Run Code Online (Sandbox Code Playgroud)

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

@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)
}
Run Code Online (Sandbox Code Playgroud)


Rus*_*lan 12

只需让对象实现一个接口,就可以使用任何模拟库来模拟对象.这里有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)
    }
}
Run Code Online (Sandbox Code Playgroud)

或者如果你想要模拟SomeObject内部callerFun:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这看起来像一个反模式......不应仅仅为了测试而在主代码中创建额外的类/接口! (13认同)
  • 不是额外的类,只是接口.你应该这样做,因为这是在JVM上进行测试的最佳实践. (3认同)

lel*_*man 9

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

使用 Mockito Kotlin(需要添加一个扩展配置,如here所述来模拟最终类):

testCompile "com.nhaarman:mockito-kotlin:1.5.0"
Run Code Online (Sandbox Code Playgroud)

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

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
}
Run Code Online (Sandbox Code Playgroud)

然后你可以玩得开心,创建一个模拟实例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)
}
Run Code Online (Sandbox Code Playgroud)

添加一些 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)
Run Code Online (Sandbox Code Playgroud)

最后,给定一个 object

object Foo {
    fun bar(arg: String) = 0
}
Run Code Online (Sandbox Code Playgroud)

你可以这样测试

withMockObject(Foo.javaClass).test {
    doAnswer { 1 }.whenever(Foo).bar(any())

    Assert.assertEquals(1, Foo.bar(""))
}

Assert.assertEquals(0, Foo.bar(""))
Run Code Online (Sandbox Code Playgroud)

  • 好吧,您不需要更改_所有测试_,只需更改模拟功能。但最重要的是,如果 `object` 的实现发生了变化,您将需要重新编译旧代码。如果你今天编译 `Foo.bar()`,编译结果也会使用一个 `INSTANCE` 字段。 (2认同)

Ioa*_*dze 7

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

这是我的建议

val someObjectDelegate : SomeInterface? = null

object SomeObject: by someObjectDelegate ?: SomeObjectImpl

object SomeObjectImpl : SomeInterface {

    fun someFun() {
        println("SomeObjectImpl someFun called")
    }
}

interface SomeInterface {
    fun someFun()
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

在SomeObject初始化之后,委托将处理所有功能。
您可以在官方文档中找到更多信息