ScalaTest is a comprehensive testing framework for Scala and Java that provides multiple testing styles and sophisticated matcher libraries.
—
ScalaTest provides comprehensive support for asynchronous testing with Future-based tests, automatic timeout handling, and integration with concurrent utilities. All test styles have async variants that return Future[Assertion] instead of Assertion.
All ScalaTest styles have async variants for testing asynchronous code.
import org.scalatest.funsuite.AsyncFunSuite
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.wordspec.AsyncWordSpec
import org.scalatest.freespec.AsyncFreeSpec
import org.scalatest.featurespec.AsyncFeatureSpec
import org.scalatest.funspec.AsyncFunSpec
import org.scalatest.propspec.AsyncPropSpec
// Base async test suite trait
trait AsyncTestSuite extends Suite with RecoverMethods with CompleteLastly {
// Test methods return Future[Assertion] instead of Assertion
protected def transformToOutcome(testFun: => Future[compatible.Assertion]): () => AsyncOutcome
// Execution context for running async tests
implicit def executionContext: ExecutionContext
// Timeout configuration
implicit val patienceConfig: PatienceConfig
}
// Async FunSuite example
abstract class AsyncFunSuite extends AsyncTestSuite with AsyncTestSuiteMixin {
protected final class AsyncFunSuiteAsyncTest(testName: String, testTags: Tag*)
extends AsyncTest(testName, testTags: _*)
def test(testName: String, testTags: Tag*)(testFun: => Future[compatible.Assertion]): Unit
def ignore(testName: String, testTags: Tag*)(testFun: => Future[compatible.Assertion]): Unit
}Async Test Examples:
import org.scalatest.funsuite.AsyncFunSuite
import scala.concurrent.Future
import scala.concurrent.duration._
class AsyncCalculatorSuite extends AsyncFunSuite {
test("async addition should work") {
val futureResult = Future {
Thread.sleep(100) // Simulate async work
2 + 2
}
futureResult.map { result =>
assert(result === 4)
}
}
test("multiple async operations") {
val future1 = Future(10)
val future2 = Future(20)
for {
a <- future1
b <- future2
} yield {
assert(a + b === 30)
}
}
test("async failure handling") {
val failingFuture = Future.failed[Int](new RuntimeException("Expected failure"))
recoverToSucceededIf[RuntimeException] {
failingFuture
}
}
}Specialized assertion methods for Future-based testing.
import org.scalatest.concurrent.ScalaFutures
trait ScalaFutures {
// Configuration for Future handling
implicit val patienceConfig: PatienceConfig
// Block until Future completes and apply function to result
def whenReady[T](future: Future[T], timeout: Timeout = timeout, interval: Interval = interval)
(fun: T => Unit): Unit
// Extract Future value (blocking)
implicit class FutureValues[T](future: Future[T]) {
def futureValue: T
def futureValue(timeout: Timeout): T
}
// Assert Future completes successfully
def noException should be thrownBy future
// Assert Future fails with specific exception
a [ExceptionType] should be thrownBy future
}
// Patience configuration
case class PatienceConfig(timeout: Span, interval: Span)
object PatienceConfig {
implicit val defaultPatienceConfig: PatienceConfig =
PatienceConfig(timeout = scaled(Span(150, Millis)), interval = scaled(Span(15, Millis)))
}Future Assertion Examples:
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.matchers.should.Matchers
import scala.concurrent.Future
import scala.concurrent.duration._
class FutureAssertionSpec extends AnyFlatSpec with Matchers with ScalaFutures {
implicit override val patienceConfig = PatienceConfig(
timeout = scaled(Span(5, Seconds)),
interval = scaled(Span(50, Millis))
)
"Future assertions" should "work with whenReady" in {
val future = Future { 42 }
whenReady(future) { result =>
result should be(42)
}
}
"Future values" should "be extractable" in {
val future = Future { "hello world" }
future.futureValue should startWith("hello")
future.futureValue should have length 11
}
"Failed futures" should "be testable" in {
val failedFuture = Future.failed[String](new IllegalArgumentException("Bad argument"))
a [IllegalArgumentException] should be thrownBy {
failedFuture.futureValue
}
}
}Handle and test for expected failures in async code.
trait RecoverMethods {
// Recover from expected exception type
def recoverToSucceededIf[T <: AnyRef](future: Future[Any])
(implicit classTag: ClassTag[T]): Future[Assertion]
// Recover and return the exception
def recoverToExceptionIf[T <: AnyRef](future: Future[Any])
(implicit classTag: ClassTag[T]): Future[T]
}
// Usage in async tests
class AsyncRecoverySpec extends AsyncFunSuite {
test("should recover from expected exception") {
val failingFuture = Future.failed[Int](new IllegalArgumentException("Expected"))
recoverToSucceededIf[IllegalArgumentException] {
failingFuture
}
}
test("should capture and examine exception") {
val failingFuture = Future.failed[Int](new IllegalArgumentException("Test message"))
recoverToExceptionIf[IllegalArgumentException] {
failingFuture
}.map { exception =>
assert(exception.getMessage === "Test message")
}
}
}Manage asynchronous test fixtures and resources.
import org.scalatest.funsuite.FixtureAsyncFunSuite
// Async fixture suite
abstract class FixtureAsyncFunSuite extends FixtureAsyncTestSuite with AsyncTestSuiteMixin {
type FixtureParam
// Async fixture management
def withFixture(test: OneArgAsyncTest): FutureOutcome
// Async test definition
def test(testName: String, testTags: Tag*)(testFun: FixtureParam => Future[compatible.Assertion]): Unit
}
// Async fixture example
class AsyncDatabaseSuite extends FixtureAsyncFunSuite {
type FixtureParam = Database
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
val database = Database.connect()
complete {
super.withFixture(test.toNoArgAsyncTest(database))
} lastly {
database.close()
}
}
test("async database operations") { db =>
for {
_ <- db.insert("user", Map("name" -> "Alice"))
user <- db.findByName("Alice")
} yield {
assert(user.name === "Alice")
}
}
}Test conditions that should eventually become true.
import org.scalatest.concurrent.Eventually
trait Eventually {
// Repeatedly test condition until it succeeds or times out
def eventually[T](fun: => T)(implicit config: PatienceConfig): T
// Configuration for eventually
implicit val patienceConfig: PatienceConfig
}
// Integration with async tests
trait AsyncEventually extends Eventually {
def eventually[T](fun: => Future[T])(implicit config: PatienceConfig): Future[T]
}Eventually Examples:
import org.scalatest.concurrent.Eventually
import org.scalatest.time.{Millis, Seconds, Span}
class EventuallySpec extends AnyFlatSpec with Eventually {
implicit override val patienceConfig = PatienceConfig(
timeout = scaled(Span(5, Seconds)),
interval = scaled(Span(100, Millis))
)
"Eventually" should "wait for condition to become true" in {
var counter = 0
eventually {
counter += 1
assert(counter >= 10)
}
}
"Eventually with async" should "work with futures" in {
@volatile var ready = false
// Simulate async operation that sets ready flag
Future {
Thread.sleep(1000)
ready = true
}
eventually {
assert(ready === true)
}
}
}Configure execution context for async tests.
// Default execution context
import org.scalatest.concurrent.ScalaFutures._
trait AsyncTestSuite {
// Default execution context (can be overridden)
implicit def executionContext: ExecutionContext =
scala.concurrent.ExecutionContext.Implicits.global
}
// Custom execution context
class CustomAsyncSuite extends AsyncFunSuite {
// Use custom thread pool
implicit override def executionContext: ExecutionContext =
ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
}Configure timeouts and patience parameters for async operations.
import org.scalatest.time._
// Time span specifications
case class Span(length: Long, unit: Units)
// Timeout and interval configuration
case class PatienceConfig(timeout: Span, interval: Span) {
def scaled(factor: Double): PatienceConfig
}
// Predefined time units
object Span {
def apply(length: Long, unit: Units): Span
}
sealed abstract class Units
case object Nanosecond extends Units
case object Nanoseconds extends Units
case object Microsecond extends Units
case object Microseconds extends Units
case object Millisecond extends Units
case object Milliseconds extends Units
case object Millis extends Units
case object Second extends Units
case object Seconds extends Units
case object Minute extends Units
case object Minutes extends Units
case object Hour extends Units
case object Hours extends Units
case object Day extends Units
case object Days extends UnitsTimeout Configuration Examples:
import org.scalatest.time._
import scala.concurrent.duration._
class TimeoutConfigSpec extends AsyncFunSuite {
// Custom patience configuration
implicit override val patienceConfig = PatienceConfig(
timeout = scaled(Span(10, Seconds)),
interval = scaled(Span(200, Millis))
)
test("long running operation") {
val longFuture = Future {
Thread.sleep(2000) // 2 second delay
"completed"
}
longFuture.map { result =>
assert(result === "completed")
}
}
test("operation with custom timeout") {
val future = Future { "result" }
whenReady(future, timeout(5.seconds)) { result =>
assert(result === "result")
}
}
}Run async tests in parallel for better performance.
// Parallel async execution
trait ParallelTestExecution { this: AsyncTestSuite =>
// Enable parallel execution of async tests
}
class ParallelAsyncSuite extends AsyncFunSuite with ParallelTestExecution {
test("parallel async test 1") {
Future {
Thread.sleep(100)
assert(1 + 1 === 2)
}
}
test("parallel async test 2") {
Future {
Thread.sleep(100)
assert(2 + 2 === 4)
}
}
}Test reactive streams and async data processing.
// Example with Akka Streams (not part of ScalaTest core)
import akka.stream.scaladsl.Source
import akka.stream.testkit.scaladsl.TestSink
class StreamTestSpec extends AsyncFunSuite {
test("stream processing") {
val source = Source(1 to 10)
.map(_ * 2)
.filter(_ > 10)
val future = source.runFold(0)(_ + _)
future.map { sum =>
assert(sum === 60) // 12 + 14 + 16 + 18 + 20 = 80
}
}
}Common patterns for handling errors in async tests.
class AsyncErrorHandlingSpec extends AsyncFunSuite {
test("transform failures") {
val future = Future.failed[Int](new RuntimeException("Original error"))
future.recover {
case _: RuntimeException => 42
}.map { result =>
assert(result === 42)
}
}
test("chain operations with error handling") {
def mightFail(value: Int): Future[Int] = {
if (value < 0) Future.failed(new IllegalArgumentException("Negative value"))
else Future.successful(value * 2)
}
val result = for {
a <- Future.successful(5)
b <- mightFail(a)
c <- mightFail(b)
} yield c
result.map { finalValue =>
assert(finalValue === 20) // 5 * 2 * 2
}
}
test("verify specific failure") {
val future = Future.failed[String](new IllegalStateException("Bad state"))
recoverToSucceededIf[IllegalStateException] {
future
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-scalatest--scalatest-2-11