PUBLIC OBJECT

Read a File in a Kotlin/Multiplatform Test

I like writing unit tests that read test cases from a data file:

In each case I get to leverage somebody else’s hard work to define expected inputs and outputs. Using an upstream project’s test file verbatim makes it easy to apply updates.

CWD in Kotlin/Multiplatform Tests

Doing this on Kotlin/JVM is very easy. Gradle’s test runner conveniently sets the test process’s current working directory (CWD) to the enclosing module’s directory:

val file = File("src/test/testdata/file.txt")
val testData = FileInputStream(file).use { inputStream ->
  readTestData(inputStream)
}

Kotlin/Native is trickier ’cause java.io.File doesn’t exist there. But I prefer Okio’s FileSystem anyway:

val path = "src/commonTest/testdata/file.txt".toPath()
return FileSystem.SYSTEM.read(path) {
  readTestData(this)
}

But soon our luck runs out.

On Kotlin/JS the test’s working directory is not the enclosing module’s directory; instead it’s far off in child of a build/ directory off the project root (KT-49125). We’ll also need the optional okio-nodefilesystem artifact to read files from JavaScript.

And when tests run on the iOS simulator, the working directory is completely unrelated to the project directory.

Set an Environment Variable Instead!

We can’t rely on the current working directory to be consistent so we’ll tell our test process where to look using an environment variable.

Paste this in your build.gradle.kts to set an environment variable for each kind of test:

allprojects {
  tasks.withType<KotlinJvmTest>().configureEach {
    environment("ZIPLINE_ROOT", rootDir)
  }

  tasks.withType<KotlinNativeTest>().configureEach {
    environment("SIMCTL_CHILD_ZIPLINE_ROOT", rootDir)
    environment("ZIPLINE_ROOT", rootDir)
  }

  tasks.withType<KotlinJsTest>().configureEach {
    environment("ZIPLINE_ROOT", rootDir.toString())
  }
}

Note the SIMCTL_CHILD_ prefix! It’s how I got the environment variable to propagate into the iOS simulator. This StackOverflow question tipped me off on it. I couldn’t find formal documentation for this thing.

Reading that Environment Variable

Just as we set our environment variable in a different way on each platform, we gotta do similarly to read it back.

In commonTest:

internal expect fun getEnv(name: String): String?

In jvmTest:

internal actual fun getEnv(name: String): String? =
  System.getenv(name)

In jsTest:

internal actual fun getEnv(name: String): String? = 
  js("globalThis.process.env[name]") as String?

And in each native target:

import platform.posix.getenv
import kotlinx.cinterop.toKString

internal actual fun getEnv(name: String): String? =
  getenv(name)?.toKString()

Finally the payoff goes in commonTest:

    private val ziplineRoot = getEnv("ZIPLINE_ROOT")!!.toPath()

Multiplatform Tests Are Rad

It takes some setup to read files and do environment variables in Kotlin/Multiplatform.

But once it’s done? I get to verify that my code does the same thing on every platform. I love it.