Kotlin Multiplatform Development Help

Test your multiplatform app − tutorial

In this tutorial, you'll learn how to create, configure, and run tests in Kotlin Multiplatform applications.

Tests for multiplatform projects can be divided into two categories:

  • Tests for common code. These tests can be run on any platform using any supported framework.

  • Tests for platform-specific code. These are essential to test platform-specific logic. They use a platform-specific framework and can benefit from its additional features, such as a richer API and a wider range of assertions.

Both categories are supported in multiplatform projects. This tutorial will first show you how to set up, create, and run unit tests for common code in a simple Kotlin Multiplatform project. Then, you'll work with a more complex example that requires tests both for common and platform-specific code.

Test a simple multiplatform project

Create your project

  1. Prepare your environment for multiplatform development. Check the list of necessary tools and update them to the latest versions if necessary.

  2. Open the Kotlin Multiplatform wizard.

  3. On the New project tab, ensure that the Android and iOS options are selected.

  4. For iOS, choose the Do not share UI option. It is not necessary for this tutorial.

  5. Click the Download button and unpack the resulting archive.

Kotlin Multiplatform wizard

Write code

  1. Launch Android Studio.

  2. On the Welcome screen, click Open, or select File | Open in the editor.

  3. Navigate to the unpacked project folder and then click Open.

    Android Studio detects that the folder contains a Gradle build file and opens the folder as a new project.

  4. The default view in Android Studio is optimized for Android development. To see the full file structure of the project, which is more convenient for multiplatform development, switch the view from Android to Project:

    Select the project view
  5. In the shared/src/commonMain/kotlin directory, create a new common.example.search directory.

  6. In this directory, create a Kotlin file, Grep.kt, and add the following function:

    fun grep(lines: List<String>, pattern: String, action: (String) -> Unit) { val regex = pattern.toRegex() lines.filter(regex::containsMatchIn) .forEach(action) }

    This function is designed to resemble the UNIX grep command. Here, the function takes lines of text, a pattern used as a regular expression, and a function that is invoked every time a line matches the pattern.

Add tests

