350 pages of hands-on tutorial right into your email inbox. Learn how to use state of the art development environment and build a full-fledged command-line application. You will receive free updates to the tutorial. Start learning Kotlin today!
Okay, a show of hands.
Who’s heard of this lovely and astounding programming language Kotlin, and wanted to try it out on the real backend project…
and couldn’t?
Yeah, most of us. This is common. The team discusses that everyone really wants to go Kotlin, and they still decide to create that new codebase in Java.
You and your fellow colleagues are afraid that this will go crazy wrong, right?
And you are right to have such a feeling, as you don’t have enough confidence yet to make the switch. What if the whole team is still learning this new technology, and there is an unexpected challenge that nobody can resolve for weeks?
Yeah… tricky situation.
Now, you’re probably quite a bit into this project, and your backend codebase is all in verbose Java, and you think: “No way I can try Kotlin now! Not until next one…”
Wrong.
There is no point in waiting until the next opportunity because of the high chances that it’ll turn out the same!
Now, what if I told you that you can still try out Kotlin together with your teammates in this codebase without any risks, and no strings attached?
Let me explain.
With Kotlin’s 100% bidirectional interoperability, it’s possible to mix Java and Kotlin code easily.
What this means, is that you can convert a single file from Java to Kotlin to get the feels of what it’ll look like. And everything will work just like before.
Now, what if I told you that such a conversion is one hotkey away?
Let me show an example. Imagine you had this simple service class in your Java backend application:
// QuizService.java
package com.iwillteachyoukotlin.quizzy;
import org.springframework.stereotype.Service;
@Service
public class QuizService {
private final QuizRepository quizRepository;
public QuizService(QuizRepository quizRepository) {
this.quizRepository = quizRepository;
}
public Quiz create(Quiz quiz) {
if (quiz.id != null) {
throw new BadRequestException("id should be empty");
}
return quizRepository.save(quiz);
}
public Quiz getQuiz(int quizId) {
return quizRepository
.findById(quizId)
.orElseThrow(() -> new NotFoundException("quiz not found"));
}
}
Now, you can press CMD+ALT+SHIFT+K
(or CTRL+ALT+SHIFT+K
), or you could use Convert Java File to Kotlin File
action:
The result is the following code. Notice how both constructor and field definition has merged into a single declaration in Kotlin:
// QuizService.kt
package com.iwillteachyoukotlin.quizzy
import org.springframework.stereotype.Service
@Service
class QuizService(private val quizRepository: QuizRepository) {
fun create(quiz: Quiz): Quiz {
if (quiz.id != null) {
throw BadRequestException("id should be empty")
}
return quizRepository.save(quiz)
}
fun getQuiz(quizId: Int): Quiz {
return quizRepository
.findById(quizId)
.orElseThrow { NotFoundException("quiz not found") }
}
}
Note for Lombok users:
Use Delombok on this file before converting to Kotlin.
Of course, you need to add Kotlin support to your build tool before you can even run this code:
(Note: refer to this guide if you’re using Maven)
Add a Gradle plugin appropriately:
plugins {
id "org.jetbrains.kotlin.jvm" version "1.3.11"
}
Note for Spring Boot users:
You need to add this plugin, so that Kotlin classes will open automatically where needed, so that Spring Boot can use reflection on them:
id "org.jetbrains.kotlin.plugin.spring" version "1.3.11"
And if you’re using JPA/Hibernate entities, you’ll need this plugin:
id "org.jetbrains.kotlin.plugin.jpa" version "1.3.11"
This adds the default constructor for your entities.
Also, you’ll need to add a dependency on Kotlin standard library and reflection library:
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlin:kotlin-reflect"
// …
}
Now you have one Kotlin file among the sea of Java files. Java classes are calling this Kotlin code, and this Kotlin code is calling to Java code.
And it all perfectly works. And all tests pass!
Now, simple automatic conversion is ok and all, but it’s not enough to really leverage the power of Kotlin.
If you had any null check anywhere that returns
or throws
an exception, for example:
fun update(quiz: Quiz): Quiz {
if (quiz.id == null) {
throw BadRequestException("id should not be empty")
}
return quizRepository.save(quiz)
}
These null checks that either throw
or return
are usually called guard if
statements. In Kotlin these can be done more succinctly with “Elvis” operator ?:
:
fun update(quiz: Quiz): Quiz {
quiz.id ?: throw BadRequestException("id should not be empty")
return quizRepository.save(quiz)
}
Optional
s to nullableAnother example that can be simplified with Kotlin is the usage of Optional
type:
fun getQuiz(quizId: Int): Quiz {
return quizRepository
.findById(quizId)
.orElseThrow { NotFoundException("quiz not found") }
}
Here we’re going to use another method of our repository that returns either a found object or null
:
fun getQuiz(quizId: Int): Quiz {
return quizRepository.findByIdOrNull(quizId)
?: throw NotFoundException("quiz not found")
}
As you can see, here we use the Elvis operator, as well. Quite handy, isn’t it?
Let’s imagine that the application above had this test in its integration test suite:
@Test
public void failsToCreate_whenIdIsProvided() throws Exception {
// ARRANGE
final Quiz quiz = new Quiz(
42,
"title",
"description",
"cta",
"https://example.org/image.png"
);
// ACT
final ResultActions actions = mockMvc.perform(post("/quizzes")
.contentType(APPLICATION_JSON_UTF8)
.content(objectMapper.writeValueAsString(quiz)));
// ASSERT
actions.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message", equalTo("id should be empty")));
}
Now, after automatic conversion to Kotlin it’ll look something like this:
@Test
@Throws(Exception::class)
fun failsToCreate_whenIdIsProvided() {
// ARRANGE
val quiz = Quiz(
42,
"title",
"description",
"cta",
"https://example.org/image.png"
)
// ACT
val actions = mockMvc!!.perform(post("/quizzes")
.contentType(APPLICATION_JSON_UTF8)
.content(objectMapper!!.writeValueAsString(quiz)))
// ASSERT
actions.andExpect(status().isBadRequest)
.andExpect(jsonPath("$.message", equalTo("id should be empty")))
}
First, we can throw away this @Throws
annotation immediately. It’s mostly useless in Kotlin.
And now, we can use actual human-readable sentences in the method names using backticks:
@Test
fun `create quiz - fails to create when id is provided`() {
// …
}
My general structure is: “{method name or use case name} – {expected outcome} when {condition}.”
lateinit
instead of nullableIn cases, when something is being provided later (like dependency injection, or initialized in the set-up section of the test suite), you should use lateinit
instead. It’s much cleaner.
Look, this code is a result of automatic conversion:
@SpringBootTest
@RunWith(SpringRunner::class)
class QuizzesIntegrationTest {
@Autowired
private val context: WebApplicationContext? = null
@Autowired
private val objectMapper: ObjectMapper? = null
private var mockMvc: MockMvc? = null
private var quizId: Int = 0
@Before
fun setUp() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context!!)
.build()
}
// …
}
All these fields are initialized later, AND before the first usage, so we can tell the compiler about that:
@Autowired
private lateinit var context: WebApplicationContext
@Autowired
private lateinit var objectMapper: ObjectMapper
private lateinit var mockMvc: MockMvc
private var quizId: Int = 0
Now, the problem is that all these nullable values were unsafely unwrapped everywhere in the test suite with !!
operator:
Of course, we can fix them all manually, but these are things that tools should fix for us.
And they do, look:
It still would be annoying to go through each occurrence though. So we should just apply code cleanups to the whole file:
Now, you see this snippet of code:
// ARRANGE
val quiz = Quiz(
42,
"title",
"description",
"cta",
"https://example.org/image.png"
)
We can make this code much more descriptive if we were to use named arguments, like so:
Now, you’ll get this cute little readable snippet of code. Also, you can re-order the arguments when you pass them as you wish, and as it makes more sense.
// ARRANGE
val quiz = Quiz(
id = 42,
title = "title",
description = "description",
ctaText = "cta",
imageUrl = "https://example.org/image.png"
)
Unfortunately, to make this feature work, the class Quiz
can’t be a Java class, it has to be in Kotlin, so we’ll have to convert the entity below to Kotlin:
@Entity(name = "quizzes")
@Data
@RequiredArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties({"hibernateLazyInitializer"})
public class Quiz {
@Id
@GeneratedValue
public Integer id = null;
public final String title;
public final String description;
public final String ctaText;
public final String imageUrl;
Quiz() {
title = "";
description = "";
ctaText = "";
imageUrl = "";
}
}
This is a result of full (auto + manual) conversion:
@Entity(name = "quizzes")
@JsonIgnoreProperties("hibernateLazyInitializer")
data class Quiz(
@Id
@GeneratedValue
var id: Int? = null,
val title: String,
val description: String,
val ctaText: String,
val imageUrl: String
)
As you can see we’re getting rid of all the Lombok stuff and using a data class
now because it can do most of the things, you would need from the entity.
If you want to learn more about how good Kotlin code will look like, I highly recommend to go through the Kotlin Koans.
You can do that right in your browser, or in your IDE if you wish.
Now, remember, at the beginning of this post, I’ve promised the “Without Risk” part, didn’t I?
Now, if I decided not to make the switch to Kotlin yet (because I still need to convince my colleagues, for example), I can use Local History
feature of my IDE to go back in time to when I started playing with Kotlin:
To open that tool in IntelliJ, you can use the contextual menu on the whole project in the project structure tool. There is an option Local History > Show History
there:
When you’ve found the place where you started playing with Kotlin, you can revert to the first Kotlin-related change, for example:
Now, there is not a single Kotlin file, and all the tests are passing. This is a handy feature in general if you screwed something up, and haven’t made a commit in Git in a while.
Did that spike your curiosity about Kotlin?
I have written a 4-part (350-pages total) “Ultimate Tutorial: Getting Started With Kotlin” (+ more to come), and you can get it as a free bonus by becoming a member of my monthly newsletter.
On top of just Kotlin, it is full of goodies like TDD, Clean Code, Software Architecture, Business Impacts, 5 WHYs, Acceptance Criteria, Personas, and more.
—Sign up here and start learning how to build full-fledged Kotlin applications!
Thank you so much for reading this article! I hope you enjoyed it. Please, tell me what you think about this in the comments!
Also, it would make me so much happier share this post with your friends and colleagues who you think might benefit from it. Or you could share it on your favorite social media!
You are welcome to read my blog about Kotlin, and my blog about TDD and best software engineering practices.
And let’s connect on LinkedIn: I post short weekly updates about software developer’s productivity and happiness, teamwork, mental health, and a bit about Kotlin.
If you want to hear more of my opinions, follow me on Twitter.
See the code above? It’s actually a runnable JUnit4 test in Kotlin. It’s also a description of business rules readable in English.
Mind. Blown.
Let’s dive in how did I get there.
class GameTest {
@Test
fun `scissors cut paper`() {
val game = Game()
val winner = game.play(Throw.SCISSORS, Throw.PAPER)
assertThat(winner).isEqualTo(Winner.FIRST_PLAYER)
}
}
At this point, the test doesn’t compile: Game, Throw, and Winner are not defined. Let’s create classes and enums for these:
class Game {
fun play(first: Throw, second: Throw): Winner {
TODO("not implemented")
}
}
enum class Throw {
SCISSORS, PAPER
}
enum class Winner {
FIRST_PLAYER
}
Now it compiles. Before we run the test suite, let’s make an assumption of how it’s going to fail. It’s going to fail with the “not implemented” error. Let’s run the test suite and see:
Great. Exactly as expected.
This technique is essential to TDD. It has a name—“Calling a Shot.” If we call a shot, and something unexpected happens, that means that we have a mistake in our test. That is a way to test your tests when writing them.
Let’s make it pass with the simplest implementation. Just return the FIRST_PLAYER
as a winner:
fun play(first: Throw, second: Throw): Winner {
return Winner.FIRST_PLAYER
}
If we run the tests now, they all pass:
Making the test pass with the simplest implementation is what makes TDD produce near 100% test coverages. And I’m not talking about lines or branches—about TRUE test coverage.
Basically, the simplest implementation is when no matter what you “subtract” from it—at least one test will fail. In a sense, writing such an implementation you’re exposing test cases that you didn’t even think about before, and likely wouldn’t have written them as automated tests.
At this point, all our tests are passing. This is called a “Green” stage of the TDD cycle. In this stage, we are free to clean up and refactor anything we want. So we take a look at both production code and test code:
Everything looks fine right now—there is no duplication, names are alright, and there is nothing weird. So we loop back to the first stage of TDD again—“Red” stage.
The next test flows naturally from our current simplest implementation. Basically, the question that we need to ask at this point—how can we prove the current implementation wrong?
Since we are returning the constant value, we can prove the implementation wrong by providing inputs where the expected winner is different. We can do it if we were to reverse SCISSORS
and PAPER
arguments:
@Test
fun `scissors cut paper - reverse`() {
val game = Game()
val winner = game.play(Throw.PAPER, Throw.SCISSORS)
assertThat(winner).isEqualTo(Winner.SECOND_PLAYER)
}
Winner.SECOND_PLAYER
is not defined yet, so we’ll create it as an enum constant:
enum class Winner {
FIRST_PLAYER, SECOND_PLAYER
}
Now, before we are going to run the test, we are going to call a shot. We expect that there will be an assertion error in the second test. The expected winner should be SECOND_PLAYER,
but the actual will be FIRST_PLAYER
since we are returning the same constant always at the moment.
Now, we’re ready to run the test suite:
org.junit.ComparisonFailure:
Expected: SECOND_PLAYER
Actual: FIRST_PLAYER
at rps.GameTest.scissors cut paper - reverse(GameTest.kt:22)
That is precisely what we expected.
Let’s make this test pass by wrapping the existing return statement in the simplest if
statement, and returning the SECOND_PLAYER
right after:
fun play(first: Throw, second: Throw): Winner {
if (first == Throw.SCISSORS) {
return Winner.FIRST_PLAYER
}
return Winner.SECOND_PLAYER
}
If we rerun the test suite, everything should be green now. Let’s look for refactoring opportunities now.
Those pesky fully-qualified names make the code slightly less readable. I think we can omit them by using direct imports. We’ll start with the test code:
import rps.Throw.*
import rps.Winner.*
// ...
@Test
fun `scissors cut paper`() {
val game = Game()
val winner = game.play(SCISSORS, PAPER)
assertThat(winner).isEqualTo(FIRST_PLAYER)
}
@Test
fun `scissors cut paper - reverse`() {
val game = Game()
val winner = game.play(PAPER, SCISSORS)
assertThat(winner).isEqualTo(SECOND_PLAYER)
}
And let’s do the same to the production code:
import rps.Throw.*
import rps.Winner.*
// ...
fun play(first: Throw, second: Throw): Winner {
if (first == SCISSORS) {
return FIRST_PLAYER
}
return SECOND_PLAYER
}
Now that we’ve done the refactoring, we should run the test suite. It is still green! Let’s take a look if we have any duplication we should get rid of:
The first thing that stands out in our test code is that we’re creating the game
object in every test in precisely the same way.
The rule of thumb for refactoring the duplication is that we need three occurrences. Here though, you can already tell that the next test will need to create the same object in the same way. So we might as well refactor right now.
Extracting the variable as a private field of the class works here:
class GameTest {
private val game = Game()
@Test
fun `scissors cut paper`() {
val winner = game.play(SCISSORS, PAPER)
assertThat(winner).isEqualTo(FIRST_PLAYER)
}
@Test
fun `scissors cut paper - reverse`() {
val winner = game.play(PAPER, SCISSORS)
assertThat(winner).isEqualTo(SECOND_PLAYER)
}
}
Right after making this atomic change, we should run the tests. And they still pass!
Now, the next duplication is calling to game.play
with two throw arguments and then asserting the result is correct. Again, there are only two occurrences so far, but we can easily tell that every test will have the same snippet of the code.
Let’s extract the private method assertGame:
private fun assertGame(first: Throw,
second: Throw,
expected: Winner) {
val actual = game.play(first, second)
assertThat(actual).isEqualTo(expected)
}
And the tests that are now using this function look like this:
@Test
fun `scissors cut paper`() {
assertGame(SCISSORS, PAPER, FIRST_PLAYER)
}
@Test
fun `scissors cut paper - reverse`() {
assertGame(PAPER, SCISSORS, SECOND_PLAYER)
}
Let’s verify that we didn’t break anything by rerunning the test suite. It’s all green—great!
Now if you look carefully, these two tests are actually describing a single business rule, and they are simple one-liners. And we can already tell that every rule of the Rock-Paper-Scissors game will come in a pair like this.
Let’s put these two lines together and get rid of the “reversed” test:
@Test
fun `scissors cut paper`() {
assertGame(SCISSORS, PAPER, FIRST_PLAYER)
assertGame(PAPER, SCISSORS, SECOND_PLAYER)
}
And if we run the test suite—it still passes. At this point, we have eliminated the duplication. And spelling out the next business rule will be easy.
Watch:
@Test
fun `paper covers rock`() {
assertGame(PAPER, ROCK, FIRST_PLAYER)
assertGame(ROCK, PAPER, SECOND_PLAYER)
}
And to make this pass, we can just add second == ROCK
to our if
condition:
fun play(first: Throw, second: Throw): Winner {
if (first == SCISSORS || second == ROCK) {
return FIRST_PLAYER
}
return SECOND_PLAYER
}
While there is no duplication, the test still doesn’t read very well. At this point, we can play with a few options. For example, create a custom AssertJ assertion:
assertThat(SCISSORS).beats(PAPER)
That will expand to two assertGame
calls. Another option is to use Kotlin’s infix extension functions that make English like sentences a piece of cake. Let’s implement a beats
infix function for Throw
enum:
private infix fun Throw.beats(other: Throw) {
assertGame(this, other, FIRST_PLAYER)
assertGame(other, this, SECOND_PLAYER)
}
And now we can replace double calls to assertGame
in our tests:
@Test
fun `scissors cut paper`() {
SCISSORS beats PAPER
}
@Test
fun `paper covers rock`() {
PAPER beats ROCK
}
Again, we’re running into the same problem: the code itself already telling us in English what the test is about so we can merge these tests into one:
@Test
fun `game rules`() {
SCISSORS beats PAPER
PAPER beats ROCK
}
Adding the final pair is a no-brainer now:
@Test
fun `game rules`() {
SCISSORS beats PAPER
PAPER beats ROCK
ROCK beats SCISSORS
}
Now if we run the test—it fails because:
org.junit.ComparisonFailure:
Expected: FIRST_PLAYER
Actual: SECOND_PLAYER
at rps.GameTest.assertGame(GameTest.kt:28)
at rps.GameTest.beats(GameTest.kt:19)
at rps.GameTest.game rules(GameTest.kt:15)
Oh. I’m not sure. Of course, following the stacktrace, I could figure this out, but it is not immediately apparent.
This test failure sucks. So this is the other side of the trade-off of the abstraction in the test. You also have to make the failure more readable by giving more context in the failure message:
// inside of assertGame function:
assertThat(actual)
.withFailMessage("""
For a game $first vs. $second, the winner should be $expected
Expected: $expected
Actual: $actual
""".trimIndent())
.isEqualTo(expected)
Let’s rerun the test suite and take a look at the failure message:
java.lang.AssertionError: For a game ROCK vs. SCISSORS, the winner should be FIRST_PLAYER
Expected: FIRST_PLAYER
Actual: SECOND_PLAYER
at rps.GameTest.assertGame(GameTest.kt:35)
at rps.GameTest.beats(GameTest.kt:19)
at rps.GameTest.game rules(GameTest.kt:15)
Oh, this is much better!
if
statement to match the game rules (Green)To make this assertion pass, we need to add a condition when the ROCK
is the first
throw:
if (first == SCISSORS ||
second == ROCK ||
first == ROCK) {
return FIRST_PLAYER
}
That fails with “For a game ROCK vs. PAPER, the winner should be SECOND_PLAYER.” Let’s handle this case by clarifying that not only ROCK
should be the first throw, but also SCISSORS
has to be the second one:
if (first == SCISSORS ||
second == ROCK ||
first == ROCK && second == SCISSORS) {
return FIRST_PLAYER
}
If we follow all these failures one by one, eventually we arrive at the following if
statement that is absolutely symmetrical to the test:
Great! Remember, the winning conditions do not describe the full set of game rules. We also need to handle ties:
Let’s follow the same pattern and add the helper function for the “tie” rule:
private infix fun Throw.tiesWith(other: Throw) {
assertGame(this, other, TIE)
assertGame(other, this, TIE)
}
And let’s write all the rules for it (skipping a few cycles of TDD here to keep it short):
@Test
fun `game rules`() {
SCISSORS beats PAPER
PAPER beats ROCK
ROCK beats SCISSORS
SCISSORS tiesWith SCISSORS
PAPER tiesWith PAPER
ROCK tiesWith ROCK
}
It seems like the simplest solution for all three “tie” assertions would be to compare first throw with the second one, and see if they are equal:
fun play(first: Throw, second: Throw): Winner {
if (first == second) {
return TIE
}
if (first == SCISSORS && second == PAPER ||
first == PAPER && second == ROCK ||
first == ROCK && second == SCISSORS) {
return FIRST_PLAYER
}
return SECOND_PLAYER
}
If we run our tests now, they’ll all pass. Fantastic!
If you want to, you can go even further and introduce custom verbs for each of the Throw types. I wouldn’t recommend this for pragmatic reasons—it’s an overkill and will make people ask WTF at the rate of 20/s.
You can do it, and it is quite simple. Just introduce aliases for the beats
function. Let’s do it for the SCISSORS cut PAPER
only:
private infix fun Throw.cut(other: Throw) = beats(other)
// usage:
SCISSORS cut PAPER
For most of the involved domains, you might want to have custom verbs like that. I yet to see one though.
Thank you so much for reading. I hope you learned something new. To make me entirely happy, share this article on social media with your friends and colleagues.
Where do you see yourself using this in your test suite? Leave the comment below!