Asynchronous testing
ScalaTest supports asynchronous non-blocking testing. Given a Future
returned by the code you are testing, you need not block until the Future
completes before performing assertions against its value. You can instead map those assertions onto the Future
and return the resulting Future[Assertion]
to ScalaTest. The test will complete asynchronously, when the Future[Assertion]
completes.
The followings are the asynchronous style traits supported:
AsyncFeatureSpec
AsyncFlatSpec
AsyncFreeSpec
AsyncFunSpec
AsyncFunSuite
AsyncWordSpec
The asynchronous style traits follow the style of their synchronous cousins. For example, here's an AsyncFlatSpec
:
package org.scalatest.examples.asyncflatspec
import org.scalatest.AsyncFlatSpec
import scala.concurrent.Future
class AddSpec extends AsyncFlatSpec {
def addSoon(addends: Int*): Future[Int] = Future { addends.sum }
behavior of "addSoon"
it should "eventually compute a sum of passed Ints" in {
val futureSum: Future[Int] = addSoon(1, 2)
futureSum map { sum => assert(sum == 3) }
}
def addNow(addends: Int*): Int = addends.sum
"addNow" should "immediately compute a sum of passed Ints" in {
val sum: Int = addNow(1, 2)
assert(sum == 3)
}
}
Running the above AddSpec
in the Scala interpreter would yield:
addSoon
- should eventually compute a sum of passed Ints
- should immediately compute a sum of passed Ints
Starting with version 3.0.0, ScalaTest assertions and matchers have result type Assertion
. The result type of the first test in the example above, therefore, is Future[Assertion]
.
For clarity, here's the relevant code in a REPL session:
scala> import org.scalatest._
import org.scalatest._
scala> import Assertions._
import Assertions._
scala> import scala.concurrent.Future
import scala.concurrent.Future
scala> import scala.concurrent.ExecutionContext
import scala.concurrent.ExecutionContext
scala> implicit val executionContext = ExecutionContext.Implicits.global
executionContext: scala.concurrent.ExecutionContextExecutor = scala.concurrent.impl.ExecutionContextImpl@26141c5b
scala> def addSoon(addends: Int*): Future[Int] = Future { addends.sum }
addSoon: (addends: Int*)scala.concurrent.Future[Int]
scala> val futureSum: Future[Int] = addSoon(1, 2)
futureSum: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@721f47b2
scala> futureSum map { sum => assert(sum == 3) }
res0: scala.concurrent.Future[org.scalatest.Assertion] = scala.concurrent.impl.Promise$DefaultPromise@3955cfcb
The second test has result type Assertion
:
scala> def addNow(addends: Int*): Int = addends.sum
addNow: (addends: Int*)Int
scala> val sum: Int = addNow(1, 2)
sum: Int = 3
scala> assert(sum == 3)
res1: org.scalatest.Assertion = Succeeded
When AddSpec
is constructed, the second test will be implicitly converted to Future[Assertion]
and registered. The implicit conversion is from Assertion
to Future[Assertion]
,
so you must end synchronous tests in some ScalaTest assertion or matcher expression. If a test would not otherwise end in type Assertion
, you can place succeed
at the end of
the test. succeed
, a field in trait Assertions
, returns the Succeeded
singleton:
scala> succeed
res2: org.scalatest.Assertion = Succeeded
Thus placing succeed
at the end of a test body will satisfy the type checker:
"addNow" should "immediately compute a sum of passed Ints" in {
val sum: Int = addNow(1, 2)
assert(sum == 3)
println("hi")
succeed
}
Asynchronous execution model
Asynchronous style traits extend AsyncTestSuite
, which provides an implicit scala.concurrent.ExecutionContext
named executionContext
. This execution context is
used by asynchronous style traits to transform the Future[Assertion]
s returned by each test into the FutureOutcome
returned by the test function passed to withFixture
.
This ExecutionContext
is also intended to be used in the tests, including when you map assertions onto futures.
On both the JVM and Scala.js, the default execution context provided by ScalaTest's asynchronous testing styles confines execution to a single thread per test. On JavaScript,
where single-threaded execution is the only possibility, the default execution context is scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
. On the JVM,
the default execution context is a serial execution context provided by ScalaTest itself.
When ScalaTest's serial execution context is called upon to execute a task, that task is recorded in a queue for later execution. For example, one task that will be placed in this
queue is the task that transforms the Future[Assertion]
returned by an asynchronous test body to the FutureOutcome
returned from the test function. Other
tasks that will be queued are any transformations of, or callbacks registered on, Future
s that occur in your test body, including any assertions you map onto Future
s.
Once the test body returns, the thread that executed the test body will execute the tasks in that queue one after another, in the order they were enqueued.
ScalaTest provides its serial execution context as the default on the JVM for three reasons. First, most often running both tests and suites in parallel does not give a significant
performance boost compared to just running suites in parallel. Thus parallel execution of Future
transformations within individual tests is not generally needed for
performance reasons.
Second, if multiple threads are operating in the same suite concurrently, you'll need to make sure access to any mutable fixture objects by multiple threads is synchronized. Although
access to mutable state along the same linear chain of Future
transformations need not be synchronized, this does not hold true for callbacks, and in general it is easy to
make a mistake. Simply put: synchronizing access to shared mutable state is difficult and error prone. Because ScalaTest's default execution context on the JVM confines execution of
Future
transformations and call backs to a single thread, you need not (by default) worry about synchronizing access to mutable state in your asynchronous-style tests.
Third, asynchronous-style tests need not be complete when the test body returns, because the test body returns a Future[Assertion]
. This Future[Assertion]
will
often represent a test that has not yet completed. As a result, when using a more traditional execution context backed by a thread-pool, you could potentially start many more tests executing
concurrently than there are threads in the thread pool. The more concurrently execute tests you have competing for threads from the same limited thread pool, the more likely it will be that
tests will intermitently fail due to timeouts.
Using ScalaTest's serial execution context on the JVM will ensure the same thread that produced the Future[Assertion]
returned from a test body is also used to execute any tasks
given to the execution context while executing the test body—and that thread will not be allowed to do anything else until the test completes. If the serial execution context's task queue
ever becomes empty while the Future[Assertion]
returned by that test's body has not yet completed, the thread will block until another task for that test is enqueued. Although
it may seem counter-intuitive, this blocking behavior means the total number of tests allowed to run concurrently will be limited to the total number of threads executing suites. This fact
means you can tune the thread pool such that maximum performance is reached while avoiding (or at least, reducing the likelihood of) tests that fail due to timeouts because of thread competition.
This thread confinement strategy does mean, however, that when you are using the default execution context on the JVM, you must be sure to never block in the test body waiting for a task to be
completed by the execution context. If you block, your test will never complete. This kind of problem will be obvious, because the test will consistently hang every time you run it. (If a test
is hanging, and you're not sure which one it is, enable slowpoke notifications.) If you really do want to block in your tests, you may wish to just use a traditional style traits with
ScalaFutures
instead. Alternatively, you could override the executionContext
and use a traditional ExecutionContext
backed by a thread pool. This will
enable you to block in an asynchronous-style test on the JVM, but you'll need to worry about synchronizing access to shared mutable state.
To use a different execution context, just override executionContext
. For example, if you prefer to use the runNow
execution context on Scala.js instead of the default queue,
you would write:
implicit override def executionContext = scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow
If you prefer on the JVM to use the global execution context, which is backed by a thread pool, instead of ScalaTest's default serial execution contex, which confines execution to a single thread,
you would write:
implicit override def executionContext = scala.concurrent.ExecutionContext.Implicits.global
Serial and parallel test execution
By default (unless you mix in ParallelTestExecution
), tests in an asynchronous style traits will be executed one after another, i.e., serially. This is true whether those
tests return Assertion
or Future[Assertion]
, no matter what threads are involved. This default behavior allows you to re-use a shared fixture, such as an external
database that needs to be cleaned after each test, in multiple tests in async-style suites. This is implemented by registering each test, other than the first test, to run as a continuation
after the previous test completes.
If you want the tests of an asynchronous style trait to be executed in parallel, you must mix in ParallelTestExecution
and enable parallel execution of tests in your build.
You enable parallel execution in Runner
with the -P
command line argument. In the ScalaTest Maven Plugin, set parallel
to true
. In sbt,
parallel execution is the default, but to be explicit you can write:
parallelExecution in Test := true
On the JVM, if both ParallelTestExecution
is mixed in and parallel execution is enabled in the build, tests in an async-style suite will be started in parallel, using threads
from the Distributor
, and allowed to complete in parallel, using threads from the executionContext
. If you are using ScalaTest's serial execution context, the JVM
default, asynchronous tests will run in parallel very much like traditional style trait's tests run in parallel: 1) Because ParallelTestExecution
extends OneInstancePerTest
,
each test will run in its own instance of the test class, you need not worry about synchronizing access to mutable instance state shared by different tests in the same suite. 2) Because the serial
execution context will confine the execution of each test to the single thread that executes the test body, you need not worry about synchronizing access to shared mutable state accessed by
transformations and callbacks of Future
s inside the test.
If ParallelTestExecution
is mixed in but parallel execution of suites is not enabled, asynchronous tests on the JVM will be started sequentially, by the single thread that invoked run,
but without waiting for one test to complete before the next test is started. As a result, asynchronous tests will be allowed to complete in parallel, using threads from the executionContext
.
If you are using the serial execution context, however, you'll see the same behavior you see when parallel execution is disabled and a traditional suite that mixes in ParallelTestExecution
is
executed: the tests will run sequentially. If you use an execution context backed by a thread-pool, such as global, however, even though tests will be started sequentially by one thread, they will be
allowed to run concurrently using threads from the execution context's thread pool.
The latter behavior is essentially what you'll see on Scala.js when you execute a suite that mixes in ParallelTestExecution
. Because only one thread exists when running under JavaScript, you
can't "enable parallel execution of suites." However, it may still be useful to run tests in parallel on Scala.js, because tests can invoke API calls that are truly asynchronous by calling into external
APIs that take advantage of non-JavaScript threads. Thus on Scala.js, ParallelTestExecution
allows asynchronous tests to run in parallel, even though they must be started sequentially. This
may give you better performance when you are using API calls in your Scala.js tests that are truly asynchronous.
Futures and expected exceptions
If you need to test for expected exceptions in the context of futures, you can use the recoverToSucceededIf
and recoverToExceptionIf
methods of trait RecoverMethods
.
Because this trait is mixed into supertrait AsyncTestSuite
, both of these methods are available by default in an asynchronous style traits.
If you just want to ensure that a future fails with a particular exception type, and do not need to inspect the exception further, use recoverToSucceededIf
:
recoverToSucceededIf[IllegalStateException] {
emptyStackActor ? Peek
}
The recoverToSucceededIf
method performs a job similar to assertThrows
, except in the context of a future. It transforms a Future
of any type into a Future[Assertion]
that succeeds only if the original future fails with the specified exception. Here's an example in the REPL:
scala> import org.scalatest.RecoverMethods._
import org.scalatest.RecoverMethods._
scala> import scala.concurrent.Future
import scala.concurrent.Future
scala> import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.ExecutionContext.Implicits.global
scala> recoverToSucceededIf[IllegalStateException] {
| Future { throw new IllegalStateException }
| }
res0: scala.concurrent.Future[org.scalatest.Assertion] = ...
scala> res0.value
res1: Option[scala.util.Try[org.scalatest.Assertion]] = Some(Success(Succeeded))
Otherwise it fails with an error message similar to those given by assertThrows
:
scala> recoverToSucceededIf[IllegalStateException] {
| Future { throw new RuntimeException }
| }
res2: scala.concurrent.Future[org.scalatest.Assertion] = ...
scala> res2.value
res3: Option[scala.util.Try[org.scalatest.Assertion]] =
Some(Failure(org.scalatest.exceptions.TestFailedException: Expected exception
java.lang.IllegalStateException to be thrown, but java.lang.RuntimeException
was thrown))
scala> recoverToSucceededIf[IllegalStateException] {
| Future { 42 }
| }
res4: scala.concurrent.Future[org.scalatest.Assertion] = ...
scala> res4.value
res5: Option[scala.util.Try[org.scalatest.Assertion]] =
Some(Failure(org.scalatest.exceptions.TestFailedException: Expected exception
java.lang.IllegalStateException to be thrown, but no exception was thrown))
The recoverToExceptionIf
method differs from the recoverToSucceededIf
in its behavior when the assertion succeeds: recoverToSucceededIf
yields a
Future[Assertion]
, whereas recoverToExceptionIf
yields a Future[T]
, where T
is the expected exception type.
recoverToExceptionIf[IllegalStateException] {
emptyStackActor ? Peek
}
In other words, recoverToExceptionIf
is to intercept
as recovertToSucceededIf
is to assertThrows
. The first one allows you to perform
further assertions on the expected exception. The second one gives you a result type that will satisfy the type checker at the end of the test body. Here's an example showing recoverToExceptionIf
in the REPL:
scala> val futureEx =
| recoverToExceptionIf[IllegalStateException] {
| Future { throw new IllegalStateException("hello") }
| }
futureEx: scala.concurrent.Future[IllegalStateException] = ...
scala> futureEx.value
res6: Option[scala.util.Try[IllegalStateException]] =
Some(Success(java.lang.IllegalStateException: hello))
scala> futureEx map { ex => assert(ex.getMessage == "world") }
res7: scala.concurrent.Future[org.scalatest.Assertion] = ...
scala> res7.value
res8: Option[scala.util.Try[org.scalatest.Assertion]] =
Some(Failure(org.scalatest.exceptions.TestFailedException: "[hello]" did not equal "[world]"))
Using asynchronous withFixture
You'll use NoArgAsyncTest
and OneArgAsyncTest
for asynchronous style traits, the following is the default implementation for withFixture
:
protected def withFixture(test: NoArgAsyncTest): FutureOutcome = {
test()
}
You can, therefore, override withFixture
to perform setup before invoking the test function, and/or perform cleanup after the test completes. The recommended way to ensure
cleanup is performed after a test completes is to use the complete-lastly syntax, defined in supertrait CompleteLastly
. The complete-lastly syntax will ensure that cleanup
will occur whether future-producing code completes abruptly by throwing an exception, or returns normally yielding a future. In the latter case, complete-lastly will register the cleanup
code to execute asynchronously when the future completes.
The withFixture
method is designed to be stacked, and to enable this, you should always call the super implementation of withFixture
, and let it invoke the test
function rather than invoking the test function directly. In other words, instead of writing “test()
”, you should write “super.withFixture(test)
”, like this:
override def withFixture(test: NoArgAsyncTest) = {
complete {
super.withFixture(test)
} lastly {
}
}
If you have no cleanup to perform, you can write withFixture like this instead:
override def withFixture(test: NoArgAsyncTest) = {
super.withFixture(test) // Invoke the test function
}
If you want to perform an action only for certain outcomes, you'll need to register code performing that action as a callback on the Future
using one of Future
's
registration methods: onComplete
, onSuccess
, or onFailure
. Note that if a test fails, that will be treated as a scala.util.Success
(org.scalatest.Failed
). So if you want to perform an action if a test fails, for example, you'd register the callback using onSuccess
.
Here's an example in which withFixture(NoArgAsyncTest)
is used to take a snapshot of the working directory if a test fails, and send that information to the standard output stream:
package org.scalatest.examples.asyncflatspec.noargasynctest
import java.io.File
import org.scalatest._
import scala.concurrent.Future
class ExampleSpec extends AsyncFlatSpec {
override def withFixture(test: NoArgAsyncTest) = {
super.withFixture(test) onFailedThen { _ =>
val currDir = new File(".")
val fileNames = currDir.list()
info("Dir snapshot: " + fileNames.mkString(", "))
}
}
def addSoon(addends: Int*): Future[Int] = Future { addends.sum }
"This test" should "succeed" in {
addSoon(1, 1) map { sum => assert(sum == 2) }
}
it should "fail" in {
addSoon(1, 1) map { sum => assert(sum == 3) }
}
}
Running this version of ExampleSpec
in the interpreter in a directory with two files, hello.txt and world.txt would give the following output:
scala> org.scalatest.run(new ExampleSpec)
ExampleSpec:
This test
- should succeed
- should fail *** FAILED ***
2 did not equal 3 (:33)
Note that the NoArgAsyncTest
passed to withFixture
, in addition to an apply method that executes the test, also includes the test name and the config map passed to
runTest
. Thus you can also use the test name and configuration objects in your withFixture
implementation.
Lastly, if you want to transform the outcome in some way in withFixture
, you'll need to use either the map
or transform
methods of Future
, like this:
override def withFixture(test: NoArgAsyncTest) = {
val futureOutcome = super.withFixture(test) // Invoke the test function
futureOutcome change { outcome =>
}
}
Note that a NoArgAsyncTest
's apply method will return a scala.util.Failure
only if the test completes abruptly with a "test-fatal" exception (such as OutOfMemoryError
)
that should cause the suite to abort rather than the test to fail. Thus usually you would use map
to transform future outcomes, not transform, so that such test-fatal exceptions pass through unchanged.
The suite will abort asynchronously with any exception returned from NoArgAsyncTest
's apply method in a scala.util.Failure
.
If all or most tests need the same fixture, you can avoid some of the boilerplate of the loan-fixture method approach by using a fixture.AsyncTestSuite
and overriding
withFixture
(OneArgAsyncTest
). Each test in a fixture.AsyncTestSuite
takes a fixture as a parameter, allowing you to pass the fixture into the test.
You must indicate the type of the fixture parameter by specifying FixtureParam
, and implement a withFixture
method that takes a OneArgAsyncTest
.
This withFixture
method is responsible for invoking the one-arg async test function, so you can perform fixture set up before invoking and passing the fixture into the test
function, and ensure clean up is performed after the test completes.
To enable the stacking of traits that define withFixture
(NoArgAsyncTest
), it is a good idea to let withFixture
(NoArgAsyncTest
) invoke the
test function instead of invoking the test function directly. To do so, you'll need to convert the OneArgAsyncTest
to a NoArgAsyncTest
. You can do that by passing
the fixture object to the toNoArgAsyncTest
method of OneArgAsyncTest
. In other words, instead of writing “test(theFixture)
”, you'd delegate responsibility
for invoking the test function to the withFixture(NoArgAsyncTest)
method of the same instance by writing:
withFixture(test.toNoArgAsyncTest(theFixture))
Here's a complete example:
package org.scalatest.examples.asyncflatspec.oneargasynctest
import org.scalatest._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
sealed abstract class StringOp
case object Clear extends StringOp
case class Append(value: String) extends StringOp
case object GetValue
class StringActor {
private final val sb = new StringBuilder
def !(op: StringOp): Unit =
synchronized {
op match {
case Append(value) => sb.append(value)
case Clear => sb.clear()
}
}
def ?(get: GetValue.type)(implicit c: ExecutionContext): Future[String] =
Future {
synchronized { sb.toString }
}
}
class ExampleSpec extends fixture.AsyncFlatSpec {
type FixtureParam = StringActor
def withFixture(test: OneArgAsyncTest): FutureOutcome = {
val actor = new StringActor
complete {
actor ! Append("ScalaTest is ")
withFixture(test.toNoArgAsyncTest(actor))
} lastly {
actor ! Clear
}
}
"Testing" should "be easy" in { actor =>
actor ! Append("easy!")
val futureString = actor ? GetValue
futureString map { s =>
assert(s == "ScalaTest is easy!")
}
}
it should "be fun" in { actor =>
actor ! Append("fun!")
val futureString = actor ? GetValue
futureString map { s =>
assert(s == "ScalaTest is fun!")
}
}
}
Next, learn about using ScalaTest with Scala-js.