mockito / mockito-kotlin

Using Mockito with Kotlin
MIT License
3.11k stars 202 forks source link

Mocking final field of final class #368

Closed karlkar closed 4 years ago

karlkar commented 4 years ago

Hello, I'm trying to use mockito-kotlin 2.2.0 in order to mock a final field in a final class of Crashlytics.

class CrashlyticsCrashReportingServiceTest {

    private val crashlyticsCore: CrashlyticsCore = mock()
    private val crashlytics: Crashlytics = mock {
        on { core } doReturn crashlyticsCore
    }

    private val tested: CrashlyticsCrashReportingService =
        CrashlyticsCrashReportingService(crashlytics)

    @Test
    fun `should log exception to crashlytics when requested`() {
        // given
        val exception: IOException = mock()

        // when
        tested.reportCrash(exception)

        // then
        verify(crashlyticsCore).logException(exception)
    }

    @Test
    fun `given message is provided should log message and exception when requested`() {
        // given
        val message = "Message"
        val exception: IOException = mock()

        // when
        tested.reportCrash(exception, message)

        // then
        verify(crashlyticsCore).log(message)
        verify(crashlyticsCore).logException(exception)
    }
}

class CrashlyticsCrashReportingService(
    private val crashlyticsInstance: Crashlytics
): CrashReportingService {

    override fun reportCrash(throwable: Throwable, message: String?) {
        message?.let {
            crashlyticsInstance.core.log(message)
        }
        crashlyticsInstance.core.logException(throwable)
    }
}

I have added src/test/resources/mockito-extensions.org.mockito.plugins.MockMaker file with "mock-maker-inline" text inside of it. However I'm still getting

org.mockito.exceptions.misusing.MissingMethodInvocationException: 
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
   Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.

    at com.nhaarman.mockitokotlin2.KStubbing.on(KStubbing.kt:70)
    at com.example.base.crash.CrashlyticsCrashReportingServiceTest.<init>(CrashlyticsCrashReportingServiceTest.kt:15)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at org.junit.runners.BlockJUnit4ClassRunner.createTest(BlockJUnit4ClassRunner.java:217)
    at org.junit.runners.BlockJUnit4ClassRunner$1.runReflectiveCall(BlockJUnit4ClassRunner.java:266)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.BlockJUnit4ClassRunner.methodBlock(BlockJUnit4ClassRunner.java:263)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runners.Suite.runChild(Suite.java:128)
    at org.junit.runners.Suite.runChild(Suite.java:27)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

How can I fix that?

bohsen commented 4 years ago

@karlkar Are you using Mockito-inline or AllOpen plugin? If not you won't be able to mock it.

karlkar commented 4 years ago

I'm not using AllOpen plugin and don't really want to go this way. In case of Mockito-inline - I've added mock-maker-inline file. Isn't that enough?

bohsen commented 4 years ago

Should be enough, but sometimes people setup the file wrongly. Maybe try the relatively new testImplementation "org.mockito:mockito-inline:$mockito_version" gradle dependency. This can be used instead of using the inline-mock-maker file.

karlkar commented 4 years ago

I've added testImplementation "org.mockito:mockito-inline:2.28.2" didn't help - doesn't matter if I have MockMaker file created or not.

bohsen commented 4 years ago

@karlkar Just realized these are instrumentation tests that are executed as such. Inline-mock-maker won't work here.

Inline-mock-maker will only work for tests on the JVM as it targets java bytecode. Instrumentation tests are meant to be executed in the Android environment using dalvik bytecode hence the issue.

Only solution to this is linkedins dexmaker I believe. But haven't tried it.

karlkar commented 4 years ago

No, those are unit tests. And in fact I've added a third test case to my set:


    class FinalClass {
        val finalField: String = "something"
    }

    @Test
    fun test() {
        val concrete = FinalClass()

        val mocky = mock(FinalClass::class.java)
        given(mocky.finalField).willReturn("not anymore");

        val res = mocky.finalField
        assert(res != concrete.finalField)
    }

and it works fine ^^

bohsen commented 4 years ago

Ohh okay. Are you using Robolectric?

What if you move the stubbing inside the test as in the above instead of using on { core } doReturn crashlyticsCore?

karlkar commented 4 years ago

Ok, to be very precise - currently I have MockMaker file created in proper directory (checked 100 times). in build.gradle I've added

    testImplementation 'junit:junit:4.12'
    testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
    testImplementation 'org.mockito:mockito-inline:2.28.2'

so MockMaker file should not be necessary, but let it stay there. Now if I use such code:

