How To Write Human-Readable Tests in Kotlin With Kotest and MockK

The complete toolbox for writing tests in Kotlin that are easy to comprehend and maintain

Tobias Schmidt
Better Programming

--

Typing on keyboard
Photo by ThisIsEngineering from Pexels.

In all likelihood, everybody who has ever worked with Java knows about Mockito and Hamcrest. These frameworks help immensely when testing code by isolating dependent components and writing assertions, but they don’t do us a lot of favors regarding readability.

In this article, I want to discuss why I decided to switch to using Kotest and MockK after I left Java for Kotlin.

Where We Come From

Let’s have a look at what a regular and simple unit test looks like using Mockito and Hamcrest:

What’s used in this snippet:

  • An initialization method for creating our mocks for two dependent-on services.
  • Assigning a mock response for a method.
  • Creating a captor to verify passed arguments.
  • Assertions about the number of calls to a method.

Our code is generally not as readable “from left to right” (or vice versa) as we’re used to. Even if we are regularly in touch with that syntax, the cognitive load is still higher. We should always keep in mind that one of the most important aspects of a unit test is that it’s easy to read and comprehend. That means that every bit of reduced effort to know what’s going on is worth integrating.

Diagram explaining unit tests

If we phrase the verify assertion of our example into a sentence, we see that there’s some mix-up in comparison to the actual code. Yes, we could rephrase that into something more fitting, but I’m dividing the conditions that have to be fulfilled (verify and times = our method should be called once) and the target (services.add).

Looking at Hamcrest’s assertThat assertion, we can already see some improvement, but it’s still not perfect.

Let’s dive into MockK and Kotest to learn about how we can further improve this.

MockK for Mocking

MockK offers all the features we know from using Mockito, but with better readability and Kotlin compatibility.

Mocking

When creating your mock objects to isolate your system under test, there’s no huge difference to Mockito from a syntax perspective. The interesting and distinguishing part is how we define our stub responses with every.

The default mock requires you to specify responses for all receiving calls. If there’s a call but no stubbed response, the test will fail (e.g. calling any other method than add would end up with failed test in the example). If you want to explicitly ignore those, you can create a relaxed mock.

Relaxed mock

Looking at the readability of the method’s stub, we see its greatness.

Spying

Working with spies is equally easy.

verify also looks a lot cleaner now. The class and method are bound together and the number of expected calls is right next to the assertion keyword itself.

Example of spying

For me, MockK’s syntax has a lot less “noise” and is easier to grasp.

Capturing

Validation of passed arguments by capturing is done either via CapuringSlot or MutableList. The first option is for easier matching of a single call.

Spring support

As a Kotlin developer, there’s a good chance that you’re also using Spring Boot and wondering about MockK’s compatibility with it. springmock provides compatibility with Spring Boot’s integration tests.

It provides all the same functionality as the Mockito-based Spring Boot mock beans. Just start using @MockkBean and @SpykBean.

Note: This is a non-exhaustive list of MockK’s feature set. There’s much more to explore on mockk.io. Also, Oleksiy Pylypenko wrote a great comprehensive series about MockK’s features.

Kotest for Assertions

Kotest enables us to write assertions in a simple and clean way.

Matchers

Kotest brings a large list of core matchers with a syntax that is similar to JavaScript’s Jest. Matchers can be used either as an extension or as an inflix function.

Extension functions have the advantage of the auto-completion feature from your favorite IDE. I still prefer using the inflix style due to its strict separation of actual and expected value.

Exceptions

Checking for exceptions and their validations can be done with shouldThrow. You can either only verify that the exception is thrown or also do additional checks on the caught exception.

Clues

Immediately knowing what caused the error is also really important in order to not be frustrated when dealing with failed tests. Kotest helps with that by introducing withClue, which enables you to add further details to your assertions or avoid confusing assertion messages due to null values.

Kotest is not only a library for assertions but also for property testing. Further, it offers a lot more features not described in this article. I highly recommend browsing its documentation on kotest.io.

Putting It All Together

Let’s update our initial example:

That already looks much better, even though it’s only a very simple example. For tests with a much more complex stubbing setup and assertions, using MockK and Kotest has an even greater impact on how fast you can comprehend tests you wrote a very long time ago or that were written by somebody else.

Key Takeaways

Tests are designed to break regularly, so you know that you’ve likely caused unintended side effects — that’s why you’ll read them a lot. With MockK and Kotest, you get the complete tooling you need for writing tests in Kotlin with great readability.

And as always, don’t just take my opinion. Try it out yourself today.

Thank you for reading.

--

--

Software Engineer & Serverless Enthusiast, focusing on AWS & Azure as well as Kotlin & Node.js. Always learning & looking to meet people on the same journey!