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.
Mockito @Mock AnnotationWe can mock an object using @Mock annotation too. It's useful when we want to use the mocked object at multiple places because we avoid calling mock() method multiple times. The code becomes more readable and we can specify mock object name that will be useful in case of errors.
Mockito has been around since the early days of Android development and eventually became the de-facto mocking library for writing unit tests. Mockito and Mockk are written in Java and Kotlin, respectively, and since Kotlin and Java are interoperable, they can exist within the same project.
verify supports the same argument matchers as every , along with a few additional matchers. Inside the verification block (between the opening curly bracket { and closing curly bracket } ), you write the method you want to verify.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With