or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

assertions.mdconfiguration.mdexceptions.mdfixtures.mdindex.mdtest-suites.mdtransforms.md
tile.json

fixtures.mddocs/

Fixtures

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.

Capabilities

AnyFixture - Base Fixture Class

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 - Synchronous Fixture

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)
  }
}

FutureFixture - Asynchronous Fixture

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!")
    }
  }
}

FunFixtures - Function-Style Fixtures

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)
    }
  }
}

Lifecycle Context

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 Serializable

Usage 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}")
  }
}

Fixture Patterns

Resource Pools

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()
  }
}

External Services

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
  }
}

Test Data

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() }
)