How to mock objects in Kotlin?

aloj picture aloj · Mar 13, 2018 · Viewed 11.1k times · Source

I want to test a class that calls an object (static method call in java) but I'm not able to mock this object to avoid real method to be executed.

object Foo {
    fun bar() {
        //Calls third party sdk here
    }
}

I've tried different options like Mockk, How to mock a Kotlin singleton object? and using PowerMock in the same way as in java but with no success.

Code using PowerMockito:

@RunWith(PowerMockRunner::class)
@PrepareForTest(IntentGenerator::class)
class EditProfilePresenterTest {

    @Test
    fun shouldCallIntentGenerator() {

        val intent = mock(Intent::class.java)

        PowerMockito.mockStatic(IntentGenerator::class.java)
        PowerMockito.`when`(IntentGenerator.newIntent(any())).thenReturn(intent) //newIntent method param is context

       presenter.onGoToProfile()

       verify(view).startActivity(eq(intent))        

    }
}

With this code I get

java.lang.IllegalArgumentException: Parameter specified as non-null is null: method com.sample.test.IntentGenerator$Companion.newIntent, parameter context

any() method is from mockito_kotlin. Then If I pass a mocked context to newIntent method, it seems real method is called.

Answer

lelloman picture lelloman · Mar 15, 2018

First, that object IntentGenerator looks like a code smell, why would you make it an object? If it's not your code you could easily create a wrapper class

class IntentGeneratorWrapper {

    fun newIntent(context: Context) = IntentGenerator.newIntent(context)    

}

And use that one in your code, without static dependencies.

That being said, I have 2 solutions. Say you have an object

object IntentGenerator {
    fun newIntent(context: Context) = Intent()
}

Solution 1 - Mockk

With Mockk library the syntax is a bit funny compared to Mockito but, hey, it works:

testCompile "io.mockk:mockk:1.7.10"
testCompile "com.nhaarman:mockito-kotlin:1.5.0"

Then in your test you use objectMockk fun with your object as argument and that will return a scope on which you call use, within use body you can mock the object:

@Test
fun testWithMockk() {
    val intent: Intent = mock()
    whenever(intent.action).thenReturn("meow")

    objectMockk(IntentGenerator).use {
        every { IntentGenerator.newIntent(any()) } returns intent
        Assert.assertEquals("meow", IntentGenerator.newIntent(mock()).action)
    }
}

Solution 2 - Mockito + reflection

In your test resources folder create a mockito-extensions folder (e.g. if you're module is "app" -> app/src/test/resources/mockito-extensions) and in it a file named org.mockito.plugins.MockMaker. In the file just write this one line mock-maker-inline. Now you can mock final classes and methods (both IntentGenerator class and newIntent method are final).

Then you need to

  1. Create an instance of IntentGenerator. Mind that IntentGenerator is just a regular java class, I invite you to check it with Kotlin bytecode window in Android Studio
  2. Create a spy object with Mockito on that instance and mock the method
  3. Remove the final modifier from INSTANCE field. When you declare an object in Kotlin what is happening is that a class (IntentGenerator in this case) is created with a private constructor and a static INSTANCE method. That is, a singleton.
  4. Replace IntentGenerator.INSTANCE value with your own mocked instance.

The full method would look like this:

@Test
fun testWithReflection() {
    val intent: Intent = mock()
    whenever(intent.action).thenReturn("meow")

    // instantiate IntentGenerator
    val constructor = IntentGenerator::class.java.declaredConstructors[0]
    constructor.isAccessible = true
    val intentGeneratorInstance = constructor.newInstance() as IntentGenerator

    // mock the the method
    val mockedInstance = spy(intentGeneratorInstance)
    doAnswer { intent }.`when`(mockedInstance).newIntent(any())

    // remove the final modifier from INSTANCE field
    val instanceField = IntentGenerator::class.java.getDeclaredField("INSTANCE")
    val modifiersField = Field::class.java.getDeclaredField("modifiers")
    modifiersField.isAccessible = true
    modifiersField.setInt(instanceField, instanceField.modifiers and Modifier.FINAL.inv())

    // set your own mocked IntentGenerator instance to the static INSTANCE field
    instanceField.isAccessible = true
    instanceField.set(null, mockedInstance)

    // and BAM, now IntentGenerator.newIntent() is mocked
    Assert.assertEquals("meow", IntentGenerator.newIntent(mock()).action)
}

The problem is that after you mocked the object, the mocked instance will stay there and other tests might be affected. A made a sample on how to confine the mocking into a scope here

Why PowerMock is not working

You're getting

Parameter specified as non-null is null

because IntentGenerator is not being mocked, therefore the method newIntent that is being called is the actual one and in Kotlin a method with non-null arguments will invoke kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull at the beginning of your method. You can check it with the bytecode viewer in Android Studio. If you changed your code to

PowerMockito.mockStatic(IntentGenerator::class.java)
PowerMockito.doAnswer { intent }.`when`(IntentGenerator).newIntent(any())

You would get another error

org.mockito.exceptions.misusing.NotAMockException: Argument passed to when() is not a mock!

If the object was mocked, the newInstance method called would not be the one from the actual class and therefore a null could be passed as argument even if in the signature it is non-nullable