Now, let's test the common code. An essential part will be a source set for common tests, which has the kotlin.test API library as a dependency.

  1. In the shared directory, open the build.gradle.kts file. Add a source set for testing the common code with a dependency on the kotlin.test library:

    sourceSets { //... commonTest.dependencies { implementation(libs.kotlin.test) } }
  2. Once the dependency is added, you're prompted to resync the project. Click Sync Now to synchronize Gradle files:

    Synchronize Gradle files
  3. The commonTest source set stores all common tests. Now you also need to create a directory with the same name in your project:

    1. Right-click the shared/src directory and select New | Directory. The IDE will present a list of options.

    2. Start typing the commonTest/kotlin path to narrow down the selection, then choose it from the list:

      Creating common test directory
  4. In the commonTest/kotlin directory, create a new common.example.search package.

  5. In this package, create the Grep.kt file and update it with the following unit test:

    import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals class GrepTest { companion object { val sampleData = listOf( "123 abc", "abc 123", "123 ABC", "ABC 123" ) } @Test fun shouldFindMatches() { val results = mutableListOf<String>() grep(sampleData, "[a-z]+") { results.add(it) } assertEquals(2, results.size) for (result in results) { assertContains(result, "abc") } } }

As you can see, imported annotations and assertions are neither platform- nor framework-specific. When you run this test later, a platform-specific framework will provide the test runner.

Explore the kotlin.test API

The kotlin.test library provides platform-agnostic annotations and assertions for you to use in your tests. Annotations, such as Test, map to those provided by the selected framework or their nearest equivalent.

Assertions are executed through an implementation of the Asserter interface. This interface defines the different checks commonly performed in testing. The API has a default implementation, but typically you will use a framework-specific implementation.

For example, the JUnit 4, JUnit 5, and TestNG frameworks are all supported on JVM. On Android, a call to assertEquals() might result in a call to asserter.assertEquals(), where the asserter object is an instance of JUnit4Asserter. On iOS, the default implementation of the Asserter type is used in conjunction with the Kotlin/Native test runner.

Run tests

You can execute the test by running:

  • The shouldFindMatches() test function using the Run icon in the gutter.

  • The test file using its context menu.

  • The GrepTest test class using the Run icon in the gutter.

There's also a handy ⌃ ⇧ F10/Ctrl+Shift+F10 shortcut. Regardless of the option you choose, you'll see a list of targets to run the test on:

Run test task

For the android option, tests are run using JUnit 4. For iosSimulatorArm64, the Kotlin compiler detects testing annotations and creates a test binary that is executed by Kotlin/Native's own test runner.

Here is an example of the output generated by a successful test run:

Test output

Work with more complex projects

Write tests for common code

You've already created a test for common code with the grep() function. Now, let's consider a more advanced common code test with the CurrentRuntime class. This class contains details of the platform on which the code is executed. For example, it might have the values "OpenJDK" and "17.0" for Android unit tests that run on a local JVM.

An instance of CurrentRuntime should be created with the name and version of the platform as strings, where the version is optional. When the version is present, you only need the number at the start of the string, if available.

  1. In the commonMain/kotlin directory, create a new org.kmp.testing directory.

  2. In this directory, create the CurrentRuntime.kt file and update it with the following implementation:

    class CurrentRuntime(val name: String, rawVersion: String?) { companion object { val versionRegex = Regex("^[0-9]+(\\.[0-9]+)?") } val version = parseVersion(rawVersion) override fun toString() = "$name version $version" private fun parseVersion(rawVersion: String?): String { val result = rawVersion?.let { versionRegex.find(it) } return result?.value ?: "unknown" } }
  3. In the commonTest/kotlin directory, create a new org.kmp.testing package.

  4. In this package, create the CurrentRuntimeTest.kt file and update it with the following platform and framework-agnostic test:

    import kotlin.test.Test import kotlin.test.assertEquals class CurrentRuntimeTest { @Test fun shouldDisplayDetails() { val runtime = CurrentRuntime("MyRuntime", "1.1") assertEquals("MyRuntime version 1.1", runtime.toString()) } @Test fun shouldHandleNullVersion() { val runtime = CurrentRuntime("MyRuntime", null) assertEquals("MyRuntime version unknown", runtime.toString()) } @Test fun shouldParseNumberFromVersionString() { val runtime = CurrentRuntime("MyRuntime", "1.2 Alpha Experimental") assertEquals("MyRuntime version 1.2", runtime.toString()) } @Test fun shouldHandleMissingVersion() { val runtime = CurrentRuntime("MyRuntime", "Alpha Experimental") assertEquals("MyRuntime version unknown", runtime.toString()) } }

You can run this test using any of the ways available in the IDE.

Add platform-specific tests

Now you have experience writing tests for common code, let's explore writing platform-specific tests for Android and iOS.

To create an instance of CurrentRuntime, declare a function in the common CurrentRuntime.kt file as follows:

expect fun determineCurrentRuntime(): CurrentRuntime

The function should have separate implementations for each supported platform. Otherwise, the build will fail. As well as implementing this function on each platform, you should provide tests. Let's create them for Android and iOS.

For Android

  1. In the androidMain/kotlin directory, create a new org.kmp.testing package.

  2. In this package, create the AndroidRuntime.kt file and update it with the actual implementation of the expected determineCurrentRuntime() function:

    actual fun determineCurrentRuntime(): CurrentRuntime { val name = System.getProperty("java.vm.name") ?: "Android" val version = System.getProperty("java.version") return CurrentRuntime(name, version) }
  3. Create a directory for tests inside the shared/src directory:

    1. Right-click the shared/src directory and select New | Directory. The IDE will present a list of options.

    2. Start typing the androidUnitTest/kotlin path to narrow down the selection, then choose it from the list:

    Creating Android test directory
  4. In the kotlin directory, create a new org.kmp.testing package.

  5. In this package, create the AndroidRuntimeTest.kt file and update it with the following Android test:

    import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals class AndroidRuntimeTest { @Test fun shouldDetectAndroid() { val runtime = determineCurrentRuntime() assertContains(runtime.name, "OpenJDK") assertEquals(runtime.version, "17.0") } }

It may seem strange that an Android-specific test is run on a local JVM. This is because these tests run as local unit tests on the current machine. As described in the Android Studio documentation, these tests differ from instrumented tests, which run on a device or an emulator.

You can add other types of tests to your project. To learn about instrumented tests, see this Touchlab guide.

For iOS

  1. In the iosMain/kotlin directory, create a new org.kmp.testing directory.

  2. In this directory, create the IOSRuntime.kt file and update it with the actual implementation of the expected determineCurrentRuntime() function:

    import kotlin.experimental.ExperimentalNativeApi import kotlin.native.Platform @OptIn(ExperimentalNativeApi::class) actual fun determineCurrentRuntime(): CurrentRuntime { val name = Platform.osFamily.name.lowercase() return CurrentRuntime(name, null) }
  3. Create a new directory in the shared/src directory:

    1. Right-click the shared/src directory and select New | Directory. The IDE will present a list of options.

    2. Start typing the iosTest/kotlin path to narrow down the selection, then choose it from the list:

    Creating iOS test directory
  4. In the iosTest/kotlin directory, create a new org.kmp.testing directory.

  5. In this directory, create the IOSRuntimeTest.kt file and update it with the following iOS test:

    package org.kmp.testing import kotlin.test.Test import kotlin.test.assertEquals class IOSRuntimeTest { @Test fun shouldDetectOS() { val runtime = determineCurrentRuntime() assertEquals(runtime.name, "ios") assertEquals(runtime.version, "unknown") } }

Run multiple tests and analyze reports

At this stage, you have the code for common, Android, and iOS implementations, as well as their tests. The directory structure in your project should look something like this:

Whole project structure

You can run individual tests from the context menu or use the shortcut. One more option is to use Gradle tasks. For example, if you run the allTests Gradle task, every test in your project will be run with the corresponding test runner:

Gradle test tasks

When you run tests, in addition to the output in your IDE, HTML reports are generated. You can find them in the shared/build/reports/tests directory:

HTML reports for multiplatform tests

Run the allTests task and examine the reports it generated:

  • The allTests/index.html file contains combined reports for common and iOS tests (iOS tests depend on common tests and are run after them).

  • The testDebugUnitTest and testReleaseUnitTest folders contain reports for both default Android build flavors. (Currently, Android test reports are not automatically merged with the allTests report.)

HTML report for multiplatform tests

Rules for using tests in multiplatform projects

You've now created, configured, and executed tests in Kotlin Multiplatform applications. When working with tests in your future projects, remember:

  • When writing tests for common code, use only multiplatform libraries, like kotlin.test. Add dependencies to the commonTest source set.

  • The Asserter type from the kotlin.test API should only be used indirectly. Although the Asserter instance is visible, you don't need to use it in your tests.

  • Always stay within the testing library API. Fortunately, the compiler and the IDE prevent you from using framework-specific functionality.

  • Although it doesn't matter which framework you use for running tests in commonTest, it's a good idea to run your tests with each framework you intend to use to check that your development environment is set up correctly.

  • When writing tests for platform-specific code, you can use the functionality of the corresponding framework, for example, annotations and extensions.

  • You can run tests both from the IDE and using Gradle tasks.

  • When you run tests, HTML test reports are generated automatically.

What's next?

Last modified: 25 September 2024