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

Enterprise AI Trend Report: Gain insights on ethical AI, MLOps, generative AI, large language models, and much more.

2024 Cloud survey: Share your insights on microservices, containers, K8s, CI/CD, and DevOps (+ enter a $750 raffle!) for our Trend Reports.

PostgreSQL: Learn about the open-source RDBMS' advanced capabilities, core components, common commands and functions, and general DBA tasks.

AI Automation Essentials. Check out the latest Refcard on all things AI automation, including model training, data security, and more.

Related

  • Why "Polyglot Programming" or "Do It Yourself Programming Languages" or "Language Oriented Programming" sucks?
  • Test-Driven Development With The oclif Testing Library: Part One
  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing
  • Best Practices for Writing Unit Tests: A Comprehensive Guide

Trending

  • Build Your Own Programming Language
  • Elevate Your Terminal Game: Hacks for a Productive Workspace
  • Enhancing Performance With Amazon Elasticache Redis: In-Depth Insights Into Cluster and Non-Cluster Modes
  • Understanding Kernel Monitoring in Windows and Linux
  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?

In this article, We'll learn how to use Kotlin-TDD to implement the TDD technique. We'll also learn how to improve the readability of our test and reuse the same process

By 
Ludovic Dorival user avatar
Ludovic Dorival
·
Dec. 24, 21 · Tutorial
Like (1)
Save
Tweet
Share
6.1K 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, me included,  I considered testing as a luxury for developers since this is the first task we sacrifice when we try to build a Minimum Viable Product.

But we are wrong to do that because the tests:

  • Reduce possibles production regressions 
    • What saves us time when troubleshooting
  • Validate functional design
    • Make sure we respect the target solution
  • Increase the overall quality of your program
    • The program is more reliable and we are more confident to push a new version on production

Well, ok we agree to do more tests but TDD... is it something applicable in daily practice?

TDD as Test-Driven Development 

Test-driven development refers to a programming style favoring testing before writing production code. Basically, you have to follow the steps including:

  • Write Test
  • Run Test (the test should fail)
  • Write Production Code
  • Run Test (the test should pass)
  • Refactor until the code is conform
  • Repeat, “accumulating” unit tests over time

If you want to know more about TDD, there are a tons of articles related to this topic that explained in detail the benefits of TDD (for example What is Test Driven Development (TDD)?).

There are many ways to implement TDD  like unit testing, feature testing, etc. However, in practice, this is not really natural and we have to struggle with ourselves to really use it.

Kotlin for TDD?

Kotlin has many advantages compared to Java for writing TDD. Particularly:

  • Kotlin reduces java boilerplate thanks to function extension which can improve your test readability.
  • Kotlin supports the infix notation allowing to write a code in more natural English.
Kotlin
 
val canOrder = user.isAuthenticated and user.hasACreditCard


  • Kotlin supports backticks function name, really convenient to write tests
Kotlin
 
class MyTestCase {
     @Test fun `ensure everything works`() { /*...*/ }

     @Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}


  • Kotlin is interoperable with Java meaning that you can use Kotlin to write your tests whereas your production code is written in Java.

That's why I consider Kotlin the best language to implement TDD. It came up to my mind 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 by 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 thanks to 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:

Groovy
 
testCompile "io.github.ludorival:kotlin-tdd:1.1.2"


With Maven:

XML
 
<dependency>
<groupId>io.github.ludorival</groupId>
<artifactId>kotlin-tdd</artifactId>
<version>1.1.2</version>
<scope>test</scope>
</dependency>


2 - Write Your Interface Action

Inside of each step, you can access a field named action which is the instance you will pass to your TDD configuration (see in the next step). It has no real use in the library but it allows you to use a common instance throughout your tests. Here we will use it to expose different possible actions in the application:

Kotlin
 
// src/test/kotlin/com/example/kotlintdd/Action.kt
package com.example.kotlintdd

interface Action {
  
  fun createTodoList(): TodoList
  
  fun createItem(name: String): Item
  
  fun addItem(todoList: TodoList, item: Item): TodoList
}


Don't worry if you see compilation errors, it's part of the TDD process ;). Indeed compilation failure is considered as a test failure in TDD.

3 - Configure an Instance of Givenwhenthen To Use in All Our Unit Tests

Kotlin
 
// src/test/kotlin/com/example/kotlintdd/UnitTest.kt
package com.example.kotlintdd
  
import io.github.ludorival.kotlintdd.GWTContext // it is an alias of GivenWhenThen.Context
import io.github.ludorival.kotlintdd.GivenWhenThen
  
object UnitTest : GivenWhenThen<Action> {
    override val action: Action = object: Action {
        override fun createTodoList() = TodoList()
        override fun createItem(name: String) = Item(name)
      	override fun addItem(todoList: TodoList, item: Item) = todoList.add(item)
    }
}
// defines the entrypoint on file-level to be automatically recognized by your IDE
fun <R> given(block: GWTContext<Action, Unit>.() -> R) = UnitTest.given(block)
fun <R> `when`(block: GWTContext<Action, Unit>.() -> R) = UnitTest.`when`(block)


4 - 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. Here we will create a file that will host this DSL.

Kotlin
 
// src/test/kotlin/com/example/kotlintdd/DSL.kt
package com.example.kotlintdd

import io.github.ludorival.kotlintdd.GWTContext

val GWTContext<*, *>.`a todo list` get() = action.createTodoList()

fun GWTContext<*, *>.`an item`(name: String) = action.createItem(name)

val GWTContext<*, *>.`I add the last item into my todo list` get() = 
    action.addItem(first<TodoList>(), last<Item>())

val GWTContext<*, *>.`I expect this item is present in my todo list` get() =
    assertTrue(first<TodoList>().contains(last<Item>()))


Here we are extending the GivenWhenThen.Context by adding our own custom DSL. Notice that you have access to the action instance which allows us to call our respective action.

Check at lines 11 and 14 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.

Note that the TodoList, Item can be removed at line 11 since it is inferred, given action.addItem(first(), last()).

You can see all available functions in the documentation.

5 - 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. Don't worry this is part of the TDD process.

 7 - Write Production Code

Now it is time to make the test green.

  • 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 Test as well.

Let's assume that you have a Spring application where the TodoList and Item are saved in a database. The creation and the update should be done through the database for those 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 Action interface:

Kotlin
 
= restTemplate .exchange("$url/todo", HttpMethod.POST, HttpEntity(TodoList()), TodoList::class.java) return response.body!! } override fun createItem(name: String): Item { val response: ResponseEntity = 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 = restTemplate .exchange( "$url/todo/${todoList.id}/add", HttpMethod.PUT, HttpEntity(item), TodoList::class.java ) return response.body!! } } " data-lang="text/x-kotlin">
// src/test/kotlin/com/example/kotlintdd/acceptance/RestActions.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 RestAction : Action {

    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 setup 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

object AcceptanceTest: GivenWhenThen<Action> {
    override val action: Action = RestAction()

}
fun <R> given(block: GWTContext<Action, Unit>.() -> 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 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 TDD technique. We see that by writing a custom DSL, we improve the readability of our test and it becomes easy to understand a test even after several months. Then we saw that we can reuse the same process for the Acceptance test without changing the way we build our test.

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 Domain-Specific Language Database Test-driven development

Opinions expressed by DZone contributors are their own.

Related

  • Why "Polyglot Programming" or "Do It Yourself Programming Languages" or "Language Oriented Programming" sucks?
  • Test-Driven Development With The oclif Testing Library: Part One
  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing
  • Best Practices for Writing Unit Tests: A Comprehensive Guide

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send 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: