I'm trying to use scalatest Asynchronous Test Suites, but aside from some restrictions over setting timeout and what not, I don't see what does the test suite actually add.
I wonder if anyone versed into asynchronous testing with scalatest could quickly explain the differences between Asynchronous Test Suites and org.scalatest.concurrent
. What do async test suites actually add over org.scalatest.concurrent
? Is one approach better over the other?
We compare the following ScalaTest facilities for testing code that returns Future
s:
AsyncFlatSpec
class AsyncSpec extends AsyncFlatSpec {
...
Future(3).map { v => assert(v == 3) }
...
}
Future
completes, i.e., return Future[Assertion]
instead of Assertion
Futures
execute and complete in the order they are started and one after anotherFutures
Await
, whenReady
Future[Assertion]
class ScalaFuturesSpec extends FlatSpec with ScalaFutures {
...
whenReady(Future(3) { v => assert(v == 3) }
...
}
Future
before we can return Assertion
scala.concurrent.ExecutionContext.Implicits.global
which is a
multi-threaded pool for parallel executionAssertion
class EventuallySpec extends FlatSpec with Eventually {
...
eventually { assert(Future(3).value.contains(Success(3))) }
...
}
Futures
Futures
it is likely global execution context will be used scalatest-async-testing-comparison is an example demonstrating the difference in two execution model.
Given the following test body
val f1 = Future {
val tmp = mutableSharedState
Thread.sleep(5000)
println(s"Start Future1 with mutableSharedState=$tmp in thread=${Thread.currentThread}")
mutableSharedState = tmp + 1
println(s"Complete Future1 with mutableSharedState=$mutableSharedState")
}
val f2 = Future {
val tmp = mutableSharedState
println(s"Start Future2 with mutableSharedState=$tmp in thread=${Thread.currentThread}")
mutableSharedState = tmp + 1
println(s"Complete Future2 with mutableSharedState=$mutableSharedState")
}
for {
_ <- f1
_ <- f2
} yield {
assert(mutableSharedState == 2)
}
let us consider the output of AsyncSpec
against ScalaFuturesSpec
testOnly example.AsyncSpec:
Start Future1 with mutableSharedState=0 in thread=Thread[pool-11-thread-3-ScalaTest-running-AsyncSpec,5,main]
Complete Future1 with mutableSharedState=1
Start Future2 with mutableSharedState=1 in thread=Thread[pool-11-thread-3-ScalaTest-running-AsyncSpec,5,main]
Complete Future2 with mutableSharedState=2
testOnly example.ScalaFuturesSpec:
Start Future2 with mutableSharedState=0 in thread=Thread[scala-execution-context-global-119,5,main]
Complete Future2 with mutableSharedState=1
Start Future1 with mutableSharedState=0 in thread=Thread[scala-execution-context-global-120,5,main]
Complete Future1 with mutableSharedState=1
Note how in serial execution model same thread is used and Futures completed in order. On the other hand,
in global execution model different threads were used, and Future2
completed before Future1
, which caused
race condition on the shared mutable state, which in turn made the test fail.
In unit tests we should use mocked subsystems where returned Futures
should be completing near-instantly, so there
is no need for Eventually
in unit tests. Hence the choice is between async styles and ScalaFutures
. The main difference
between the two is that former is non-blocking unlike the latter. If possible, we should never block, so we
should prefer async styles like AsyncFlatSpec
. Further big difference is the execution model. Async styles
by default use custom serial execution model which provides thread-safety on shared mutable state, unlike global
thread-pool backed execution model often used with ScalaFutures
. In conclusion, my suggestion is we use async style
traits unless we have a good reason not to.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With