Nikita
Nikita

Reputation: 2943

How to write async Scala.js test (e.g. using ScalaTest)?

Some of my code is async, and I want to test this code's execution has resulted in correct state. I do not have a reference to a Future or a JS Promise that I could map over – the async code in question lives inside a JS library that I'm using, and it just calls setTimeout(setSomeState, 0), which is why my only recourse is to test the state asynchronously, after a short delay (10 ms).

This is my best attempt:

import org.scalatest.{Assertion, AsyncFunSpec, Matchers}    
import scala.concurrent.Promise
import scala.scalajs.js
import scala.scalajs.concurrent.JSExecutionContext

class FooSpec extends AsyncFunSpec with Matchers {

  implicit override def executionContext = JSExecutionContext.queue

  it("async works") {
    val promise = Promise[Assertion]()

    js.timers.setTimeout(10) {
      promise.success {
        println("FOO")
        assert(true)
      }
    }

    promise.future
  }
}

This works when the assertion succeeds – with assert(true). However, when the assertion fails (e.g. if you replace it with assert(false)), the test suite freezes up. sbt just stops printing anything, and hangs indefinitely, the test suite never completes. In case of such failure FooSpec: line does get printed, but not the name of the test ("async works"), nor the "FOO" string.

If I comment out the executionContext line, I get the "Queue is empty while future is not completed, this means you're probably using a wrong ExecutionContext for your task, please double check your Future." error which is explained in detail in one of the links below.

I think these links are relevant to this problem:

https://github.com/scalatest/scalatest/issues/910

https://github.com/scalatest/scalatest/issues/1039

But I couldn't figure out a solution that would work.

Should I be building the Future[Assertion] in a different way, maybe?

I'm not tied to ScalaTest, but judging by the comments in one of the links above it seems that uTest has a similar problem except it tends to ignore the tests instead of stalling the test suite.

I just want to make assertions after a short delay, seems like it should definitely be possible. Any advice on how to accomplish that would be much appreciated.

Upvotes: 1

Views: 648

Answers (2)

Nikita
Nikita

Reputation: 2943

As was explained to me in this scala.js gitter thread, I'm using Promise.success incorrectly. That method expects a value to complete the promise with, but assert(false) throws an exception, it does not return a value of type Assertion.

Since in my code assert(false) is evaluated before Promise.success is called, the exception is thrown before the promise has a chance to complete. However, the exception is thrown in an synchronous callback to setTimeout, so it is invisible to ScalaTest. ScalaTest is then left waiting for a promise.future that never completes (because the underlying promise never completes).

Basically, my code is equivalent to this:

val promise = Promise[Assertion]()
js.timers.setTimeout(10) {
  println("FOO")
  val successValue = assert(false) // exception thrown here
  promise.success(successValue) // this line does not get executed
}
promise.future

Instead, I should have used Promise.complete which expects a Try. Try.apply accepts an argument in pass-by-name mode, meaning that it will be evaluated only after Try() is called.

So the working code looks like this:

it("async works") {
  val promise = Promise[Assertion]()
  js.timers.setTimeout(10) {
    promise.complete(Try({
      println("FOO")
      assert(true)
    })
  })
  promise.future
}

Upvotes: 3

GhostCat
GhostCat

Reputation: 140457

The real answer here is: you should try to get the "async" part out of your unit test setup.

All dealing with waits; sleeps; and so on adds a level of complexity that you do not want to have in your unit tests.

As you are not showing the production code you are using I can only make some suggestion how to approach this topic on a general level.

Example: when one builds his threading on top of Java's ExecutorService, you can go for a same-thread executor service; and your unit tests are using a single thread; and many things become much easier.

Long story short: consider looking into that "concept" that gives you "async" in your solution; and if there are ways to avoid the "really async" part; but of course without (!) making test-only changes to your production code.

Upvotes: 0

Related Questions