Search code examples
scalaunit-testingasynchronousscalatestscala.js

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.


Solution

  • 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
    }