DEV Community

loading...
Kotest

Testing Kotlin/JS with Kotest

sksamuel profile image Stephen Samuel ・5 min read

One of the areas Kotlin has focused on in recent releases is with multi-platform support. Kotlin code, the Kotlin standard library and other supported dependencies can be compiled into ES5 Javascript.

When targeting platforms that are not JVM based, our options for testing are reduced. The two best known Java testing frameworks (JUnit and TestNG) are JVM specific and won't work when we compile to Javascript. Instead, in this short article I will show how it is possible to use Kotest to test against Kotlin/JS.

Firstly, I will assume you already have a project setup with javascript support. If not, then follow this guide.

This means you will end up a structure that contains a main source folder and a test sources folder as is usual, with the koltin-js standard library added.

Let's add a basic Javascript function that validates US social security numbers. This will be the function that we want to test with Kotest.

Inside the source root, we create a file called ssn.kt that contains the following code:

import kotlin.js.RegExp

private val socialRegex = RegExp("^\\d{3}-\\d{2}-\\d{4}$")

fun validateSocial(ssn: String): Boolean {
   return socialRegex.test(ssn) && !ssn.contains("0") && !ssn.startsWith("666")
}

You'll notice we are using the kotlin.js.RegExp class. This is a Kotlin wrapper around the built in Javascript regular expression support. This is one of the nice touches about Kotlin's multi-platform support - it allows you to use platform specific functions, rather than going down lowest common denominator path of only allowing functions that work on all platforms.

There are some basic rules when it comes to validating SSN numbers. The general format is a 3 digit area code, a 2 digit group code, and a 4 digit serial number. Our function requires hyphens between these sets of numbers.

Additionally, no number can be zero, and 666 is not valid as an area code.

Setting up the build for Kotest

To begin testing, we need to add the appropriate Kotest dependencies. These are kotest-framework-engine-js and kotest-framework-api-js for the test framework, and kotest-assertions-core-js for the assertions library. Note that the assertions library is optional, in case you want to use assertions from another library.

If you are not using coroutines already, you will need to bring in the kotlinx-coroutines-core-js dependency in as well.

So, your build file should contain something similar to this:

kotlin {

   js {
      browser()
   }

   sourceSets {
      test {
         dependencies {
            implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.9'
            implementation 'io.kotest:kotest-assertions-core-js:4.2.5'
            implementation 'io.kotest:kotest-framework-api-js:4.2.5'
            implementation 'io.kotest:kotest-framework-engine-js:4.2.5'
         }
      }
   }
}

Writing a Test

To begin writing our tests, we create a Spec. A spec is the name Kotest uses for a class that contains test definitions. These are just regular classes that extend one of the Kotest spec styles. For Javascript we must choose one of the simpler specs - FunSpec in this case.

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

class SsnTest : FunSpec({

   test("a SSN should be invalid when it contains a zero in any position") {
      validateSocial("543-23-5013") shouldBe false
      validateSocial("043-23-5313") shouldBe false
      validateSocial("313-03-5310") shouldBe false
   }
})

Nested inside the constructor lambda block is our first test case. We check that if a zero appears inside the SSN then it is not valid.

Executing this test is just a matter of invoking the gradle test task, either at the command line, or from within intelliJ. The Kotlin/JS plugin for gradle will take care of downloading Mocha and/or Karma depending on whether the project is Browser and/or NodeJS based.

https://imgur.com/U5qUV8r.png

And, if all goes well, you should see something like this:

https://imgur.com/z9ocTC3.png

Let's round out our test suite, by adding tests to confirm the 666 rule, and that inputs must be in the required 3-2-4 format.

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

class SsnTest : FunSpec({

   test("a SSN should be invalid when it contains a zero in any position") {
      validateSocial("543-23-5013") shouldBe false
      validateSocial("043-23-5313") shouldBe false
      validateSocial("313-03-5310") shouldBe false
   }

   test("a SSN should be invalid when it starts with 666") {
      validateSocial("666-23-1234") shouldBe false
   }

   test("a SSN should be in the required format") {
      validateSocial("123-45-6789") shouldBe true
      validateSocial("123-45-678") shouldBe false
      validateSocial("12-45-6789") shouldBe false
      validateSocial("1234-56-678") shouldBe false
      validateSocial("123456789") shouldBe false
      validateSocial("123-456789") shouldBe false
      validateSocial("12345-6789") shouldBe false
      validateSocial("") shouldBe false
   }

})

https://imgur.com/aWeItVd.png

Coroutines and Promises

The interop of Kotest and Javascript doesn't stop there. Every test in Kotest is executed inside of a coroutine, so we can test suspend functions directly. Internally these are mapped to Javascript Promises.

We're going to test a function that uses the Ktor HTTP client to connect to a public API to fetch a picture of a dog.

https://images.dog.ceo/breeds/leonberg/n02111129_844.jpg

Firstly, add implementation 'io.ktor:ktor-client-js:1.4.0' to your main source. Then create a new file called http.kt which contains the following function:

import io.ktor.client.HttpClient
import io.ktor.client.engine.js.Js
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readText

private val client = HttpClient(Js)

suspend fun fetch(): Dog {
   val resp = client.get<HttpResponse>("https://dog.ceo/api/breeds/image/random")
   return JSON.parse<Dog>(resp.readText())
}

data class Dog(val message: String, val status: String)

This fetch function invokes the API, returning an instance of Dog, parsed from the returned Json. Notice the use of the JSON.parse function which is part of the Kotlin/JS std library. There is no error handling here since this is just a simple example of suspendable functions in action.

The next part is of course to write the test:

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.string.shouldEndWith

class DogTest : FunSpec({
   test("fetching a dog using JS promises") {
      fetch().message.shouldEndWith(".jpg")
   }
})

As you can see, testing a suspend function is as easy as testing the validateSocial function from earlier.

https://imgur.com/rbvKRu2.png

And just to prove that the test is indeed waiting for the response to complete, we can add an incorrect assertion to see the test fail.

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.string.shouldEndWith

class DogTest : FunSpec({
   test("fetching a dog using JS promises") {
      fetch().message.shouldEndWith(".jpg2")
   }
})

https://imgur.com/A3D1DBH.png

https://imgur.com/JL05cIe.png

That's how simple it is to use Kotest for Javascript tests. Kotlin/Javascript interop is still somewhat new and so there are some restrictions that you may run across.

The full project can be found here.

Discussion (0)

pic
Editor guide