如何模拟Kotlin Singleton对象



给定一个kotlin singleton对象和一个称为方法的乐趣

object SomeObject {
   fun someFun() {}
}
fun callerFun() {
   SomeObject.someFun()
}

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

有一个非常不错的模拟库-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>()

只是让您对象实现接口,而不是用任何模拟库模拟对象。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)
    }
}

您可以使用class demegates 模拟无需任何额外库的对象。

这是我的提议

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

当然上面的名称不好,但是为了一个示例,它显示了目的。

初始化某些对象后,委托将处理所有功能。
您可以在官方文档中找到更多

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

使用Mockito Kotlin(需要添加如下所述的扩展配置以模拟最终类):

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(""))

对于Mockito,我们可以使用Mockito.mockStatic()

Mockito.mockStatic(SomeObject::class.java).use { mocked ->
    mocked.`when`<SomeType> { SomeObject.callAFunction() }
        .thenReturn(someMockedValue)
    
    // Your test goes here
}

Ouside use的范围,callAFunction()的值是重置

操纵字节代码的缺乏,除非您愿意并且能够更改代码,否则答案是否定的。模拟callerFun呼叫SomeObject.someFun()的最直接方式(以及我建议的方式)是提供某种方式将其滑入模拟对象。

,例如

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

这里的想法是改变您愿意更改的东西。如果您确定自己想要单身和顶级功能,该功能在该单身顿上作用于上面的单词,那么在不更改其公共签名的情况下可以进行顶级函数,这是将其实现移至internal功能这允许滑动模拟。

最新更新