ScalaTest provides multiple patterns for managing test setup, teardown, and shared resources across test executions. The framework offers simple before/after hooks, parameterized fixtures, loan patterns, and sophisticated lifecycle management to ensure clean, isolated, and efficient test execution.
Simple setup and teardown hooks that execute around each test or entire test suite.
/**
* Simple before/after hooks for each test
*/
trait BeforeAndAfter {
/**
* Execute before each test method
*/
protected def before(): Unit
/**
* Execute after each test method (even if test fails)
*/
protected def after(): Unit
}
/**
* Enhanced per-test lifecycle hooks
*/
trait BeforeAndAfterEach {
/**
* Execute before each test with access to test data
*/
protected def beforeEach(): Unit
/**
* Execute after each test with access to test data
*/
protected def afterEach(): Unit
}
/**
* Per-test hooks with test metadata access
*/
trait BeforeAndAfterEachTestData {
/**
* Execute before each test with test information
* @param testData metadata about the test being executed
*/
protected def beforeEach(testData: TestData): Unit
/**
* Execute after each test with test information
* @param testData metadata about the test that was executed
*/
protected def afterEach(testData: TestData): Unit
}Usage Examples:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import org.scalatest.{BeforeAndAfter, BeforeAndAfterEach}
class DatabaseTestSpec extends AnyFunSuite with Matchers with BeforeAndAfter {
var database: TestDatabase = _
before {
// Setup before each test
database = new TestDatabase()
database.connect()
database.createTestTables()
}
after {
// Cleanup after each test
database.dropTestTables()
database.disconnect()
}
test("user creation should work") {
val user = User("Alice", "alice@example.com")
database.save(user)
val retrieved = database.findByEmail("alice@example.com")
retrieved should be(defined)
retrieved.get.name should equal("Alice")
}
test("user deletion should work") {
val user = User("Bob", "bob@example.com")
database.save(user)
database.delete(user.id)
database.findByEmail("bob@example.com") should be(empty)
}
}
class LoggingTestSpec extends AnyFunSuite with BeforeAndAfterEach {
override def beforeEach(): Unit = {
println(s"Starting test: ${getClass.getSimpleName}")
TestLogger.setLevel(LogLevel.DEBUG)
}
override def afterEach(): Unit = {
TestLogger.clearLogs()
println(s"Finished test: ${getClass.getSimpleName}")
}
test("service should log operations") {
val service = new UserService()
service.createUser("Test User")
TestLogger.getMessages() should contain("Creating user: Test User")
}
}Setup and teardown that execute once per test suite rather than per test.
/**
* Suite-level lifecycle hooks
*/
trait BeforeAndAfterAll {
/**
* Execute once before all tests in the suite
*/
protected def beforeAll(): Unit
/**
* Execute once after all tests in the suite (even if tests fail)
*/
protected def afterAll(): Unit
}
/**
* Suite-level hooks with configuration access
*/
trait BeforeAndAfterAllConfigMap {
/**
* Execute before all tests with access to configuration
* @param configMap configuration passed to the test run
*/
protected def beforeAll(configMap: ConfigMap): Unit
/**
* Execute after all tests with access to configuration
* @param configMap configuration passed to the test run
*/
protected def afterAll(configMap: ConfigMap): Unit
}Usage Examples:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfterAll
class IntegrationTestSpec extends AnyFunSuite with Matchers with BeforeAndAfterAll {
var server: TestServer = _
var database: TestDatabase = _
override def beforeAll(): Unit = {
// Expensive setup once per suite
database = new TestDatabase()
database.migrate()
server = new TestServer(database)
server.start()
println("Test environment ready")
}
override def afterAll(): Unit = {
// Cleanup once per suite
try {
server.stop()
database.cleanup()
} catch {
case e: Exception => println(s"Cleanup error: ${e.getMessage}")
}
}
test("API should create users") {
val response = server.post("/users", """{"name": "Alice"}""")
response.status should equal(201)
response.body should include("Alice")
}
test("API should list users") {
server.post("/users", """{"name": "Bob"}""")
val response = server.get("/users")
response.status should equal(200)
response.body should include("Bob")
}
}Pass specific fixture objects to each test method using the fixture traits.
/**
* Base for fixture-based test suites
*/
trait fixture.TestSuite extends Suite {
/**
* The type of fixture object passed to tests
*/
type FixtureParam
/**
* Create and potentially cleanup fixture for each test
* @param test the test function that receives the fixture
*/
def withFixture(test: OneArgTest): Outcome
}
/**
* Fixture variants for all test styles
*/
abstract class fixture.FunSuite extends fixture.TestSuite {
/**
* Register test that receives fixture parameter
* @param testName name of the test
* @param testFun test function receiving fixture
*/
protected def test(testName: String)(testFun: FixtureParam => Any): Unit
}
// Similar fixture variants available:
// fixture.FlatSpec, fixture.WordSpec, fixture.FreeSpec,
// fixture.FunSpec, fixture.FeatureSpec, fixture.PropSpecUsage Examples:
import org.scalatest.fixture
import org.scalatest.matchers.should.Matchers
import org.scalatest.Outcome
class FixtureExampleSpec extends fixture.FunSuite with Matchers {
// Define the fixture type
type FixtureParam = TestDatabase
// Create fixture for each test
def withFixture(test: OneArgTest): Outcome = {
val database = new TestDatabase()
database.connect()
database.createTestTables()
try {
test(database) // Pass fixture to test
} finally {
database.dropTestTables()
database.disconnect()
}
}
test("user operations with database fixture") { db =>
val user = User("Alice", "alice@example.com")
db.save(user)
val retrieved = db.findByEmail("alice@example.com")
retrieved should be(defined)
retrieved.get.name should equal("Alice")
}
test("concurrent user access") { db =>
val user1 = User("Bob", "bob@example.com")
val user2 = User("Charlie", "charlie@example.com")
db.save(user1)
db.save(user2)
db.count() should equal(2)
}
}Combine multiple fixture patterns using stackable traits.
/**
* Stack multiple fixture traits together
*/
trait fixture.TestSuiteMixin extends fixture.TestSuite {
// Stackable fixture behavior
abstract override def withFixture(test: OneArgTest): Outcome
}Usage Examples:
import org.scalatest.fixture
import org.scalatest.matchers.should.Matchers
import org.scalatest.{Outcome, BeforeAndAfterAll}
// Stackable database fixture
trait DatabaseFixture extends fixture.TestSuiteMixin {
this: fixture.TestSuite =>
type FixtureParam = TestDatabase
abstract override def withFixture(test: OneArgTest): Outcome = {
val database = new TestDatabase()
database.connect()
try {
super.withFixture(test.toNoArgTest(database))
} finally {
database.disconnect()
}
}
}
// Stackable HTTP client fixture
trait HttpClientFixture extends fixture.TestSuiteMixin {
this: fixture.TestSuite =>
abstract override def withFixture(test: OneArgTest): Outcome = {
val httpClient = new TestHttpClient()
httpClient.configure()
try {
super.withFixture(test)
} finally {
httpClient.cleanup()
}
}
}
class ComposedFixtureSpec extends fixture.FunSuite
with Matchers with BeforeAndAfterAll
with DatabaseFixture with HttpClientFixture {
test("integration test with multiple fixtures") { db =>
// Both database and HTTP client are available
val user = User("Integration", "integration@test.com")
db.save(user)
// HTTP client configured by HttpClientFixture
val response = httpClient.get(s"/users/${user.id}")
response.status should equal(200)
}
}Use the loan pattern for automatic resource management with try-finally semantics.
Usage Examples:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class LoanPatternSpec extends AnyFunSuite with Matchers {
// Loan pattern helper
def withTempFile[T](content: String)(testCode: java.io.File => T): T = {
val tempFile = java.io.File.createTempFile("test", ".txt")
java.nio.file.Files.write(tempFile.toPath, content.getBytes)
try {
testCode(tempFile) // Loan the resource
} finally {
tempFile.delete() // Always cleanup
}
}
def withDatabase[T](testCode: TestDatabase => T): T = {
val db = new TestDatabase()
db.connect()
db.createTestTables()
try {
testCode(db)
} finally {
db.dropTestTables()
db.disconnect()
}
}
test("file processing with loan pattern") {
withTempFile("Hello, World!") { file =>
val content = scala.io.Source.fromFile(file).mkString
content should equal("Hello, World!")
file.exists() should be(true)
}
// File automatically deleted after test
}
test("database operations with loan pattern") {
withDatabase { db =>
val user = User("Loan", "loan@example.com")
db.save(user)
db.findAll() should have size 1
db.findByEmail("loan@example.com") should be(defined)
}
// Database automatically cleaned up
}
test("nested loan patterns") {
withDatabase { db =>
withTempFile("config data") { configFile =>
// Both resources available within nested scope
val config = loadConfig(configFile)
db.updateConfig(config)
db.getConfig() should equal(config)
}
// File cleaned up, database still available
db.getConfig() should not be null
}
// Database cleaned up
}
}Create a new test class instance for each test method, ensuring complete isolation.
/**
* Create new instance of test class for each test method
*/
trait OneInstancePerTest extends Suite {
// Each test gets a fresh instance of the test class
// Useful for mutable fixture state
}Usage Examples:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import org.scalatest.OneInstancePerTest
class IsolatedStateSpec extends AnyFunSuite with Matchers with OneInstancePerTest {
// Mutable state - each test gets a fresh instance
var counter = 0
val cache = scala.collection.mutable.Map[String, String]()
test("first test modifies state") {
counter = 42
cache("key1") = "value1"
counter should equal(42)
cache should have size 1
}
test("second test sees fresh state") {
// Fresh instance - counter is 0, cache is empty
counter should equal(0)
cache should be(empty)
counter = 100
cache("key2") = "value2"
}
test("third test also sees fresh state") {
// Another fresh instance
counter should equal(0)
cache should be(empty)
}
}Access test metadata and configuration in lifecycle hooks.
/**
* Test metadata available in lifecycle hooks
*/
case class TestData(
name: String, // Test name
configMap: ConfigMap, // Configuration passed to test run
tags: Set[String] // Tags applied to the test
)
/**
* Configuration map for test runs
*/
type ConfigMap = Map[String, Any]Usage Examples:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import org.scalatest.{BeforeAndAfterEachTestData, TestData, Tag}
object SlowTest extends Tag("slow")
object DatabaseTest extends Tag("database")
class TestDataExampleSpec extends AnyFunSuite with Matchers with BeforeAndAfterEachTestData {
override def beforeEach(testData: TestData): Unit = {
println(s"Starting test: ${testData.name}")
if (testData.tags.contains(SlowTest.name)) {
println("This is a slow test - setting longer timeout")
}
if (testData.tags.contains(DatabaseTest.name)) {
println("This test needs database - ensuring connection")
}
// Access configuration
val environment = testData.configMap.getOrElse("env", "test")
println(s"Running in environment: $environment")
}
override def afterEach(testData: TestData): Unit = {
println(s"Finished test: ${testData.name}")
if (testData.tags.contains(DatabaseTest.name)) {
println("Cleaning up database resources")
}
}
test("fast unit test") {
// Regular test
1 + 1 should equal(2)
}
test("slow integration test", SlowTest) {
// Tagged as slow test
Thread.sleep(100)
"slow operation" should not be empty
}
test("database test", DatabaseTest) {
// Tagged as database test
"database operation" should not be empty
}
test("complex test", SlowTest, DatabaseTest) {
// Multiple tags
"complex operation" should not be empty
}
}Fixtures for asynchronous test suites that return Future[Assertion].
/**
* Async fixture support
*/
abstract class fixture.AsyncFunSuite extends fixture.AsyncTestSuite {
protected def test(testName: String)(testFun: FixtureParam => Future[compatible.Assertion]): Unit
}
// Available for all async test styles:
// fixture.AsyncFlatSpec, fixture.AsyncWordSpec, etc.Usage Examples:
import org.scalatest.fixture
import org.scalatest.matchers.should.Matchers
import scala.concurrent.{Future, ExecutionContext}
class AsyncFixtureSpec extends fixture.AsyncFunSuite with Matchers {
type FixtureParam = AsyncTestService
def withFixture(test: OneArgAsyncTest): FutureOutcome = {
val service = new AsyncTestService()
// Setup
val setupFuture = service.initialize()
// Run test with cleanup
val testFuture = setupFuture.flatMap { _ =>
val testResult = test(service)
testResult.toFuture.andThen {
case _ => service.cleanup() // Cleanup regardless of result
}
}
new FutureOutcome(testFuture)
}
test("async service operation") { service =>
for {
result <- service.processData("test data")
status <- service.getStatus()
} yield {
result should not be empty
status should equal("processing_complete")
}
}
}trait DatabaseTestSupport extends BeforeAndAfterEach {
this: Suite =>
var db: TestDatabase = _
override def beforeEach(): Unit = {
super.beforeEach()
db = TestDatabase.create()
db.runMigrations()
}
override def afterEach(): Unit = {
try {
db.cleanup()
} finally {
super.afterEach()
}
}
}trait ExternalServiceFixture extends BeforeAndAfterAll {
this: Suite =>
var mockServer: MockWebServer = _
override def beforeAll(): Unit = {
super.beforeAll()
mockServer = new MockWebServer()
mockServer.start()
}
override def afterAll(): Unit = {
try {
mockServer.shutdown()
} finally {
super.afterAll()
}
}
}class ConfigurableTestSpec extends AnyFunSuite with BeforeAndAfterAllConfigMap {
var testConfig: TestConfig = _
override def beforeAll(configMap: ConfigMap): Unit = {
val environment = configMap.getOrElse("env", "test").toString
val dbUrl = configMap.getOrElse("db.url", "jdbc:h2:mem:test").toString
testConfig = TestConfig(environment, dbUrl)
}
test("configuration-driven test") {
testConfig.environment should not be empty
testConfig.databaseUrl should startWith("jdbc:")
}
}