import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.given
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import org.junit.Test
import java.io.IOException

class CrashlyticsCrashReportingServiceTest {

    private val crashlyticsCore: CrashlyticsCore = mock()
    private val crashlytics: Crashlytics = mock {
        on { core } doReturn crashlyticsCore
    }

    private val tested: CrashlyticsCrashReportingService =
        CrashlyticsCrashReportingService(crashlytics)

    @Test
    fun `should log exception to crashlytics when requested`() {
        // given
        val exception: IOException = mock()
        given(crashlytics.core).willReturn(crashlyticsCore)

        // when
        tested.reportCrash(exception)

        // then
        verify(crashlyticsCore).logException(exception)
    }

    @Test
    fun `given message is provided should log message when logging exception when requested`() {
        // given
        val message = "Message"
        val exception: IOException = mock()
        given(crashlytics.core).willReturn(crashlyticsCore)

        // when
        tested.reportCrash(exception, message)

        // then
        verify(crashlyticsCore).log(message)
        verify(crashlyticsCore).logException(exception)
    }

    class FinalClass {
        val finalField: String = "something"
    }

    @Test
    fun test() {
        val concrete = FinalClass()

        val mocky: FinalClass = mock()
        given(mocky.finalField).willReturn("not anymore")

        val res = mocky.finalField
        assert(res != concrete.finalField)
    }
}

all 3 tests are failing.

But when I switch to using Mockito:

import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore
import org.junit.Assert.assertThat
import org.junit.Ignore
import org.junit.Test
import org.mockito.BDDMockito.given
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import java.io.IOException

class CrashlyticsCrashReportingServiceTest {

    private val crashlyticsCore: CrashlyticsCore = mock(CrashlyticsCore::class.java)
    private val crashlytics: Crashlytics = mock(Crashlytics::class.java)

    private val tested: CrashlyticsCrashReportingService =
        CrashlyticsCrashReportingService(crashlytics)

    @Test
    fun `should log exception to crashlytics when requested`() {
        // given
        val exception: IOException = mock(IOException::class.java)
        Mockito.`when`(crashlytics.core).thenReturn(crashlyticsCore)

        // when
        tested.reportCrash(exception)

        // then
        verify(crashlyticsCore).logException(exception)
    }

    @Test
    fun `given message is provided should log message when logging exception when requested`() {
        // given
        val message = "Message"
        val exception: IOException = mock(IOException::class.java)
        Mockito.`when`(crashlytics.core).thenReturn(crashlyticsCore)

        // when
        tested.reportCrash(exception, message)

        // then
        verify(crashlyticsCore).log(message)
        verify(crashlyticsCore).logException(exception)
    }

    class FinalClass {
        val finalField: String = "something"
    }

    @Test
    fun test() {
        val concrete = FinalClass()

        val mocky = mock(FinalClass::class.java)
        given(mocky.finalField).willReturn("not anymore");

        val res = mocky.finalField
        assert(res != concrete.finalField)
    }
}

the third test passes.

I am not using Robolectric.

karlkar commented 4 years ago

When I've removed the on {} part from the initailziers 3rd test has passed also for the first case. The rest stays red.

bohsen commented 4 years ago

The problem comes from this I believe:

  1. inside when() you don't call method on mock but on some other object.

Try using the following inside the test (not sure it'll work though):

doReturn(crashlyticsCore).whenever(crashlytics.core)
karlkar commented 4 years ago

It didn't help :( I went further - I've created 2 classes to simulate Crashlytics. One in Java (just like Crashlytics) and one in kotlin:

public class PseudoCrashlytics {

    public final PseudoCrashlyticsCore core;

    public PseudoCrashlytics() {
        core = new PseudoCrashlyticsCore();
    }

    public class PseudoCrashlyticsCore {
        public void log(String message) {}
        public void logException(Throwable ex) {}
    }
}
class PseudoCrashlytics2 {

    val core: PseudoCrashlyticsCore = PseudoCrashlyticsCore()

    inner class PseudoCrashlyticsCore {
        fun log(m: String){}
        fun logException(m: Throwable){}
    }
}

In case of kotlin's version all tests are green. In case of Java version - tests are red (besides the last one with the FinalClass of course).

bohsen commented 4 years ago

Ahh yes java. Forgot about that. Add a getter for the final field to the java version and you should get the same output as the kotlin one (you could even add final modifier to the getter).

bohsen commented 4 years ago

Mocking operates on methods and interfaces, not fields

karlkar commented 4 years ago

That's a 3rd party class :/ Ok, I'll have to create some kind of a wrapper for it. Thanks for your help!