Migrating your tests to Kotlin
This article is co-authored with Alex Levin
Kotlin is a great language with lots of great features. If you are thinking about migrating your project to Kotlin, why not to start with tests? In this article we will share tips to make your transition smooth.
Tip 1: Decide, which framework you will use
Most likely you are already using mockito. In that case, start with adding the following dependencies to your project:
In the future (when you switch your main codebase, too) you may benefit from switching to Kotlin-first frameworks (like mockk, kotest, strikt and such). This goes handy if you want a more null-safe environment.
The following article will use mockito
Tip 2: Use auto-converter from Java to Kotlin (no, seriously)
If you use IntelliJ Idea that option is available via Option+Shift+Command+K on Mac or just do double Shift and type something like “Java to Kotlin”.
Start with converting old simple tests. There’s a good chance that everything will work out of the box.
After that we recommend replacing Java functions from JUnit and Mockito to Kotlin function from kotlin-test-junit
and kotlin-mockito
.
Tip 3: Use Kotlin
Make yourself familiar with the standard library.
Instead of Java’s Stream API you can use Kotlin Collections, which give you nicer syntax:
Before:
paymentInstruments.stream().filter(pi -> Objects.equals(publicId, pi.getId())).findFirst().orElseThrow()
After:
paymentInstruments.first { it.id == publicId }
Also change Java-way collection creation to Kotlin ones (
e.g. List.of, Collections.asList, Collections.singletonList → listOf
)
Use whitespaces in tests’ names
fun subServiceIsInvoked() → fun `Sub Service is invoked`()
Now long names are easier to read.
Use lateinit var
instead of assigning null
Consider having a service that looks like this:
class Service(val subService: SubService)
After auto-conversion of tests you may have this code pattern:
@InjectMocks
val service: Service? = null
@Mock
val subService: SubService? = null
To avoid unnecessary null-checks, consider rewriting it using lateinit var
:
@InjectMocks
lateinit var service: Service
@Mock
lateinit var subService: SubService
NB: Perharps you might consider avoiding mocks injection, as it may make it difficult to track if the mock is actually used or not
Tip 4: Enjoy reified generics
Mockito matchers are slightly different in the mockito-kotlin library. Because of reified generics in most cases you don’t need to specify type explicitly:
Before:
doReturn(result).when(client).getCards(any(Data.class), any(Request.class));
After:
doReturn(result).whenever(client).getCards(any(), any())
Use any()
for non-nullable matching, otherwise use anyOrNull()
.
In some cases you still need to write types when there’s ambiguity (more than one method may match).
NB: whenever
is used instead of when
because when
is the keyword in Kotlin which means that its name should be escaped with backticks
(and that’s not looking very nice usually)
Another good example would be simplifying exception checking.
Before:
assertThrows(SomeException.class, () -> {
service.doSomething();
});
After:
assertFailsWith<SomeException> { service.doSomething() }
Tip 5: Use scope functions and DSL based on them
Mock creation
You can put the mock-specific behavior to the relevant mock for extra readability.
Before:
final Service service = mock(Service.class);
when(service.getCards(any())).thenReturn(response);
After:
val service: Service = mock {
on { getCards(any()) } doReturn response
}
Assertions
Same for assertions. As a bonus, you can call fields of the scoped object without explicit object’s name.
For that you can use with
scope function:
Before:
assertEquals(request.getAmount(), purchase.getAmount());
assertEquals(request.getShopConfigurationId(), purchase.getConfiguration().getId());
assertEquals(request.getWalletId(), purchase.getWallet().getPublicId();
assertEquals(request.getDescription(), purchase.getDescription());
assertEquals(request.getExternalReference(), purchase.getExternalReference());
After:
with(purchase) {
assertEquals(request.amount, amount)
assertEquals(request.shopConfigurationId, configuration.id)
assertEquals(request.walletId, wallet.publicId)
assertEquals(request.description, description)
assertEquals(request.externalReference, externalReference)
}
Wrapping up
We hope those ideas will help you to introduce Kotlin in tests. If you are not ready for rewriting old tests, you can also start with adding new tests only in Kotling and occasionally convert the older ones. And most importantly, enjoy!
Written by Karin-Aleksandra Monoid and Alex Levin