Testing Kotlin/JS with Kotest
Tags: kotlin, testing, javascript, kotest
Published: 2020-09-20 19:17:03

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](https://kotest.io/) to test against Kotlin/JS.

Firstly, I will assume you already have a project setup with javascript support. If not, then [follow this guide](https://kotlinlang.org/docs/reference/js-overview.html). 

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](https://www.ssa.gov/ssnumber/). 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:

```kotlin
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](https://www.lexjansen.com/nesug/nesug07/ap/ap19.pdf) 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:

```gradle
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](https://kotest.io/styles/). For Javascript we must choose one of the simpler specs - `FunSpec` in this case.

```kotlin
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](https://imgur.com/U5qUV8r.png)](https://imgur.com/U5qUV8r.png)

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

[![https://imgur.com/z9ocTC3.png](https://imgur.com/z9ocTC3.png)](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.


```kotlin
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](https://imgur.com/aWeItVd.png)](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://dog.ceo/api/breeds/image/random).

[![https://images.dog.ceo/breeds/leonberg/n02111129_844.jpg](https://images.dog.ceo/breeds/leonberg/n02111129_844.jpg)](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:

```kotlin
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:

```kotlin
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](https://imgur.com/rbvKRu2.png)](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.

```kotlin
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/A3D1DBH.png)](https://imgur.com/A3D1DBH.png)

[![https://imgur.com/JL05cIe.png](https://imgur.com/JL05cIe.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](https://github.com/kotest/kotest-examples-javascript).