How to unit-test Kotlin-JS code with coroutines?
Asked Answered
H

3

11

I've created a multi-platform Kotlin project (JVM & JS), declared an expected class and implemented it:

// Common module:
expect class Request(/* ... */) {
    suspend fun loadText(): String
}

// JS implementation:
actual class Request actual constructor(/* ... */) {
    actual suspend fun loadText(): String = suspendCoroutine { continuation ->
        // ...
    }
}

Now I'm trying to make a unit test using kotlin.test, and for the JVM platform I simply use runBlocking like this:

@Test
fun sampleTest() {
    val req = Request(/* ... */)
    runBlocking { assertEquals( /* ... */ , req.loadText()) }
}

How can I reproduce similar functionality on the JS platform, if there is no runBlocking?

Haggard answered 23/12, 2017 at 22:14 Comment(0)
G
5

Mb it's late, but there are open issue for adding possibility to use suspend functions in js-tests (there this function will transparent convert to promise)

Workaround:

One can define in common code:

expect fun runTest(block: suspend () -> Unit)

that is implemented in JVM with

actual fun runTest(block: suspend () -> Unit) = runBlocking { block() }

and in JS with

actual fun runTest(block: suspend () -> Unit): dynamic = promise { block() } 
Glue answered 10/5, 2018 at 10:58 Comment(3)
I think you have a typo in the code, but the idea seems fine, thanks!Haggard
I'm struggling to get the JS one to work, what exactly was the typo?Ingenuity
This works fine, but since all tests are started concurrently (I think?), when you have many slow tests they exceed the two seconds timeout. Is there any way to modify this so there's no timeout issues?Pamper
L
2

TL;DR

  1. On JS one can use GlobalScope.promise { ... }.
  2. But for most use cases the best option is probably to use runTest { ... } (from kotlinx-coroutines-test), which is cross-platform, and has some other benefits over runBlocking { ... } and GlobalScope.promise { ... } as well.

Full answer

I'm not sure what things were like when the question was originally posted, but nowadays the standard, cross-platform way to run tests that use suspend functions is to use runTest { ... } (from kotlinx-coroutines-test).

Note that in addition to running on all platforms, this also includes some other features, such as skipping delays (with the ability to mock the passage of time).

If for any reason (which is not typical, but might sometimes be the case) it is actually desirable to run the code in the test as it runs in production (including actual delays), then runBlocking { ... } can be used on JVM and Native, and GlobalScope.promise { ... } on JS. If going for this option, it might be convenient to define a single function signature which uses runBlocking on JVM and Native, and GlobalScope.promise on JS, e.g.:

// Common:
expect fun runTest(block: suspend CoroutineScope.() -> Unit)

// JS:
@OptIn(DelicateCoroutinesApi::class)
actual fun runTest(block: suspend CoroutineScope.() -> Unit): dynamic = GlobalScope.promise(block=block)

// JVM, Native:
actual fun runTest(block: suspend CoroutineScope.() -> Unit): Unit = runBlocking(block=block)
Letta answered 1/9, 2022 at 18:4 Comment(0)
Y
0

I was able to make the following work:

expect fun coTest(timeout: Duration = 30.seconds, block: suspend () -> Unit): Unit

// jvm
actual fun coTest(timeout: Duration, block: suspend () -> Unit) {
    runBlocking {
        withTimeout(timeout) {
            block.invoke()
        }
    }
}

// js
private val testScope = CoroutineScope(CoroutineName("test-scope"))
actual fun coTest(timeout: Duration, block: suspend () -> Unit): dynamic = testScope.async {
    withTimeout(timeout) {
        block.invoke()
    }
}.asPromise()

This launches a co-routine in a scope of your choice using async which you can then return like a promise.

You then write a test like so:

@Test
fun myTest() = coTest {
  ...
}
Yee answered 26/5, 2022 at 11:0 Comment(1)
I tried this, and it generally works! However, one problem I encountered is that when a test fails it cancels the whole job, causing all following tests to fail as well. This doesn't happen when using GlobalScope.promise { ... } (see my answer).Letta

© 2022 - 2024 — McMap. All rights reserved.