Skip to content

Instantly share code, notes, and snippets.

@xa17d

xa17d/article.md Secret

Created April 12, 2020 21:05
Show Gist options
  • Save xa17d/a48692a3d17a7acd3d6666f4120027d6 to your computer and use it in GitHub Desktop.
Save xa17d/a48692a3d17a7acd3d6666f4120027d6 to your computer and use it in GitHub Desktop.

How to use Mockito and Mockk both in the same instrumentation test

tl;dr: If you want to use both Mockito and Mockk in the same instrumentation test, use this configuration:

androidTestImplementation("io.mockk:mockk-android:1.9.3") {
    exclude module: "objenesis"
}
androidTestImplementation "org.objenesis:objenesis:2.6" // because of https://github.com/mockk/mockk/issues/281

androidTestImplementation("org.mockito:mockito-android:3.3.3") {
    exclude group: "net.bytebuddy", module: "byte-buddy-android"
}
androidTestImplementation "net.bytebuddy:byte-buddy-android:1.10.9"

Background

We at mySugr have a long history in using Mockito in our tests to create mocks. Since we're using Kotlin coroutines more and more, we needed another mocking framework that has first class support for coroutines and other Kotlin features. We chose mockk. Migrating all existing tests to mockk is not really an option. There are just to many of them. And there is not really a value added in doing so. So we decided to use mockk and mockito in parallel. We use mockk for new tests and keep mockito for existing tests. This works well for unit tests, but for instrumentations tests on Android we got strange exceptions like:

org.mockito.exceptions.base.MockitoException:
Mockito cannot mock this class: class com.mysugr.experiment.mockk.MyClassOpen.

Mockito can only mock non-private & non-final classes.
If you're not sure why you're getting this error, please report to the mailing list.



IMPORTANT INFORMATION FOR ANDROID USERS:

The regular Byte Buddy mock makers cannot generate code on an Android VM!
To resolve this, please use the 'mockito-android' dependency for your application:
http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22mockito-android%22%20g%3A%22org.mockito%22

Java               : 0.9
JVM vendor name    : The Android Project
JVM vendor version : 2.1.0
JVM name           : Dalvik
JVM version        : 0.9
JVM info           : null
OS name            : Linux
OS version         : 3.18.91+


Underlying exception : java.lang.IllegalArgumentException: Could not create type
at com.mysugr.experiment.mockk.AndroidTest.mockito(AndroidTest.kt:60)
at java.lang.reflect.Method.invoke(Native Method)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at com.mysugr.testing.coroutine.TestCoroutineScopeRule$apply$1.evaluate(TestCoroutineScopeRule.kt:32)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
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 androidx.test.ext.junit.runners.AndroidJUnit4.run(AndroidJUnit4.java:104)
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 org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:392)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2075)
Caused by: java.lang.IllegalArgumentException: Could not create type
at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:154)
at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:365)
at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:174)
at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:376)
at org.mockito.internal.creation.bytebuddy.TypeCachingBytecodeGenerator.mockClass(TypeCachingBytecodeGenerator.java:32)
at org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker.createMockType(SubclassByteBuddyMockMaker.java:71)
at org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker.createMock(SubclassByteBuddyMockMaker.java:42)
at org.mockito.android.internal.creation.AndroidByteBuddyMockMaker.createMock(AndroidByteBuddyMockMaker.java:39)
at org.mockito.internal.util.MockUtil.createMock(MockUtil.java:35)
at org.mockito.internal.MockitoCore.mock(MockitoCore.java:63)
at org.mockito.Mockito.mock(Mockito.java:1908)
at org.mockito.Mockito.mock(Mockito.java:1817)
... 31 more
Caused by: java.lang.NoSuchFieldError: No instance field targetApiLevel of type I in class Lcom/android/dx/dex/DexOptions; or its superclasses (declaration of 'com.android.dx.dex.DexOptions' appears in /data/app/com.mysugr.experiment.mockk.test-qbECL3cF6dtXH-Y-XYdIDQ==/base.apk)
at net.bytebuddy.android.AndroidClassLoadingStrategy$DexProcessor$ForSdkCompiler.makeDefault(AndroidClassLoadingStrategy.java:216)
at net.bytebuddy.android.AndroidClassLoadingStrategy$Injecting.<init>(AndroidClassLoadingStrategy.java:387)
at org.mockito.android.internal.creation.AndroidLoadingStrategy.resolveStrategy(AndroidLoadingStrategy.java:39)
at org.mockito.internal.creation.bytebuddy.SubclassBytecodeGenerator.mockClass(SubclassBytecodeGenerator.java:174)
at org.mockito.internal.creation.bytebuddy.TypeCachingBytecodeGenerator$1.call(TypeCachingBytecodeGenerator.java:37)
at org.mockito.internal.creation.bytebuddy.TypeCachingBytecodeGenerator$1.call(TypeCachingBytecodeGenerator.java:34)
at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:152)
... 42 more

or

