8

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.

aloj
  • 2,926
  • 5
  • 24
  • 35

3 Answers3

10

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

lelloman
  • 12,742
  • 5
  • 55
  • 75
  • 1
    Working with Mockk! Thanks! – aloj Mar 19 '18 at 17:11
  • @aloj you're welcome :) The second option was just for fun actually, I wouldn't use it in a commercial project – lelloman Mar 19 '18 at 18:38
  • @lelloman you wrote that using `object IntentGenerator` is a code smell. Do you have any good sources on this subject? I am often struggle when to use `object` and when not to use it (apart from testability and stateful objects). – user1185087 Feb 13 '20 at 09:16
  • I would suggest uncle Bob's talks about clean code. My point there is not really against `object` itself, but the fact that you end up with a static dependency inside your class. – lelloman Feb 13 '20 at 10:14
0

Check your any(), it returns null, and this is the cause of your exception.

To debug, replace any() with any().also(::println) and see the command line output.
If any().also(::println) fails please use any<MockContext>().also(::println).

I've just read the implementation of the mock framework. any has a type parameter which has implicitly inferred. From your error message, I guess the type of newIntent's parameter is Context, which is an abstract class, so of course you can't create an instance of it.

If my guess is true, replace any() with any<Activity>() will probably solve this.
And whether my guess is true or not, please read the StackOverflow help center and "git gud scrub" about how to ask programming questions. The information you've provided is extremely unhelpful for solving your question.

ice1000
  • 5,559
  • 4
  • 28
  • 68
  • maybe add a solution as well? – Lovis Mar 14 '18 at 09:15
  • I don't know anything about the implementation of his `any()`, so no idea.. – ice1000 Mar 14 '18 at 09:15
  • This is any() implementation https://github.com/nhaarman/mockito-kotlin/blob/3566e9121de1b0dceb2fa0b783447773327c3314/mockito-kotlin/src/main/kotlin/com/nhaarman/mockito_kotlin/Mockito.kt, I've also tried with fun safeAny(): T = Mockito.any() with same result – aloj Mar 14 '18 at 09:21
  • @aloj you sure that it's `any` from mockito-kotlin, and not accidently from Mockito itself? Check your imports, please – Lovis Mar 14 '18 at 09:23
  • Could you please replace `any()` with `any().also(::println)` and tell us the command line output. – ice1000 Mar 14 '18 at 09:26
  • I added and I think I can see this in the error message: You cannot use argument matchers outside of verification or stubbing. Examples of correct usage of argument matchers: when(mock.get(anyInt())).thenReturn(null); doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject()); verify(mock).someMethod(contains("foo")) – aloj Mar 14 '18 at 10:22
  • This message may appear after an NullPointerException if the last matcher is returning an object like any() but the stubbed method signature expect a primitive argument, in this case, use primitive alternatives. when(mock.get(any())); // bad use, will raise NPE when(mock.get(anyInt())); // correct usage use Also, this error might show up because you use argument matchers with methods that cannot be mocked. Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode(). Mocking methods declared on non-public parent classes is not supported. – aloj Mar 14 '18 at 10:22
  • com.mobilendo.planes.ui.profile.edit.EditProfilePresenterTest > shouldCallIntentGenerator FAILED java.lang.IllegalArgumentException at EditProfilePresenterTest.kt:131 com.mobilendo.planes.ui.profile.edit.EditProfilePresenterTest > Test mechanism.classMethod FAILED org.mockito.exceptions.misusing.InvalidUseOfMatchersException – aloj Mar 14 '18 at 10:31
  • I can't figure out which words are your description and which are command line output – ice1000 Mar 14 '18 at 11:47
  • If `any().also(::println)` fails please use `any().also(::println)` – ice1000 Mar 14 '18 at 11:52
  • Please @ice1000 find it here https://pastebin.com/qy641Gzw, I don't know If you mean exactly this by command line output – aloj Mar 14 '18 at 13:29
  • @ice1000 the question seems pretty well asked to me, the problem is clear, it shows the some effort to solve it and it has value for other people who might run into the same problem – lelloman Mar 15 '18 at 22:45
  • @lelloman it's because the asker has edited the question multiple times, under the commenters' instructions – ice1000 Mar 16 '18 at 03:04
-2

This blog post clearly demonstrates how to do this using mockito. I highly recommend this approach. It's really simple.

Basically, in your src/test/resources folder create a folder: mockito-extensions. Add a file in that directory called org.mockito.plugins.MockMaker, and inside that file have the following text:

mock-maker-inline

You can now mock final kotlin classes and methods.

spierce7
  • 12,064
  • 11
  • 56
  • 91
  • I already did that and can mock classes, problem is mocking objects – aloj Mar 14 '18 at 08:34
  • Oh. It works for Robolectric. I don't know how to mock objects. You'd have to create a mock instance of the object class, and then use reflection to swap out it's value on the `INSTANCE` variable where the object is stored. – spierce7 Mar 14 '18 at 16:05