Kotlin Multiplatform Parameterized Tests and Grouping Using The Standard Kotlin Testing Framework
Tags: kotlin, testing, multiplatform
Published: 2023-04-16 10:42:40

---
title: Kotlin Multiplatform Parameterized Tests and Grouping Using The Standard Kotlin Testing Framework
published: true
date: 2022-07-11 08:00:52 UTC
tags: Kotlin,Testing,Multiplatform
canonical_url: https://akjaw.com/kotlin-multiplatform-parameterized-tests-and-grouping/
---

 ![Kotlin Multiplatform Parameterized Tests and Grouping Using The Standard Kotlin Testing Framework](https://akjaw.com/content/images/2023/01/hero.png)

This article was originally posted on [my blog](https://akjaw.com/kotlin-multiplatform-parameterized-tests-and-grouping/), for better viewing experience try it out there.

I covered the [Kotlin Multiplatform Testing ecosystem](https://akjaw.com/testing-on-kotlin-multiplatform-and-strategy-to-speed-up-development/) before. But I'll try to summarize it briefly here.

Coming from Android, we're a little spoiled when it comes to testing frameworks. The default is [JUnit 4](https://junit.org/junit4/?ref=akjaw.com), but we also have [JUnit 5](https://junit.org/junit5/?ref=akjaw.com) which are very mature frameworks. On Kotlin Multiplatform, we can't leverage JUnit features like parameterized tests and nested test classes. By default, Kotlin Multiplatorm uses the [kotlin.test](https://junit.org/junit5/?ref=akjaw.com) framework, which unfortunately is not as feature rich as JUnit.

There are alternatives frameworks like the [Kotest](https://kotest.io/docs/framework/framework.html?ref=akjaw.com), which does offer parameterized tests, but has some limitations with Kotlin Multiplatform like the Kotest plugin not being able to run tests from commonTest, so they always need to be executed from the command line.

However, in this article I'd like to focus on the standard testing framework, because it comes out of the box with Kotlin and has good IDE support, and also has a similar API as JUnit.

## Parameterized tests

I tried to keep the test examples simple and self-explanatory, the last one is more complicated, but I hope it's still easy to follow.

### Parsing a string to an Enum

We have an Enum class representing Card (Suite), and an extension function that parses a string and returns the corresponding card or null.

``` kotlin
enum class Card {
    HEARTS,
    DIAMONDS,
    SPADES,
    CLUBS
}

fun String.parseToCard(): Card? =
    when (this) {
        "Hearts" -> Card.HEARTS
        "Diamonds" -> Card.DIAMONDS
        "Spades" -> Card.SPADES
        "Clubs" -> Card.CLUBS
        else -> null
    }

class CardParsingTest {

    @Test
    fun `'Hearts' is a card`() = validTestCase("Hearts", Card.HEARTS)

    @Test
    fun `'Diamonds' is a card`() = validTestCase("Diamonds", Card.DIAMONDS)

    @Test
    fun `'Spades' is a card`() = validTestCase("Spades", Card.SPADES)

    @Test
    fun `'Clubs' is a card`() = validTestCase("Clubs", Card.CLUBS)

    @Test
    fun `'Heart' is not a card`() = invalidTestCase("Heart")

    @Test
    fun `'Diamons' is not a card`() = invalidTestCase("Diamons")

    @Test
    fun `'Spade' is not a card`() = invalidTestCase("Spade")

    @Test
    fun `'Club' is not a card`() = invalidTestCase("Club")

    private fun validTestCase(stringCard: String, expectedCard: Card) {
        assertEquals(actual = stringCard.parseToCard(), expected = expectedCard)
    }

    private fun invalidTestCase(stringCard: String) {
        assertNull(actual = stringCard.parseToCard())
    }
}

```

There are two helper functions **validTestCase** and **invalidTestCase** , depending on the expected outcome the respective "TestCase" is called. Having one function could also work in this case, but I personally think that having this distinction makes the test class more readable.

### Formatting names

We have a Person class which we want to format depending on the available data.

```kotlin
data class Person(
    val firstName: String,
    val lastName: String,
    val secondName: String? = null,
    val secondLastName: String? = null
)

fun Person.formatToString(): String {
    val secondName = if (secondName == null) "" else " $secondName"
    val secondLastName = if (secondLastName == null) "" else "-$secondLastName"
   return "$firstName$secondName $lastName$secondLastName"
}

class PersonFormattingTest {

    @Test
    fun `Full name is correctly formatted`() = testCase(
        Person(firstName = "John", lastName = "Doe"),
        "John Doe"
    )

    @Test
    fun `Full name with second name is correctly formatted`() = testCase(
        Person(firstName = "John", secondName = "Bob", lastName = "Doe"),
        "John Bob Doe"
    )

    @Test
    fun `Full name with second last name is correctly formatted`() = testCase(
        Person(firstName = "John", lastName = "Doe", secondLastName = "Dilly"),
        "John Doe-Dilly"
    )

    @Test
    fun `Full name with second name and last name is correctly formatted`() =
        testCase(
            Person(
                firstName = "John",
                secondName = "Bob",
                lastName = "Doe",
                secondLastName = "Dilly"
            ),
            "John Bob Doe-Dilly"
        )

    private fun testCase(person: Person, expectedString: String) {
        assertEquals(actual = person.formatToString(), expected = expectedString)
    }
}
```

### Searching through keywords

We have a UseCase which filters available keywords for a given query. The keywords come from a repository, which is replaced with a test double during testing.

```kotlin
interface KeywordRepository {
    suspend fun getKeywords(): List<String>
}

class FakeDelayingKeywordRepository : KeywordRepository {
    var keywords: List<String> = emptyList()

    override suspend fun getKeywords(): List<String> {
        delay(500) // Simulate some I/O operation
        return keywords
    }
}

class SearchForKeyword(
    private val keywordRepository: KeywordRepository,
    private val dispatcher: CoroutineDispatcher,
) {

    sealed class SearchResult {
        object Empty : SearchResult()
        object InvalidQuery : SearchResult()
        object Error : SearchResult()
        data class Success(val keywords: List<String>) : SearchResult()
    }

    suspend fun execute(query: String): SearchResult = withContext(dispatcher) {
        if (query.count() < 3) return@withContext InvalidQuery

        val keywords = keywordRepository.getKeywords()

        if (keywords.isEmpty()) return@withContext Error

        val matches = keywords.filter { keyword -> keyword.contains(query) }
        if (matches.isEmpty()) {
            Empty
        } else {
            Success(matches)
        }
    }
}

class SearchTest {

    private lateinit var fakeKeywordRepository: FakeDelayingKeywordRepository
    private lateinit var dispatcher: TestDispatcher
    private lateinit var systemUnderTest: SearchForKeyword

    @BeforeTest
    fun setUp() {
        fakeKeywordRepository = FakeDelayingKeywordRepository()
        dispatcher = UnconfinedTestDispatcher()
        systemUnderTest = SearchForKeyword(fakeKeywordRepository, dispatcher)
    }

    @Test
    fun `When query is empty then InvalidQuery is returned`() =
        testCase(query = "", expectedResult = InvalidQuery)

    @Test
    fun `When query has 2 characters then InvalidQuery is returned`() =
        testCase(query = "fi", expectedResult = InvalidQuery)

    @Test
    fun `When query valid the expected Success result is returned`() =
        testCase(
            query = "fir",
            keywords = listOf("first", "second", "Fire", "sound"),
            expectedResult = Success(listOf("first")),
        )

    @Test
    fun `When query valid but does not match any keywords then Empty is returned`() =
        testCase(
            query = "asd",
            keywords = listOf("second", "Fire", "sound"),
            expectedResult = Empty,
        )

    @Test
    fun `When query valid but keyword repository is empty then Error is returned`() =
        testCase(
            query = "asd",
            keywords = emptyList(),
            expectedResult = Error,
        )

    private fun testCase(
        query: String,
        expectedResult: SearchForKeyword.SearchResult,
        keywords: List<String> = emptyList()
    ) =
        runTest(dispatcher) {
            fakeKeywordRepository.keywords = keywords

            val result = systemUnderTest.execute(query)

            assertEquals(actual = result, expected = expectedResult)
        }
}

```

This test is more complicated compared to the last ones, because it requires some _Arrangement,_ before the test _Action_ and _Assertions_. It also resembles more of a "real" thing that happens during app development.

The added benefit of having a "TestCase" function is that the systemUnderTest could also be created inside of it, without repeating the same boilerplate in every test. This can be helpful if a Test Double or System Under Test take in different constructor parameters depending on the test.

### Grouping tests in Kotlin Multiplatform

Another topic I talked about in the introduction was JUnit 5 nested classes used for grouping test cases. This is not available in the standard Kotlin testing library, but there is another way of grouping test cases.

Instead of nested classes, they could be normal classes in different files. The problem with this is that the System Under Test creation has to be repeated in every test class.

To illustrate this problem, I refactored the Search UseCase to be more complicated by extracting out some classes:

```kotlin
class QueryValidator {

    fun isValid(query: String): Boolean = query.count() < 3
}

class KeywordFilter {

    fun execute(query: String, keywords: List<String>): List<String> {
        return keywords.filter { keyword -> keyword.contains(query) }
    }
}
```

The constructor of the UseCase is now the following:

```kotlin
class SearchForKeyword(
    private val keywordRepository: KeywordRepository,
    private val queryValidator: QueryValidator,
    private val keywordFilter: KeywordFilter,
    private val dispatcher: CoroutineDispatcher,
)
```

### System Under Test creation

The instantiation of the system under test is now more involved:

```kotlin
private lateinit var keywordRepository: FakeDelayingKeywordRepository
private lateinit var dispatcher: TestDispatcher
private lateinit var systemUnderTest: SearchForKeyword

@BeforeTest
fun setUp() {
    keywordRepository = FakeDelayingKeywordRepository()
    dispatcher = UnconfinedTestDispatcher()
    systemUnderTest = SearchForKeyword(
        keywordRepository,
        QueryValidator(),
        KeywordFilter(),
        dispatcher
    )
}
```

Repeating this in every test class will cause the test class to be more complex and harder to maintain every time a constructor parameters changes. To help with that, a helper [Object Mother](https://martinfowler.com/bliki/ObjectMother.html?ref=akjaw.com) function can be created:

```kotlin
fun createSearchForKeyword(
    dispatcher: CoroutineDispatcher,
    keywordRepository: KeywordRepository = FakeDelayingKeywordRepository(),
): SearchForKeyword {
    return SearchForKeyword(
        keywordRepository,
        QueryValidator(),
        KeywordFilter(),
        dispatcher
    )
}

```

The QueryValidator and KeywordFilter are implementation details from the perspective of the test, so their creation is hidden here. However, the dispatcher and repository are used inside the test class, so they are created in the test class and passed in here.

### Different Test classes

The test cases for the Search UseCase remain the same because the behavior did not change, however the test class will be split into two: **SearchInvalidKeywordTest** and **SearchValidKeywordTest**

```kotlin
class SearchInvalidKeywordTest {

    private lateinit var dispatcher: TestDispatcher
    private lateinit var systemUnderTest: SearchForKeyword

    @BeforeTest
    fun setUp() {
        dispatcher = UnconfinedTestDispatcher()
        systemUnderTest = createSearchForKeyword(dispatcher)
    }

    @Test
    fun `When query is empty then InvalidQuery is returned`() =
        testCase(query = "")

    @Test
    fun `When query has 2 characters then InvalidQuery is returned`() =
        testCase(query = "fi")

    private fun testCase(query: String) =
        runTest(dispatcher) {
            val result = systemUnderTest.execute(query)

            assertEquals(actual = result, expected = InvalidQuery)
        }
}

```

The keyword repository is removed from here, because it is not used by the tests. The default parameter in **createSearchForKeyword** function creates the repository.

```kotlin
class SearchValidKeywordTest {

    private lateinit var fakeKeywordRepository: FakeDelayingKeywordRepository
    private lateinit var dispatcher: TestDispatcher
    private lateinit var systemUnderTest: SearchForKeyword

    @BeforeTest
    fun setUp() {
        fakeKeywordRepository = FakeDelayingKeywordRepository()
        dispatcher = UnconfinedTestDispatcher()
        systemUnderTest = createSearchForKeyword(dispatcher, fakeKeywordRepository)
    }

    @Test
    fun `When query valid the expected Success result is returned`() =
        testCase(
            query = "fir",
            expectedResult = Success(listOf("first")),
            keywords = listOf("first", "second", "Fire", "sound")
        )

    @Test
    fun `When query valid but does not match any keywords then Empty is returned`() =
        testCase(
            query = "asd",
            expectedResult = Empty,
            keywords = listOf("second", "Fire", "sound")
        )

    @Test
    fun `When query valid but keyword repository is empty then Error is returned`() =
        testCase(
            query = "asd",
            expectedResult = Error,
            keywords = emptyList()
        )

    private fun testCase(
        query: String,
        expectedResult: SearchForKeyword.SearchResult,
        keywords: List<String>
    ) =
        runTest(dispatcher) {
            fakeKeywordRepository.keywords = keywords

            val result = systemUnderTest.execute(query)

            assertEquals(actual = result, expected = expectedResult)
        }
}

```

In this test class, the keyword repository is a property because it is used to _Arrange_ the correct return value before calling the System Under Test.

This method of grouping tests, is more verbose and complicated which for simple cases might be an overkill, but when the test cases keep growing (like when there are a lot of edge cases), this grouping is definitely better than having one 600+ line test class.

## Summary

Although the standard Kotlin testing framework lacks some JUnit features, we are able to implement them by hand. They are more verbose, but we have to work with what we got.

If you have your own strategies for writing Kotlin Multiplatform tests, feel free to share them in the comments 😁