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
Prepare your environment for multiplatform development. Check the list of necessary tools and update them to the latest versions if necessary.
Open the Kotlin Multiplatform wizard.
On the New project tab, ensure that the Android and iOS options are selected.
For iOS, choose the Do not share UI option. It is not necessary for this tutorial.
Click the Download button and unpack the resulting archive.
Write code
Launch Android Studio.
On the Welcome screen, click Open, or select File | Open in the editor.
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.
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:
In the
shared/src/commonMain/kotlin
directory, create a newcommon.example.search
directory.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.
In the
shared
directory, open thebuild.gradle.kts
file. Add a source set for testing the common code with a dependency on thekotlin.test
library:sourceSets { //... commonTest.dependencies { implementation(libs.kotlin.test) } }Once the dependency is added, you're prompted to resync the project. Click Sync Now to synchronize Gradle files:
The
commonTest
source set stores all common tests. Now you also need to create a directory with the same name in your project:Right-click the
shared/src
directory and select New | Directory. The IDE will present a list of options.Start typing the
commonTest/kotlin
path to narrow down the selection, then choose it from the list:
In the
commonTest/kotlin
directory, create a newcommon.example.search
package.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:
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:
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.
In the
commonMain/kotlin
directory, create a neworg.kmp.testing
directory.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" } }In the
commonTest/kotlin
directory, create a neworg.kmp.testing
package.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:
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
In the
androidMain/kotlin
directory, create a neworg.kmp.testing
package.In this package, create the
AndroidRuntime.kt
file and update it with the actual implementation of the expecteddetermineCurrentRuntime()
function:actual fun determineCurrentRuntime(): CurrentRuntime { val name = System.getProperty("java.vm.name") ?: "Android" val version = System.getProperty("java.version") return CurrentRuntime(name, version) }Create a directory for tests inside the
shared/src
directory:Right-click the
shared/src
directory and select New | Directory. The IDE will present a list of options.Start typing the
androidUnitTest/kotlin
path to narrow down the selection, then choose it from the list:
In the
kotlin
directory, create a neworg.kmp.testing
package.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
In the
iosMain/kotlin
directory, create a neworg.kmp.testing
directory.In this directory, create the
IOSRuntime.kt
file and update it with the actual implementation of the expecteddetermineCurrentRuntime()
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) }Create a new directory in the
shared/src
directory:Right-click the
shared/src
directory and select New | Directory. The IDE will present a list of options.Start typing the
iosTest/kotlin
path to narrow down the selection, then choose it from the list:
In the
iosTest/kotlin
directory, create a neworg.kmp.testing
directory.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:
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:
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:
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
andtestReleaseUnitTest
folders contain reports for both default Android build flavors. (Currently, Android test reports are not automatically merged with theallTests
report.)
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 thekotlin.test
API should only be used indirectly. Although theAsserter
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?
Explore the layout of multiplatform projects in Understand Multiplatform project structure.
Check out Kotest, another multiplatform testing framework provided by the Kotlin ecosystem. Kotest allows writing tests in a range of styles and supports complementary approaches to regular testing. These include data-driven and property-based testing.