ScalaTest provides comprehensive support for test fixtures and suite lifecycle management, enabling setup and teardown operations, resource management, and data sharing between tests.
Execute setup and cleanup code before and after each individual test.
trait BeforeAndAfterEach extends SuiteMixin {
/**
* Execute before each test method
*/
def beforeEach(): Unit = ()
/**
* Execute after each test method
*/
def afterEach(): Unit = ()
/**
* Override to customize the execution around each test
*/
abstract override def withFixture(test: NoArgTest): Outcome = {
beforeEach()
try super.withFixture(test)
finally afterEach()
}
}Usage Examples:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.BeforeAndAfterEach
class DatabaseSuite extends AnyFunSuite with BeforeAndAfterEach {
var database: Database = _
override def beforeEach(): Unit = {
database = new Database()
database.connect()
database.createTables()
}
override def afterEach(): Unit = {
database.dropTables()
database.disconnect()
database = null
}
test("should insert user") {
val user = User("John", "john@example.com")
database.insert(user)
database.count("users") should equal (1)
}
test("should delete user") {
val user = User("Jane", "jane@example.com")
database.insert(user)
database.delete(user.id)
database.count("users") should equal (0)
}
}Execute setup and cleanup code once before and after all tests in the suite.
trait BeforeAndAfterAll extends SuiteMixin {
/**
* Execute once before all tests in the suite
*/
def beforeAll(): Unit = ()
/**
* Execute once after all tests in the suite
*/
def afterAll(): Unit = ()
/**
* Override to customize suite-wide setup/teardown
*/
abstract override def run(testName: Option[String], args: Args): Status = {
if (!args.runTestInNewInstance) beforeAll()
try {
val status = super.run(testName, args)
status.waitUntilCompleted()
status
} finally {
if (!args.runTestInNewInstance) afterAll()
}
}
}Usage Examples:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.BeforeAndAfterAll
class ServerSuite extends AnyFunSuite with BeforeAndAfterAll {
var server: TestServer = _
override def beforeAll(): Unit = {
server = new TestServer(port = 8080)
server.start()
server.waitUntilReady()
}
override def afterAll(): Unit = {
server.stop()
server.waitUntilStopped()
}
test("server should respond to health check") {
val response = httpGet(s"http://localhost:8080/health")
response.status should equal (200)
response.body should include ("OK")
}
test("server should handle API requests") {
val response = httpPost(s"http://localhost:8080/api/users", """{"name": "test"}""")
response.status should equal (201)
}
}Share fixtures between tests using parameterized test functions.
trait FixtureTestSuite extends TestSuite {
/**
* The type of fixture object passed to tests
*/
type FixtureParam
/**
* Create and cleanup fixture for each test
*/
def withFixture(test: OneArgTest): Outcome
/**
* Run a single test with fixture
*/
def runTest(testName: String, args: Args): Status = {
val oneArgTest = new OneArgTest {
val name = testName
def apply(fixture: FixtureParam): Outcome = {
// Test implementation provided by concrete suite
}
}
withFixture(oneArgTest).toStatus
}
}
/**
* Fixture-enabled FunSuite
*/
abstract class FixtureAnyFunSuite extends FixtureTestSuite with TestRegistration {
/**
* Register a test that requires a fixture
*/
protected def test(testName: String)(testFun: FixtureParam => Any): Unit
}Usage Examples:
import org.scalatest.fixture.AnyFunSuite
import java.io.{File, FileWriter}
class FileFixtureSuite extends AnyFunSuite {
// Fixture is a temporary file
type FixtureParam = File
def withFixture(test: OneArgTest): Outcome = {
val tempFile = File.createTempFile("test", ".txt")
try {
// Setup: write initial content
val writer = new FileWriter(tempFile)
writer.write("initial content")
writer.close()
// Run test with fixture
test(tempFile)
} finally {
// Cleanup: delete temp file
tempFile.delete()
}
}
test("should read file content") { file =>
val content = scala.io.Source.fromFile(file).mkString
content should include ("initial")
}
test("should append to file") { file =>
val writer = new FileWriter(file, true) // append mode
writer.write("\nappended content")
writer.close()
val content = scala.io.Source.fromFile(file).mkString
content should include ("appended")
}
}Lightweight fixture sharing using traits.
trait FixtureContext {
// Define fixture data and helper methods as trait members
}Usage Examples:
import org.scalatest.funsuite.AnyFunSuite
class ContextFixtureSuite extends AnyFunSuite {
trait DatabaseContext {
val database = new InMemoryDatabase()
database.createSchema()
val testUser = User("testuser", "test@example.com")
database.insert(testUser)
def findUser(name: String): Option[User] = database.findByName(name)
}
trait ApiContext {
val baseUrl = "http://test.example.com"
val apiKey = "test-api-key"
def makeRequest(endpoint: String): HttpResponse = {
// Mock HTTP request implementation
HttpResponse(200, s"Response from $endpoint")
}
}
test("should find existing user") {
new DatabaseContext {
findUser("testuser") should be (defined)
findUser("nonexistent") should be (empty)
}
}
test("should handle API requests") {
new ApiContext {
val response = makeRequest("/users")
response.status should equal (200)
response.body should include ("Response from /users")
}
}
test("should combine contexts") {
new DatabaseContext with ApiContext {
// Use both database and API fixtures
val user = findUser("testuser").get
val response = makeRequest(s"/users/${user.id}")
response.status should equal (200)
}
}
}Access configuration and test data through the Args parameter.
trait BeforeAndAfterEachTestData extends SuiteMixin {
/**
* Execute before each test with access to test data
*/
def beforeEach(testData: TestData): Unit = ()
/**
* Execute after each test with access to test data
*/
def afterEach(testData: TestData): Unit = ()
abstract override def withFixture(test: NoArgTest): Outcome = {
beforeEach(test)
try super.withFixture(test)
finally afterEach(test)
}
}
trait BeforeAndAfterAllConfigMap extends SuiteMixin {
/**
* Execute before all tests with access to config map
*/
def beforeAll(configMap: ConfigMap): Unit = ()
/**
* Execute after all tests with access to config map
*/
def afterAll(configMap: ConfigMap): Unit = ()
}
/**
* Test metadata and configuration
*/
trait TestData {
val name: String
val scopes: Vector[String]
val text: String
val tags: Set[String]
val pos: Option[source.Position]
}
/**
* Configuration map for test execution
*/
class ConfigMap(map: Map[String, Any]) {
def apply(key: String): Any = map(key)
def get(key: String): Option[Any] = map.get(key)
def contains(key: String): Boolean = map.contains(key)
def ++(other: ConfigMap): ConfigMap = new ConfigMap(map ++ other.map)
}Usage Examples:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.{BeforeAndAfterEachTestData, BeforeAndAfterAllConfigMap}
class ConfigurableSuite extends AnyFunSuite
with BeforeAndAfterEachTestData
with BeforeAndAfterAllConfigMap {
var globalConfig: String = _
override def beforeAll(configMap: ConfigMap): Unit = {
globalConfig = configMap.getOrElse("environment", "test").toString
println(s"Running tests in $globalConfig environment")
}
override def beforeEach(testData: TestData): Unit = {
println(s"Starting test: ${testData.name}")
if (testData.tags.contains("slow")) {
println("This is a slow test, please be patient...")
}
}
override def afterEach(testData: TestData): Unit = {
println(s"Completed test: ${testData.name}")
}
test("should use global config") {
globalConfig should not be empty
// Test implementation using globalConfig
}
test("slow database operation", SlowTest) {
// This test will get special logging due to the SlowTest tag
Thread.sleep(100) // Simulate slow operation
succeed
}
}
// Custom tag for marking slow tests
object SlowTest extends Tag("org.example.SlowTest")/**
* Test function that receives no fixture parameter
*/
trait NoArgTest extends (() => Outcome) with TestData {
def apply(): Outcome
val name: String
val scopes: Vector[String]
val text: String
val tags: Set[String]
}
/**
* Test function that receives one fixture parameter
*/
trait OneArgTest extends (FixtureParam => Outcome) with TestData {
def apply(fixture: FixtureParam): Outcome
val name: String
val scopes: Vector[String]
val text: String
val tags: Set[String]
}
/**
* Mixin trait for suites that can be mixed into other suites
*/
trait SuiteMixin { this: Suite =>
// Mixed into Suite to provide additional functionality
}
/**
* Test outcome representing the result of running a test
*/
sealed abstract class Outcome extends Product with Serializable {
def isSucceeded: Boolean
def isFailed: Boolean
def isCanceled: Boolean
def isPending: Boolean
def toStatus: Status
}
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