Unit Testing using Kotest in Kotlin Multiplatform Mobile (KMM)

Karim Reda
arconsis
Published in
8 min readSep 6, 2023

Introduction

Unit testing is a crucial practice in software development, ensuring the reliability and quality of applications. Kotlin Multiplatform Mobile (KMM) has gained popularity as a powerful framework for building mobile apps that share code across platforms. When it comes to unit testing in KMM, Kotest emerges as a popular and feature-rich testing framework. In this article, we will explore how to perform unit testing using Kotest in Kotlin Multiplatform Mobile projects, along with a code example. We will cover the key features of Kotest, demonstrate its usage for testing shared and platform-specific code, and discuss best practices for effective unit testing.

If you’re unfamiliar with Unit testing, we encourage you to refer to our article on Unit testing in Kotlin Multiplatform Mobile (KMM). It provides more insights into Unit testing, its advantages, and its best practices.

What is Kotest?

Kotest is an open-source testing framework specifically designed for Kotlin. It offers a comprehensive set of features that simplify writing expressive and concise tests. With its intuitive syntax and various testing styles, including behavior-driven development (BDD), property-based testing, and more, Kotest makes unit testing in Kotlin Multiplatform Mobile projects seamless and efficient.

Different Test Styles in Kotest

Kotest provides several test styles that you can choose from based on your preference and requirements. Here are some of the commonly used test styles in Kotest:

  1. FunSpec: This test style allows you to define tests using nested blocks of code. It is useful for organizing tests in a structured and descriptive manner.
  2. DescribeSpec: Similar to FunSpec, this test style enables you to group tests using nested blocks, but with a more behavior-driven approach. It is well-suited for describing the behavior or functionality being tested.
  3. FeatureSpec: This test style focuses on describing the features or usecases of your code. It allows you to define tests using the feature, scenario, and given/when/then keywords to structure your tests.
  4. StringSpec: This test style allows you to write tests as plain strings. It is useful when you prefer a more concise and expressive syntax.
  5. BehaviorSpec: This test style emphasizes behavior-driven testing and provides a given-when-thenstructure to describe the behavior and expected outcomes of your code.
  6. WordSpec: This test style provides a highly readable and structured way to write tests by using phrases and words to define the behavior and expectations of your code.
  7. ExpectSpec: This test style is designed to write tests using the expect function and assertions. It is particularly useful when you want to emphasize the expected outcomes of your code.

Setting Up Kotest in KMM Projects

To incorporate Kotest into your KMM project, follow the steps outlined below.

Step 1: Set up your KMM project

Create a new KMM project using the instructions provided by JetBrains. Ensure you have the necessary project structure and configuration for iOS and Android platforms.

If you’re unfamiliar with the process of creating a KMM project, we encourage you to refer to our article on getting started with Kotlin Multiplatform Mobile (KMM). It provides detailed guidance on the topic.

Step 2: Add Kotest to your project dependencies

In your KMM project, add the Kotest dependency to your shared module’s build.gradle file.

kotlin{
sourceSets{
...

val commonTest by getting {
dependencies {
implementation("io.kotest:kotest-framework-engine:$version")
implementation("io.kotest:kotest-assertions-core:$version")
}
}
}
}

Replace <version> with the latest version of the Kotest library. After syncing the project, you are ready to start writing unit tests using Kotest.

Step 3: Writing Unit Tests with Kotest in KMM

Let’s dive into writing unit tests using Kotest in Kotlin Multiplatform Mobile projects. We will demonstrate the key features of Kotest with a code example that tests a simple shared function.

In the following example, we have a Calculator class representing a simple calculator with add and subtract operations.

// Shared code: Example.kt

class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}

fun subtract(a: Int, b: Int): Int {
return a - b
}

// Other calculator operations...
}

1. DescribeSpec Style:
We create a corresponding test class called CalculatorTest using the Kotest’s DescribeSpecstyle.

Within the DescribeSpec block, we use the describe function to group related test cases. Inside each describe block, we use the it function to define individual test cases.

In this example, Each test case verifies a specific behavior of the add function and uses Kotest’s shouldBe assertion to validate the expected results.

Next, we create a separate describe block for the subtract function since it tests a different method and should not be grouped together with the add function.

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe

class CalculatorTest : DescribeSpec({

val calculator = Calculator()

beforeTest {
// Any setup logic before each test case
}

afterTest {
// Any cleanup logic after each test case
}

describe("Addition") {
it("should return the correct sum of two numbers") {
val result = calculator.add(2, 3)
result shouldBe 5
}

it("should return the number when adding zero to the number") {
val result = calculator.add(2, 0)
result shouldBe 2
}
}

describe("Subtraction") {
it("should return the correct difference between two numbers") {
val result = calculator.subtract(5, 3)
result shouldBe 2
}
}

// More test cases...

})

The describeSpec test style provides a more behavior-driven approach to writing tests. It allows you to describe the behavior or functionality being tested, providing a clear and readable structure to your test suite.

2. FunSpec Style:
Within the FunSpec block, we use the context function to group related test cases. Inside each context block, we use the test function to define individual test cases.

In this example, Each test case verifies a specific behavior of the add function and uses Kotest’s shouldBe assertion to validate the expected results.