java.lang.Exception: Unexpected exception, expected<io.mockk.MockKException> but was<java.lang.NoSuchFieldError>
at org.junit.internal.runners.statements.ExpectException.evaluate(ExpectException.java:28)
at com.mysugr.testing.coroutine.TestCoroutineScopeRule$apply$1.evaluate(TestCoroutineScopeRule.kt:32)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
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 androidx.test.ext.junit.runners.AndroidJUnit4.run(AndroidJUnit4.java:104)
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 org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:392)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2075)
Caused by: java.lang.NoSuchFieldError: No instance field minSdkVersion of type I in class Lcom/android/dx/dex/DexOptions; or its superclasses (declaration of 'com.android.dx.dex.DexOptions' appears in /data/app/com.mysugr.experiment.mockk.test-r9gglo_QPlNSN-JtUPQB7w==/base.apk)
at com.android.dx.DexMaker.generate(DexMaker.java:329)
at com.android.dx.DexMaker.generateAndLoad(DexMaker.java:521)
at com.android.dx.stock.ProxyBuilder.buildProxyClass(ProxyBuilder.java:335)
at io.mockk.proxy.android.transformation.AndroidSubclassInstrumentation.subclass(AndroidSubclassInstrumentation.kt:37)
at io.mockk.proxy.common.ProxyMaker.subclass(ProxyMaker.kt:140)
at io.mockk.proxy.common.ProxyMaker.proxy(ProxyMaker.kt:60)
at io.mockk.impl.instantiation.JvmMockFactory.newProxy(JvmMockFactory.kt:34)
at io.mockk.impl.instantiation.AbstractMockFactory.newProxy$default(AbstractMockFactory.kt:29)
at io.mockk.impl.instantiation.AbstractMockFactory.mockk(AbstractMockFactory.kt:59)
at com.mysugr.experiment.mockk.AndroidTest.t2(AndroidTest.kt:223)
at java.lang.reflect.Method.invoke(Native Method)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.ExpectException.evaluate(ExpectException.java:19)
... 25 more

NoSuchFieldError means that a field that should exist isn't. This is a strong indicator that some dependencies got messed up. That can easily happen when your included libraries have transitive dependencies to different versions of the same library. In java each library (more precisely each fully qualified name https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html) can only exist once. That means if e.g. mockk and mockito both depend on different versions of another library, only one version is loaded. Which version exactly is loaded is decided by gradle. Gradle resolves such version conflicts automatically under the hood by default. https://docs.gradle.org/current/userguide/dependency_resolution.html

In our case, the version conflict resolution led to a runtime crash, because the version that was included has breaking changes to the one that was originally used. As can be seen that an expected field cannot be found.

Investigation

To find the library that causes problems, we need to investigate further. From the error No instance field targetApiLevel of type I in class Lcom/android/dx/dex/DexOptions we know that the field is missing in the class com.android.dx.dex.DexOptions. So this class must be in the library where the wrong version is loaded. Android Studio helps because it can not only find classes in our own projects, but also in dependencies. Press ⌘ + O/Ctrl + N, type com.android.dx.dex.DexOptions and make sure "All places" is selected at the top.

IMG

In the Project View, we can see that the class is located in the dependency com.jakewharton.android.repackaged:dalvik-dx:1:

IMG

Our assuption is now that the wrong version of com.jakewharton.android.repackaged:dalvik-dx is present at runtime, where a needed field is missing, which causes the crash. After investigating transitive dependencies LINK, we see that there is really a mismatch.

This is how the transitive path to the dalvik-dx dependency looks like ("→" means depends on): mockk: io.mockk:mockk-android:1.9.3io.mockk:mockk-agent-android:1.9.3com.linkedin.dexmaker:dexmaker:2.21.0com.jakewharton.android.repackaged:dalvik-dx:9.0.0_r3 mockito: org.mockito:mockito-android:3.3.3net.bytebuddy:byte-buddy-android:1.10.5com.jakewharton.android.repackaged:dalvik-dx:1

After investigating further, we see that targetApiLevel field is actually gone in dalvik-dx:9.0.0_r3. So we found the issue! But what's next? The best best idea would be to upgrade mockito-android and hope that the transitive dependencies were updated to use the same version of dalvik-dx as mockk-android. Unfortunately, at the moment, there is no newer version for mockito-android. However, there is a newer version of byte-buddy-android (1.10.9) that actually uses dalvik-dx:9.0.0_r3. The best way to upgrade that dependency would be contribute to the mockito open source project and wait for the next release. Until the next release with the fixed dependency is ready, we can do a little hack: We tell gradle to not include byte-buddy-android from Mockito, but instead we provide the newer version:

androidTestImplementation("org.mockito:mockito-android:3.3.3") {
    exclude group: "net.bytebuddy", module: "byte-buddy-android"
}
androidTestImplementation "net.bytebuddy:byte-buddy-android:1.10.9"

Note that the exclude group is necessary for all dependencies that include byte-buddy-android, otherwise the same dependency may sneaks in via another transitive dependency.

After running the tests again, all tests succeed without the error. We fixed the problem!

Conclusion

We learned how we find the library where an incompatible version is loaded and how to replace it in gradle. In this example we were very lucky. Because the change in the version of byte-buddy-android from 1.10.5 to 1.10.9 is only a patch, meaning there aren't any breaking changes. If that isn't the case, we'd might get a new error, where another field or method is missing. Version conflicts are usually very nasty and not as easily resolved as in this case. Especially in production code I advise you to avoid such "dependency replacements" as much as possible, since it can lead to subtle runtime crashes that are hard to track down.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment