Flexible fixture system for managing test resources with both synchronous and asynchronous support. Fixtures provide setup and teardown functionality for tests that need external resources like databases, files, or network connections.
Base class for all fixture types, providing the core lifecycle methods for resource management.
/**
* Base class for all fixtures that manage test resources
* @param fixtureName Name identifier for the fixture
*/
abstract class AnyFixture[T](val fixtureName: String) {
/** Get the fixture value (the managed resource) */
def apply(): T
/** Setup performed once before all tests in the suite */
def beforeAll(): Any = ()
/** Setup performed before each individual test */
def beforeEach(context: BeforeEach): Any = ()
/** Cleanup performed after each individual test */
def afterEach(context: AfterEach): Any = ()
/** Cleanup performed once after all tests in the suite */
def afterAll(): Any = ()
}Fixture for synchronous resource management where setup and teardown operations complete immediately.
/**
* Synchronous fixture for resources that don't require async operations
* @param name The fixture name
*/
abstract class Fixture[T](name: String) extends AnyFixture[T](name) {
/** Synchronous setup before all tests */
override def beforeAll(): Unit = ()
/** Synchronous setup before each test */
override def beforeEach(context: BeforeEach): Unit = ()
/** Synchronous cleanup after each test */
override def afterEach(context: AfterEach): Unit = ()
/** Synchronous cleanup after all tests */
override def afterAll(): Unit = ()
}Usage Examples:
class DatabaseFixture extends Fixture[Database]("database") {
private var db: Database = _
def apply(): Database = db
override def beforeAll(): Unit = {
db = Database.createInMemory()
db.migrate()
}
override def afterAll(): Unit = {
db.close()
}
override def beforeEach(context: BeforeEach): Unit = {
db.clearAll()
db.seedTestData()
}
}
class DatabaseTests extends FunSuite {
val database = new DatabaseFixture()
override def munitFixtures = List(database)
test("user creation") {
val db = database()
val user = db.createUser("Alice", "alice@example.com")
assertEquals(user.name, "Alice")
}
test("user lookup") {
val db = database()
db.createUser("Bob", "bob@example.com")
val found = db.findUserByEmail("bob@example.com")
assert(found.isDefined)
}
}Fixture for asynchronous resource management where setup and teardown operations return Future[Unit].
/**
* Asynchronous fixture for resources requiring async setup/teardown
* @param name The fixture name
*/
abstract class FutureFixture[T](name: String) extends AnyFixture[T](name) {
/** Asynchronous setup before all tests */
override def beforeAll(): Future[Unit] = Future.successful(())
/** Asynchronous setup before each test */
override def beforeEach(context: BeforeEach): Future[Unit] = Future.successful(())
/** Asynchronous cleanup after each test */
override def afterEach(context: AfterEach): Future[Unit] = Future.successful(())
/** Asynchronous cleanup after all tests */
override def afterAll(): Future[Unit] = Future.successful(())
}Usage Examples:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
class HttpServerFixture extends FutureFixture[HttpServer]("httpServer") {
private var server: HttpServer = _
def apply(): HttpServer = server
override def beforeAll(): Future[Unit] = {
server = new HttpServer()
server.start().map(_ => ())
}
override def afterAll(): Future[Unit] = {
server.stop()
}
override def beforeEach(context: BeforeEach): Future[Unit] = {
server.clearRoutes().map(_ => ())
}
}
class HttpServerTests extends FunSuite {
val httpServer = new HttpServerFixture()
override def munitFixtures = List(httpServer)
test("server responds to GET") {
val server = httpServer()
for {
_ <- server.addRoute("GET", "/hello", "Hello, World!")
response <- server.get("/hello")
} yield {
assertEquals(response.body, "Hello, World!")
}
}
}Function-style fixtures that provide a more flexible approach to resource management with per-test setup and teardown.
/**
* Trait providing function-style fixture support (mixed into BaseFunSuite)
*/
trait FunFixtures {
/**
* Function-style fixture that provides setup/teardown per test
*/
class FunFixture[T] private (
setup: TestOptions => Future[T],
teardown: T => Future[Unit]
) {
/** Define a test that uses this fixture */
def test(name: String)(body: T => Any)(implicit loc: Location): Unit
/** Define a test with options that uses this fixture */
def test(options: TestOptions)(body: T => Any)(implicit loc: Location): Unit
}
}
/**
* Factory methods for creating FunFixtures
*/
object FunFixture {
/** Create a synchronous fixture */
def apply[T](
setup: TestOptions => T,
teardown: T => Unit
): FunFixture[T]
/** Create an asynchronous fixture */
def async[T](
setup: TestOptions => Future[T],
teardown: T => Future[Unit]
): FunFixture[T]
/** Combine two fixtures into a tuple */
def map2[A, B](
a: FunFixture[A],
b: FunFixture[B]
): FunFixture[(A, B)]
/** Combine three fixtures into a tuple */
def map3[A, B, C](
a: FunFixture[A],
b: FunFixture[B],
c: FunFixture[C]
): FunFixture[(A, B, C)]
}Usage Examples:
class FunFixtureExamples extends FunSuite {
// Simple synchronous fixture
val tempDir = FunFixture[Path](
setup = { _ => Files.createTempDirectory("test") },
teardown = { dir => Files.deleteRecursively(dir) }
)
tempDir.test("file operations") { dir =>
val file = dir.resolve("test.txt")
Files.write(file, "Hello, World!")
val content = Files.readString(file)
assertEquals(content, "Hello, World!")
}
// Asynchronous fixture
val httpClient = FunFixture.async[HttpClient](
setup = { _ => HttpClient.create() },
teardown = { client => client.close() }
)
httpClient.test("HTTP request") { client =>
client.get("https://api.example.com/status").map { response =>
assertEquals(response.status, 200)
}
}
// Fixture with test-specific setup
val database = FunFixture.async[Database](
setup = { testOptions =>
val dbName = s"test_${testOptions.name.replaceAll("\\s+", "_")}"
Database.create(dbName).map { db =>
db.migrate()
db
}
},
teardown = { db => db.drop() }
)
database.test("user operations") { db =>
for {
user <- db.createUser("Alice")
found <- db.findUser(user.id)
} yield {
assertEquals(found.map(_.name), Some("Alice"))
}
}
// Combined fixtures
val dbAndClient = FunFixture.map2(database, httpClient)
dbAndClient.test("integration test") { case (db, client) =>
for {
user <- db.createUser("Bob")
response <- client.post("/api/users", user.toJson)
} yield {
assertEquals(response.status, 201)
}
}
}Context objects passed to fixture lifecycle methods containing information about the current test.
/**
* Context passed to beforeEach methods
* @param test The test that is about to run
*/
class BeforeEach(val test: Test) extends Serializable
/**
* Context passed to afterEach methods
* @param test The test that just completed
*/
class AfterEach(val test: Test) extends SerializableUsage Examples:
class LoggingFixture extends Fixture[Logger]("logger") {
private val logger = Logger.getLogger("test")
def apply(): Logger = logger
override def beforeEach(context: BeforeEach): Unit = {
logger.info(s"Starting test: ${context.test.name}")
}
override def afterEach(context: AfterEach): Unit = {
logger.info(s"Completed test: ${context.afterEach.test.name}")
}
}class ConnectionPoolFixture extends FutureFixture[ConnectionPool]("connectionPool") {
private var pool: ConnectionPool = _
def apply(): ConnectionPool = pool
override def beforeAll(): Future[Unit] = {
pool = ConnectionPool.create(maxConnections = 10)
pool.initialize()
}
override def afterAll(): Future[Unit] = {
pool.shutdown()
}
}class MockServerFixture extends Fixture[MockServer]("mockServer") {
private var server: MockServer = _
def apply(): MockServer = server
override def beforeAll(): Unit = {
server = MockServer.start(8080)
}
override def afterAll(): Unit = {
server.stop()
}
override def beforeEach(context: BeforeEach): Unit = {
server.reset() // Clear all mocked endpoints
}
}val testData = FunFixture[TestData](
setup = { testOptions =>
val data = if (testOptions.tags.contains(new Tag("large-dataset"))) {
TestData.generateLarge()
} else {
TestData.generateSmall()
}
data
},
teardown = { data => data.cleanup() }
)