Mocking Kotlin classes with Mockito — the fast way
Mockito can’t mock final
classes and, by default, all classes in Kotlin are final
. Let’s see how to workaround this issue without any speed decrease.
Ok, we are going to start with an example: you are moving from Java to Kotlin and you write your first two classes in Kotlin class:
class Foo {
fun calculateTheFoo(): Int {
sleep(1_000) // Difficult things here
return 1
}
}
class Bar(private val foo: Foo) {
fun calculateDoubleFoo() = foo.calculateTheFoo() * 2
}
And now you want to test Bar
:
@Test
fun calculateDoubleFooTest() {
val foo = mock(Foo::class.java)
`when`(foo.calculateTheFoo()).thenReturn(2)
val bar = Bar(foo)
assertThat(bar.calculateDoubleFoo(), `is`(4))
}
You run your tests and get this error:
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class com.example.Foo
Mockito cannot mock/spy because :
— final class
This is normal; Mockito by default can’t mock a final
class. But you have a few options to solve this problem:
The naïve solution
Make Foo
and Foo.calculateTheFoo()
open (yes, you must write open
twice). This solution works perfectly but you are going against the language. Kotlin wants close
by default, and you are writing open
everywhere. So, for us, this is not a viable option.
Mockito's mock-maker-inline plugin
Use the mock-maker-inline
from Mockito as explained by Hadi Hariri in this post. It works perfectly and you don’t need to change anything in your production code.
The problem is the performance. This new mock process is really slow, between 1.5 and 3 times slower. Therefore, I opened a issue in the Mockito Repo to report this performance problem and the answer was:
Inline mocks have to redefine classes what is expensive. The inline mock maker will therefore always be slower, unfortunately.
Kotlin’s all-open plugin
At this point I thought that maybe kotlinc
had a flag to change the “all final by default” to “all open by default”. I “researched” a bit and I found that there wasn’t that flag, but there was a plugin to do more less the same: allopen
.
The all-open compiler plugin adapts Kotlin […] and makes classes annotated with a specific annotation and their members open without the explicit
open
keyword.
How does it work? First of all, you must create an annotation, that’s really easy with Kotlin:
annotation class Mockable
Now you need to configure allopen
in our build.gradle
file:
dependencies {
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
}
apply plugin: 'kotlin-allopen'
allOpen {
annotation('com.example.Mockable')
}
And the last step is to annotate the class that you want to mock:
@Mockable
class Foo {
fun calculateTheFoo(): Int {
sleep(1_000) // Difficult things here
return 1
}
}
That’s it! It works and there isn’t any performance decrease.
The downside of this option is that you must add a small change in your production code to mock it. That’s not ideal, but this is cleaner than the first option (writing open
). If you remove the annotation, the compiler will point out all the annotation usages, so you can clean your production code easily.
Measuring the speed improvements in our main project with 2,668 unit tests, the option with mock-maker-inline
takes 20.0 seconds and with the all-open solution 7.3 seconds. So our tests run 2.7 times faster now. On the other hand, we saw a little increase in compilation time, 0.5 seconds more. Anyway, the all-open option is much faster as you can see in the graphic.
If you are using Mockito and Kotlin, I recommend you use the all-open plugin to speed up all your tests and improve your productivity. Happy coding!
Update: if you want to disable the all-open plugin in release builds check this StackOverflow answer.