ScalaTest provides comprehensive support for testing asynchronous code with Futures, custom execution contexts, and async test suites. This enables testing of non-blocking operations while maintaining proper test isolation and timing control.
Base trait for test suites that work with asynchronous operations returning Futures.
/**
* Base trait for asynchronous test suites
*/
trait AsyncTestSuite extends Suite with RecoverMethods with CompleteLastly {
/**
* Implicit execution context for running async operations
*/
implicit def executionContext: ExecutionContext
/**
* Transform test function to handle async operations
*/
def transformToFuture(testFun: => Future[compatible.Assertion]): FutureOutcome
/**
* Run a single asynchronous test
*/
def runAsyncTest(
testName: String,
args: Args,
includeIcon: Boolean,
invokeWithAsyncTestDataFunction: AsyncTestDataInvoker
): FutureOutcome
}
/**
* Async version of FunSuite
*/
abstract class AsyncFunSuite extends AsyncTestSuite with AsyncTestRegistration {
/**
* Register an asynchronous test
*/
protected def test(testName: String)(testFun: => Future[compatible.Assertion]): Unit
}
/**
* Async version of FlatSpec
*/
abstract class AsyncFlatSpec extends AsyncTestSuite with AsyncTestRegistration {
protected final class AsyncFlatSpecStringWrapper(string: String) {
def should(testFun: => Future[compatible.Assertion]): Unit
def must(testFun: => Future[compatible.Assertion]): Unit
def can(testFun: => Future[compatible.Assertion]): Unit
}
protected implicit def convertToAsyncFlatSpecStringWrapper(o: String): AsyncFlatSpecStringWrapper
}
/**
* Async versions of other test styles
*/
abstract class AsyncFunSpec extends AsyncTestSuite with AsyncTestRegistration
abstract class AsyncWordSpec extends AsyncTestSuite with AsyncTestRegistration
abstract class AsyncFreeSpec extends AsyncTestSuite with AsyncTestRegistration
abstract class AsyncFeatureSpec extends AsyncTestSuite with AsyncTestRegistrationUsage Examples:
import org.scalatest.funsuite.AsyncFunSuite
import scala.concurrent.Future
class AsyncExampleSuite extends AsyncFunSuite {
test("async computation should complete successfully") {
val futureResult = Future {
Thread.sleep(100) // Simulate async work
42
}
futureResult.map { result =>
assert(result == 42)
}
}
test("async operation with assertions") {
def asyncOperation(): Future[String] = Future.successful("Hello, World!")
asyncOperation().map { result =>
result should include ("World")
result should have length 13
}
}
}Wrapper for Future[Outcome] that provides transformation and composition methods.
/**
* Wrapper for Future[Outcome] with convenient transformation methods
*/
class FutureOutcome(private[scalatest] val underlying: Future[Outcome]) {
/**
* Transform the outcome using a function
*/
def map(f: Outcome => Outcome)(implicit executionContext: ExecutionContext): FutureOutcome
/**
* FlatMap operation for chaining FutureOutcomes
*/
def flatMap(f: Outcome => FutureOutcome)(implicit executionContext: ExecutionContext): FutureOutcome
/**
* Transform the Future[Outcome] directly
*/
def transform(f: Try[Outcome] => Try[Outcome])(implicit executionContext: ExecutionContext): FutureOutcome
/**
* Handle failures and recover with a different outcome
*/
def recover(pf: PartialFunction[Throwable, Outcome])(implicit executionContext: ExecutionContext): FutureOutcome
/**
* Convert to a Future[Outcome]
*/
def toFuture: Future[Outcome]
/**
* Block and wait for completion (mainly for testing)
*/
def isCompleted: Boolean
}
object FutureOutcome {
/**
* Create from a Future[Outcome]
*/
def apply(future: Future[Outcome]): FutureOutcome
/**
* Create from a successful outcome
*/
def successful(outcome: Outcome): FutureOutcome
/**
* Create from a failed outcome
*/
def failed(exception: Throwable): FutureOutcome
/**
* Create from a canceled outcome
*/
def canceled(exception: Throwable): FutureOutcome
}Usage Examples:
import org.scalatest._
import scala.concurrent.Future
import scala.util.{Success, Failure}
// Creating FutureOutcome instances
val successfulOutcome = FutureOutcome.successful(Succeeded)
val failedOutcome = FutureOutcome.failed(new RuntimeException("Test failed"))
// Transforming outcomes
val transformedOutcome = successfulOutcome.map {
case Succeeded => Succeeded
case failed => failed
}
// Chaining operations
val chainedOutcome = successfulOutcome.flatMap { outcome =>
if (outcome == Succeeded) {
FutureOutcome.successful(Succeeded)
} else {
FutureOutcome.failed(new RuntimeException("Chain failed"))
}
}
// Error recovery
val recoveredOutcome = failedOutcome.recover {
case ex: RuntimeException => Failed(ex)
case other => Failed(other)
}Utilities for handling exceptions in asynchronous tests.
trait RecoverMethods {
/**
* Recover from specific exception types in async operations
*/
def recoverToSucceededIf[T <: AnyRef](future: Future[Any])(implicit classTag: ClassTag[T]): Future[Assertion]
/**
* Recover expecting a specific exception to be thrown
*/
def recoverToExceptionIf[T <: AnyRef](future: Future[Any])(implicit classTag: ClassTag[T]): Future[T]
}Usage Examples:
import org.scalatest.RecoverMethods
import scala.concurrent.Future
class AsyncRecoveryExample extends AsyncFunSuite with RecoverMethods {
test("should recover from expected exception") {
val failingFuture = Future {
throw new IllegalArgumentException("Expected error")
}
// Test succeeds if the expected exception is thrown
recoverToSucceededIf[IllegalArgumentException] {
failingFuture
}
}
test("should capture expected exception") {
val failingFuture = Future {
throw new RuntimeException("Test error")
}
// Capture the exception for further assertions
recoverToExceptionIf[RuntimeException] {
failingFuture
}.map { exception =>
exception.getMessage should include ("Test error")
}
}
}Trait for cleanup operations that run after async tests complete.
trait CompleteLastly {
/**
* Register cleanup code to run after test completion
*/
def completeLastly(completeLastlyCode: => Unit): Unit
/**
* Register async cleanup code to run after test completion
*/
def completeLastly(completeLastlyCode: => Future[Unit]): Unit
}Usage Examples:
class AsyncResourceExample extends AsyncFunSuite with CompleteLastly {
test("should cleanup resources after async test") {
val resource = acquireResource()
// Register cleanup that runs regardless of test outcome
completeLastly {
resource.close()
println("Resource cleaned up")
}
// Test code that uses the resource
Future {
resource.process()
assert(resource.isProcessed)
}
}
test("should handle async cleanup") {
val asyncResource = acquireAsyncResource()
// Register async cleanup
completeLastly {
asyncResource.cleanupAsync()
}
asyncResource.processAsync().map { result =>
assert(result.isSuccess)
}
}
}Support for asynchronous setup and teardown in test fixtures.
/**
* Async version of fixture test suites
*/
trait AsyncFixtureTestSuite extends AsyncTestSuite {
type FixtureParam
/**
* Async fixture method that provides test data
*/
def withAsyncFixture(test: OneArgAsyncTest): FutureOutcome
}
/**
* One argument async test function
*/
abstract class OneArgAsyncTest extends (FixtureParam => Future[Outcome]) with TestData {
def apply(fixture: FixtureParam): Future[Outcome]
}Usage Examples:
import scala.concurrent.Future
class AsyncFixtureExample extends AsyncFunSuite {
case class DatabaseConnection(url: String) {
def query(sql: String): Future[List[String]] = Future.successful(List("result1", "result2"))
def close(): Future[Unit] = Future.successful(())
}
type FixtureParam = DatabaseConnection
def withAsyncFixture(test: OneArgAsyncTest): FutureOutcome = {
val connection = DatabaseConnection("jdbc:test://localhost")
complete {
withFixture(test.toNoArgAsyncTest(connection))
} lastly {
// Async cleanup
connection.close()
}
}
test("should query database asynchronously") { connection =>
connection.query("SELECT * FROM users").map { results =>
results should have size 2
results should contain ("result1")
}
}
}/**
* Test outcome for async operations
*/
sealed abstract class Outcome extends Product with Serializable
case object Succeeded extends Outcome
final case class Failed(exception: Throwable) extends Outcome
final case class Canceled(exception: Throwable) extends Outcome
case object Pending extends Outcome
/**
* Async test registration
*/
trait AsyncTestRegistration {
def registerAsyncTest(testName: String, testTags: Tag*)(testFun: => Future[compatible.Assertion]): Unit
def registerIgnoredAsyncTest(testName: String, testTags: Tag*)(testFun: => Future[compatible.Assertion]): Unit
}
/**
* Test data for async tests
*/
trait AsyncTestDataInvoker {
def apply(testData: TestData): Future[Outcome]
}