How to mock a Kotlin singleton object?

38,014

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

Solution 2

There's a very nice mocking library for Kotlin - Mockk, which allows you to mock objects, the exact same way you're desiring.

As of its documentation:


Objects can be transformed to mocks following way:

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

To revert back use unmockkAll or 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)
}

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

val newObjectMock = mockk<MockObj>()

Solution 3

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

Solution 4

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

Solution 5

For Mockito, we can use Mockito.mockStatic()

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

Ouside use's scope, the value of callAFunction() is reset

Share:
38,014

Related videos on Youtube

user3284037
Author by

user3284037

Updated on July 09, 2022

Comments

  • user3284037
    user3284037 almost 2 years

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

  • LeoColman
    LeoColman over 6 years
    This looks like an anti-pattern... One shouldn't create extra classes/interfaces in main code just for the purpose of testing!
  • Ruslan
    Ruslan over 6 years
    Not extra classes, just interfaces. And you should do it because this is best practice for testing on JVM.
  • azizbekian
    azizbekian about 6 years
    Basically, you are relying on Kotlin implementation details: anytime Kotile designers decide to change the implementation of object classes then you should update all your tests.
  • lelloman
    lelloman about 6 years
    Well, you wouldn't need to change all your tests, only the mocking function. But most importantly, if the implementation of object changed, you would need to recompile older code. If you compile Foo.bar() today, the compiled outcome would use an INSTANCE field as well.
  • Ch Vas
    Ch Vas over 5 years
    That also works for me! Using mockito-kotlin 2.0.0-RC2, haven't checked if version 2 provides something more straight forward though.
  • LeoColman
    LeoColman over 4 years
    Theoretically, when you do this you should also test that your delegates work correctly, and writing a test for that will also require a delegate and so on... Using a mocked alternative, we trust that t he mock library is tested itself, and we don't need to add extra boilerplate and extra lines for this concept
  • Hatzen
    Hatzen about 4 years
    Also so question didnt say so but in android instrumentation test this wont work with devices api lower than pie: mockk.io/ANDROID.html therefore Ruslans answer is more safe to use
  • Hatzen
    Hatzen about 4 years
    For sure using a mock lib is the better approach but not possible for android instrumentationtest lower than android pie. Therefor this solution looks pretty good!
  • Serdar Samancıoğlu
    Serdar Samancıoğlu over 2 years
    Usually mock.someFun() is called in the class that is being tested, not in test class. Since you can't mock the object in the actual it won't work.
  • Bruce
    Bruce about 2 years
    this is pretty neat without using any library