How to mock a Kotlin singleton object?

匿名 (未验证) 提交于 2019-12-03 00:46:02

问题:

Given a Kotlin singleton object and a fun that call it's method

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

Is there a way to mock call to SomeObject.someFun()?

回答1:

Just make you object implement an interface, than you can mock you object with any mocking library. Here example of 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)     } }

Or in case if you want mock SomeObject inside 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)     } }


回答2:

There's a very nice new mocking library for Kotlin. It's called Mockk.

It was updated today to version 1.7, and it allows you to mock objects, the exact same way you're desiring.

As of it's documentation:


Objects can be transformed to mocks following way:

object MockObj {   fun add(a: Int, b: Int) = a + b }  objectMockk(MockObj).use {   assertEquals(3, MockObj.add(1, 2))    every { MockObj.add(1, 2) } returns 55    assertEquals(55, MockObj.add(1, 2)) }

Despite Kotlin language limits you can create new instances of objects if testing logic needs that:

val newObjectMock = mockk<MockObj>()


回答3:

You can mock Object without any extra library, by using class delegates.

Here is my proposal

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

In your tests you can set delegate object that will change behaviour, otherwise it will use it's real implementation.

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

Of course names above are bad, but for the sake of an example it shows the purpose.

After SomeObject is initialised delegate will handle all the functions.
More you can find in official documentation



回答4:

Short of manipulating byte code the answer is no, unless you are willing and able to change the code. The most straightforward way (and the way I would recommend) to mock callerFun's call to SomeObject.someFun() is to provide some way to slip it a mock object.

e.g.

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

The idea here is to change something you're willing to change. If you're certain you want a singleton and a top-level function that acts on that singleton then one way, as demonstrated above, to make the top-level function testable without changing its public signature is to move its implementation to an internal function that allows slipping a mock.



回答5:

Besides using Mockk library, which is quite convenient, one could mock an object simply with Mockito and reflection. A Kotlin object is just a regular Java class with a private constructor and an INSTANCE static field, with reflection one can replace the value of INSTANCE with a mocked object. After the test the original should be restored so that the change won't affect other tests

Using Mockito Kotlin (one needs to add an extension configuration as described here to mock final classes):

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

A first fun could replace the value of the static INSTANCE field in the object class and return the previous value

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 }

Then you could have a fun that will create a mock instance of the object and replace the original value with the mocked one, returning the original so that it can be reset later

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

Add some Kotlin sugar

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)

And finally, given an object

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

You could test it this way

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


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!