Next, we create a separate context block for the subtract function since it tests a different method and should not be grouped together with the add function.

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class CalculatorTest : FunSpec({

val calculator = Calculator()

beforeTest {
// Any setup logic before each test case
}

afterTest {
// Any cleanup logic after each test case
}

context("Addition") {
test("should return the correct sum of two numbers") {
val result = calculator.add(2, 3)
result shouldBe 5
}

test("should return the number when adding zero to the number") {
val result = calculator.add(2, 0)
result shouldBe 2
}
}

context("Subtraction") {
test("should return the correct difference between two numbers") {
val result = calculator.subtract(5, 3)
result shouldBe 2
}
}

// More test cases...

})

By using the funSpec test style, you can structure your tests in a descriptive and organized manner, making it easier to understand and maintain the test suite.

3. BehaviorSpec Style:
Within the BehaviorSpec block, we use the given, when, and then functions to define the behavior of the calculator’s add and subtract methods.

Each test case is organized into a given-when-then structure. We specify the initial state or context in the given block, the action or operation in the when block, and the expected outcome or behavior in the then block.

The result variable captures the result of the operation, and the shouldBe matcher asserts the expected outcome.

import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe

class CalculatorTest : BehaviorSpec({

val calculator = Calculator()

given("an addition operation") {
`when`("adding two numbers") {
val result = calculator.add(2, 3)
then("it should return the correct sum") {
result shouldBe 5
}
}
}

given("a subtraction operation") {
`when`("subtracting two numbers") {
val result = calculator.subtract(5, 3)
then("it should return the correct difference") {
result shouldBe 2
}
}
}

// More test cases...

})

The behaviorSpec test style provides a more behavior-driven approach to writing tests. It allows you to describe the behavior or scenario being tested, providing a clear and readable structure to your test suite.

4. FeatureSpec Style:
Within the featureSpec block, we use the feature function to group related test cases. Inside each feature block, we use the scenario function to define individual test cases.

The result variable captures the result of the operation, and the shouldBe matcher asserts the expected outcome.

import io.kotest.core.spec.style.FeatureSpec
import io.kotest.matchers.shouldBe

class CalculatorTest : FeatureSpec({

val calculator = Calculator()

feature("Addition") {
scenario("should return the correct sum of two numbers") {
val result = calculator.add(2, 3)
result shouldBe 5
}
}

feature("Subtraction") {
scenario("should return the correct difference between two numbers") {
val result = calculator.subtract(5, 3)
result shouldBe 2
}
}

// More test cases...

})

The featureSpec test style provides a way to structure your tests around the features or use cases of your code, making it easier to understand and maintain the test suite.

Step 4: Running Unit Tests

To run the unit tests, you can use your IDE’s testing support or execute the tests from the command line using Gradle. Simply run the following command:

./gradlew test

Kotest will execute the tests, and you will see the test results and any failures or errors in the output.

Exploring Key Features of Kotest for Unit Testing in KMM

Kotest offers a rich set of features that simplify the process of writing unit tests. Let’s explore some of its key features and demonstrate their usage in KMM projects:

  1. Test Styles: Kotest provides multiple test styles, including funSpec, describeSpec, behaviorSpec, and more. These styles allow you to structure your tests based on your preferences and requirements. For example, you can use the funSpec style to write traditional test functions or the behaviorSpecstyle for behavior-driven development.
  2. Assertions: Kotest offers a comprehensive set of built-in assertions to validate test outcomes. These assertions include should, shouldNot, shouldBe, shouldNotBe, and many more. You can use these assertions to compare values, check conditions, and ensure the expected behavior of your code.
  3. Asynchronous Testing: KMM applications often involve asynchronous operations. Kotest provides seamless support for testing asynchronous code using suspending functions, coroutines, or other asynchronous constructs. You can use shouldSuspend and other utilities to handle asynchronous operations in your tests.
  4. Table Testing: Kotest simplifies table-based testing, allowing you to define multiple input-output combinations within a single test. This feature is especially useful for testing edge cases and validating different scenarios. You can use the forAll function along with property-based testing to define the input space and expected outcomes succinctly.
  5. Tagging and Filtering: Kotest allows you to tag your tests and selectively run them based on these tags. This feature is helpful when you want to execute specific subsets of tests or group them based on different criteria, such as performance, functionality, or priority. You can use tags to organize and execute tests more efficiently.

To maximize the effectiveness of unit testing with Kotest in Kotlin Multiplatform Mobile projects, it is crucial to adhere to best practices for unit testing in KMM. For a deeper understanding of unit testing, its benefits, and best practices, we invite you to explore our comprehensive article on Unit Testing in Kotlin Multiplatform Mobile (KMM)

Conclusion

Unit testing is a crucial part of the software development process, and Kotlin Multiplatform Mobile (KMM) projects are no exception. Kotest, with its rich features and expressive syntax, provides an excellent choice for performing unit tests in KMM applications. By leveraging the capabilities of Kotest and following best practices, developers can ensure the quality, reliability, and maintainability of their KMM codebase. So, embrace Kotest for unit testing in KMM and deliver robust and high-quality mobile apps across platforms.

--

--