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.
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
IntentGenerator
. Mind that IntentGenerator
is just a regular java class, I invite you to check it with Kotlin bytecode
window in Android StudioINSTANCE
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.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