DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Mastering Unit Testing and Test-Driven Development in Java
  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing
  • Best Practices for Writing Unit Tests: A Comprehensive Guide
  • Exploring Unit Testing in Golang

Trending

  • Kubeflow: Driving Scalable and Intelligent Machine Learning Systems
  • Beyond ChatGPT, AI Reasoning 2.0: Engineering AI Models With Human-Like Reasoning
  • Power BI Embedded Analytics — Part 2: Power BI Embedded Overview
  • How the Go Runtime Preempts Goroutines for Efficient Concurrency
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. How to Practice TDD With Kotlin

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.

By 
Ludovic Dorival user avatar
Ludovic Dorival
·
Updated May. 08, 25 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
7.2K Views

Join the DZone community and get the full member experience.

Join For Free

I will start this article by asking you two questions:

  1. Do you like writing tests?
  2. 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:

  1. Write a test.
  2. Run the test (it should fail).
  3. Write production code.
  4. Run the test (it should pass).
  5. Refactor until the code conforms.
  6. 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.
Kotlin
 
val canOrder = user.isAuthenticated and user.hasACreditCard
  • Kotlin supports backtick function names, which are really convenient for writing tests.
Kotlin
 
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:

Kotlin
 
@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, and then infix functions.
  • AssumeActAssert: exposing the assume, and, act, and assert infix functions.

Indeed, the same test above can be written following the AAA pattern:

Kotlin
 
@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:

Kotlin
 
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:

Kotlin
 
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 inside Given step:
Kotlin
 
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 the When step:
Kotlin
 
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 first TodoList instance returned by a step.
  • The last<Item>() allows getting the last Item 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: 
Kotlin
 
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 class GivenWhenThen:
Kotlin
 
// 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:

Kotlin
 
// 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:

Kotlin
 
// src/main/kotlin/com/example/kotlintdd/Item
package com.example.kotlintdd

data class Item(val name: String) 


  • Create the TodoList class:
Kotlin
 
// 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:

Kotlin
 
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:

Kotlin
 
// 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:

Kotlin
 
// 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:

Kotlin
 
// 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!

Happy cat

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!

Kotlin (programming language) unit test Test-driven development

Opinions expressed by DZone contributors are their own.

Related

  • Mastering Unit Testing and Test-Driven Development in Java
  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing
  • Best Practices for Writing Unit Tests: A Comprehensive Guide
  • Exploring Unit Testing in Golang

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!