How to Practice TDD With Kotlin
Learn how to implement TDD with Kotlin. We'll also learn how to improve the readability of our test or reuse the same process.
Join the DZone community and get the full member experience.
Join For FreeI will start this article by asking you two questions:
- Do you like writing tests?
- Do you practice test-driven design/development (TDD) in your development?
Most of you, myself included, have considered testing a luxury for developers, often the first task we sacrifice when trying to build a Minimum Viable Product (MVP).
But we are wrong to do that because tests:
- Reduce possible production regressions
- Save us time when troubleshooting
- Validate functional design
- Ensure we respect the target solution
- Increase the overall quality of our program
- Make the program more reliable, giving us more confidence to push new versions to production
So, we agree to do more tests, but TDD... is it something applicable in daily practice?
TDD as Test-Driven Development
Test-driven development (TDD) refers to a programming style that favors testing before writing production code. Basically, you follow these steps:
- Write a test.
- Run the test (it should fail).
- Write production code.
- Run the test (it should pass).
- Refactor until the code conforms.
- Repeat, accumulating unit tests over time.
If you want to know more about TDD, there are tons of articles related to this topic that explain in detail the benefits of TDD (for example, What is Test Driven Development (TDD)?).
There are many ways to implement TDD, such as unit testing, feature testing, etc. However, in practice, it is not always natural, and we often have to struggle with ourselves to consistently use it.
Kotlin for TDD?
Kotlin has many advantages compared to Java for writing TDD, particularly:
- Kotlin reduces Java boilerplate thanks to function extensions, which can improve your test readability.
- Kotlin supports infix notation, allowing you to write code in more natural English.
val canOrder = user.isAuthenticated and user.hasACreditCard
- Kotlin supports backtick function names, which are really convenient for writing tests.
class MyTestCase {
@Test fun `ensure everything works`() { /*...*/ }
@Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}
- Kotlin is interoperable with Java, meaning you can use Kotlin to write your tests while your production code is written in Java.
That's why I consider Kotlin the best language to implement TDD. It inspired me to build a library to set up a TDD environment:
@Test
fun `I should be able to insert a new item in my todo list`() {
given {
`a todo list`
} and {
`an item`("Eat banana")
} `when` {
`I add the last item into my todo list`
} then {
`I expect this item is present in my todo list`
}
}
Seems cool, right?
Actually, Kotlin-TDD provides two flavors:
- GivenWhenThen: exposing the
given
,and
,when
, andthen
infix functions. - AssumeActAssert: exposing the
assume
,and
,act
, andassert
infix functions.
Indeed, the same test above can be written following the AAA pattern:
@Test
fun `I should be able to insert a new item in my todo list`() {
assume {
`a todo list`
} and {
`an item`("Eat banana")
} act {
`I add the last item into my todo list`
} assert {
`I expect this item is present in my todo list`
}
}
Let's try to use it in a concrete example.
Let's assume you have a requirement to create a Todo List application, and one of the acceptance criteria is: As a user, I should see my new item when it has been added to my To-do list.
We will try to practice TDD using this library.
1 - Setup Kotlin-TDD
Let's start by importing this dependency into your project. I'm assuming JUnit 5 is installed in your project.
With Gradle:
testCompile "io.github.ludorival:kotlin-tdd:2.1.0"
With Maven:
<dependency>
<groupId>io.github.ludorival</groupId>
<artifactId>kotlin-tdd</artifactId>
<version>2.1.0</version>
<scope>test</scope>
</dependency>
2 - Play with Kotlin-TDD
You can start playing with Kotlin-TDD with this example below:
import io.github.ludorival.kotlintdd.SimpleGivenWhenThen.given
class MyTest {
@Test
fun `my first test`() {
given {
1
} `when` {
it + 2 // result is 1
} then {
assertEquals(3, it) // result is 3
}
}
}
3 - Write Your Custom DSL
A Domain Specific Language is a computer language specialized to a particular application domain.
This step is optional but it helps to describe your action in a natural language.
In this tutorial, we will build a simple Todo List application. The application will allow users to create a todo list, add items to the list, and manage these items. The core functionality will be defined by the following interface:
interface API {
fun createTodoList(): TodoList
fun createItem(name: String): Item
fun addItem(todoList: TodoList, item: Item): TodoList
}
Note: Since we are in phase 1 of TDD, no production code is allowed. Let's add it to the test package for now.
This interface provides methods to create a new todo list, create a new item, and add an item to an existing todo list.
- Let's create a new class Assumption implementing the base class
Step
that will contain operations insideGiven
step:
class Assumption(private val api: API) : Step() {
val `a todo list` get() = api.createTodo()
fun `an item`(name: String) = api.createItem(name)
}
Note that we are reusing the interface API defined above.
- Create an
Action
class that will contain all possible operations inside theWhen
step:
class Action(private val api: API) : Step() {
val `I add the last item into my todo list` get() = api.addItem(
first<TodoList>(),
last<Item>()
)
}
Check at lines 4 and 5 the use of first<TodoList>()
and last<Item>()
functions. Those functions come with the Context and store all previous context of each step:
- The
first<TodoList>
allows fetching the firstTodoList
instance returned by a step. - The
last<Item>()
allows getting the lastItem
instance returned by a step.
You can see all available functions in the documentation.
- Create an Assertion class for all possible operations inside
Then
step:
class Assertion : Step() {
val `I expect this item is present in my todo list`
get() = Assertions.assertTrue {
first<TodoList>().items.contains(
last<Item>()
)
}
}
- Create a file named
UnitTest.kt
for example and extends the classGivenWhenThen
:
// src/test/kotlin/com/example/kotlintdd/UnitTest.kt
package com.example.kotlintdd
// Implement Test API
val api = object: API {
override fun createTodoList() = TodoList()
override fun createItem(name: String) = Item(name)
override fun addItem(todoList: TodoList, item: Item) = todoList.apply {
add(item)
}
}
// Use GivenWhenThen Pattern
object UnitTest : GivenWhenThen<Assumption, Action, Assertion>(
assumption = Assumption(),
action = Action(),
assertion = Assertion()
)
// defines the entrypoint on file-level to be automatically recognized by your IDE
fun <R> given(block: Assumption.() -> R) = UnitTest.given(block)
fun <R> `when`(block: Action.() -> R) = UnitTest.`when`(block)
4 - Write Your Unit Test
Now we have configured our TDD and our custom DSL, let's put it all together in a test:
// src/test/kotlin/com/example/kotlintdd/TodoListTest
package com.example.kotlintdd
import org.junit.jupiter.api.Test
class TodoListTest {
@Test
fun `I should be able to insert a new item in my todo list`() {
given {
`a todo list`
} and {
`an item`("Eat banana")
} `when` {
`I add the last item into my todo list`
} then {
`I expect this item is present in my todo list`
}
}
}
6 - Run the Test
Of course, the test is failing due to a compilation error. We did not write any production code yet. Don't worry; this is part of the TDD process.
7 - Write Production Code
Now it is time to make the test pass.
Create the Item
class:
// src/main/kotlin/com/example/kotlintdd/Item
package com.example.kotlintdd
data class Item(val name: String)
- Create the
TodoList
class:
// src/main/kotlin/com/example/kotlintdd/TodoList
package com.example.kotlintdd
class TodoList {
val list = mutableListOf()
fun add(item: Item) {
list.add(item)
return this
}
fun contains(item: Item) = list.contains(item)
}
8 - Run the Test Again
Bravo, your test is Green!
9 - Next Steps: Acceptance Test
We can continue to add more tests by combining the Given-When-Then pattern and our custom DSL. The DSL can be enhanced for more use cases. The advantage of Kotlin-TDD is that you can reuse the same process for writing acceptance tests as well.
Let's assume you have a Spring application where the TodoList
and Item
are saved in a database. The creation and update should be done through the database for these entities.
We expect to have three endpoints in our REST API:
POST /v1/todo/list // Create a new Todo list -> return the TodoList with an id
POST /v1/todo/item // Create a new Item -> return the Item with an id
PUT /v1/todo/list/{listId}/add // add the item defined by {itemId} in the list {listId}
We can write a different implementation of our API interface:
// src/test/kotlin/com/example/kotlintdd/acceptance/RestAPI.kt
package com.example.kotlintdd.acceptance
import com.example.kotlintdd.Action
import com.example.kotlintdd.Item
import com.example.kotlintdd.TodoList
import org.springframework.http.HttpEntity
import org.springframework.http.HttpMethod
import org.springframework.http.ResponseEntity
import org.springframework.web.client.RestTemplate
class RestAPI : API {
private val url = "http://localhost:8080/spring-rest/v1"
private val restTemplate = RestTemplate()
override fun createTodoList(): TodoList {
val response: ResponseEntity<TodoList> = restTemplate
.exchange("$url/todo",
HttpMethod.POST,
HttpEntity(TodoList()),
TodoList::class.java)
return response.body!!
}
override fun createItem(name: String): Item {
val response: ResponseEntity<Item> = restTemplate
.exchange("$url/item",
HttpMethod.POST,
HttpEntity(Item(name)),
Item::class.java)
return response.body!!
}
override fun addItem(todoList: TodoList, item: Item): TodoList {
val response: ResponseEntity<TodoList> = restTemplate
.exchange(
"$url/todo/${todoList.id}/add",
HttpMethod.PUT,
HttpEntity(item),
TodoList::class.java
)
return response.body!!
}
}
And you need to set up this new action for a different instance of GivenWhenThen:
// src/test/kotlin/com/example/kotlintdd/acceptance/AcceptanceTest.kt
package com.example.kotlintdd.acceptance
import com.example.kotlintdd.Action
import io.github.ludorival.kotlintdd.GWTContext
import io.github.ludorival.kotlintdd.GivenWhenThen
// use our instance of RestAPI
val api = RestAPI()
object AcceptanceTest: GivenWhenThen<Assumption, Action, Assertion>(
assumption = Assumption(api),
action = Action(api),
assertion = Assertion()
)
fun <R> given(block: Assumption.() -> R) = AcceptanceTest.given(block)
Then, I can literally copy my unit test as an acceptance test:
// src/test/kotlin/com/example/kotlintdd/acceptance/TodoListAT.kt
package com.example.kotlintdd.acceptance
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class TodoListAT {
@Test
fun `I should be able to insert a new item in my todo list`() {
given {
`a todo list`
} and {
`an item`("Eat banana")
} `when` {
`I add the last item into my todo list`
} then {
`I expect this item is present in my todo list`
}
}
}
Of course, we can factorize it into a common function, but you are starting to see the magic!
Conclusion
We saw a concrete example of how to use Kotlin-TDD to implement the TDD technique. By writing a custom DSL, we improve the readability of our tests, making it easy to understand even after several months. We also saw that we can reuse the same process for acceptance tests without changing the way we build our tests.
Now it is your turn to adopt it, and please share your feedback directly in the Github repo.
Thanks for reading!
Opinions expressed by DZone contributors are their own.
Comments