The Road to Mockative 2
A Brief History of Kotlin Multiplatform
When Kotlin first came to the scene, it was purely a JVM language. This changed with the introduction of Kotlin/JS, Kotlin/Native, and Kotlin/Wasm, which allowed developers to use Kotlin to target just about anything. Targeting multiple of these platforms with the same Kotlin codebase became known as Kotlin Multiplatform (abbreviated as KMP).
When I first dipped my toes in Kotlin Multiplatform development in 2021, the team I joined had to resort to various uncommon solutions to overcome some of the challenges of this new, immature ecosystem. We have come a long way since then, and the developer experience is a lot smoother nowadays. JetBrains has done a lot of work to improve the experience, as has the community.
Mocking dependencies in Kotlin
The ability to mock dependencies is a powerful tool in many developers’ unit testing toolbelt. It is a key part that simplifies providing stub implementations of dependencies, enabling developers to test small parts of their code (units) separately. A mocking framework was one of the things that didn’t exist in the early days of Kotlin Multiplatform. You might think you could use any existing mocking solutions that work with Kotlin, such as MockK or Mockito. However, this is where the distinction between which platform you’re targeting using Kotlin becomes essential. You see, when targeting Kotlin/JVM, we have access to all the Java APIs, which includes nice things such as reflection, runtime proxies, and bytecode generation, none of which are available when targeting other platforms supported by Kotlin. However, Kotlin/JS can do a lot of the same due to the dynamic nature of JavaScript. Kotlin/Native, however, which is the target used when developing cross-platform native applications for both mobile and desktop, has only very limited reflection and is not dynamic in any way. These constraints and the fact that all existing solutions to mocking dependencies in Kotlin were using these features in one way or another meant that a new solution had to be developed. This paved the way for Mockative.
Introducing Mockative 2
Back in October 2021, I got the idea for Mockative. I threw together a proof-of-concept over the course of a few evenings and quickly realized the potential, so I polished up what I had built and shipped it a few weeks later. Now, almost two years later, Kotlin Multiplatform has come a long way, so it was time to have a fresh look at Mockative and see if I could improve on some of the nuisances and gotchas the API of Mockative 1 inherently suffered from. This meant a breaking change and, thus, a fresh new major version number.
So what changed?
In Mockative 1, when you wanted to mock a function through an invocation of a function it would look like this:
given(api).invocation { fetch("mockative/mockative") }
.thenReturn(mockative)
Not all that bad, right? I didn’t think so either, but the given(api) function combined with the receiver of the block passed to the invocation the function being the instance you’re calling a function on always looked strange to me, and I longed for an API similar to that of MockK with its’ every function. So, in Mockative 2, the same setup as above now looks like this:
every { api.fetch("mockative/mockative") }
.returns(mockative)
You may think this is such a seemingly insignificant change, and I partly agree with you. If this was the only change I wanted to introduce in Mockative 2, I may not have chosen to break the API like this. What I really wanted to change was the matcher API, which looks like this in the first version of Mockative:
given(api).function(api::fetch)
.whenInvokedWith(eq("mockative/mockative"))
.thenReturn(mockative)
A few of the things I disliked about this are:
- There’s no inherent link between api::fetch , and the argument passed to given(api). This leads to developer mistakes like writing given(api).function(Api::fetch) (notice the capital A), which may look right but wouldn’t work as expected because it’s expecting to mock a function accepting both an instance of Api and a String as its parameters.
- The whenInvokedWith function, while enabling a type-safe non-dynamic mocking that plays well with Kotlin/Native and, thus, Kotlin Multiplatform, it inherently decouples the matchers from the parameter names of the actual function call and thus makes it error-prone.
This is where the “inline” either/or matcher API of MockK and Mockito was very desirable. Still, I always thought it was too cumbersome to achieve in Kotlin/Native and, thus, Kotlin Multiplatform due to the strict nature of native development. In Mockative 2, that now looks like this:
every { api.fetch(any()) }
.returns(repository)
Which is deceptively close to the API of MockK and thus should feel very familiar to any developers using MockK already.
Throughout the lifetime of Mockative, I picked up on a few tricks other developers implemented in their solutions that made this kind of API possible on Kotlin/Native, specifically the fact that Kotlin, on any platform that’s not a JVM, does not perform type-checks on values until they’re actually used. This means that, on non-JVM platforms, we can make the any() function return any value we want, such as Unit and do a type-cast to the value our function accepts because the type-cast to a generic type is unchecked (just like on the JVM). For once, the non-JVM targets of Kotlin were the easiest ones to provide a solution for! For Kotlin/JVM, though, a type-check is performed for every non-generic argument passed to a function, and as such, this implementation results in ClassCastExceptions being thrown for all of our functions. To make matchers work on Kotlin/JVM, we would have to think of something fancier. Fortunately, those thoughts have already been thought by the developers behind MockK, Mockito, and MocKMP, which all served as inspiration and reference for the solution that is now present in Mockative 2.
So, if you’re a Kotlin Multiplatform developer and long for a way to mock dependencies in your tests across all the platforms you’re targeting, head over to the GitHub repository and try out the new API of Mockative2 today.
Mocking in Kotlin Multiplatform was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.