Spensara Spensara Knowledge Base & Documentation Repository

Repository Docs & Knowledge Base

Consolidated Spensara documentation repository (722 Markdown & architecture documents in ../repository)
Built Word Documents (.docx): Spensara Overview (.docx) Capability Assessment (.docx) Brink POS Ingestion Analysis (.docx) The Borg Ingestion Architecture (.docx)

Agent Tooling (71)

Automation workflows, orchestration references, and developer tooling guides.
agent-tooling/.agents/skills/tunit-testing/references/lifecycle-hooks.md 11,443 bytes · Updated Oct 23, 2026 10:44 AM EDT
Raw File

Lifecycle Hooks

Comprehensive guide to setup and cleanup hooks in TUnit tests.

Table of Contents

Hook Levels

TUnit provides hooks at different levels:

Hook TypeScopeWhen It RunsMethod Type
[Before(Test)]Per testBefore each testInstance
[After(Test)]Per testAfter each testInstance
[Before(Class)]Per classOnce before first test in classStatic
[After(Class)]Per classOnce after all tests in classStatic
[BeforeEvery(Test)]GlobalBefore every test in sessionStatic
[AfterEvery(Test)]GlobalAfter every test in sessionStatic
[Before(TestSession)]SessionOnce before test sessionStatic
[After(TestSession)]SessionOnce after test sessionStatic

Hook Method Signatures

Hooks can be synchronous or asynchronous:

csharp
// Synchronous
[Before(Test)]
public void Setup()
{
    _value = 99;
}

// Asynchronous
[Before(Test)]
public async Task SetupAsync()
{
    await _service.InitializeAsync();
}

Note: async void is NOT allowed.

Hook Parameters

Hooks can accept optional parameters:

csharp
// With context
[Before(Test)]
public async Task Setup(TestContext context)
{
    Console.WriteLine($"Starting: {context.Metadata.TestName}");
}

// With cancellation token
[Before(Test)]
public async Task Setup(CancellationToken cancellationToken)
{
    await LongOperation(cancellationToken);
}

// With both
[Before(Test)]
public async Task Setup(TestContext context, CancellationToken ct)
{
    Console.WriteLine($"Starting: {context.Metadata.TestName}");
    await LongOperation(ct);
}

Context types by hook level:

  • [Before(Test)] → TestContext
  • [Before(Class)] → ClassHookContext
  • [Before(Assembly)] → AssemblyHookContext
  • [Before(TestSession)] → TestSessionContext

Instance Hooks

Run before/after each test in the class. Must be instance methods.

csharp
public class MyTests : TestsBase
{
    private HttpClient _client = null!;
    private MyService _service = null!;

    [Before(Test)]
    public async Task Setup()
    {
        // Runs before each test
        _client = new HttpClient();
        _service = new MyService();
        await _service.InitializeAsync();
    }

    [After(Test)]
    public async Task Cleanup()
    {
        // Runs after each test
        await _service.DisposeAsync();
        _client.Dispose();
    }

    [Test]
    public async Task Test1()
    {
        // Uses fresh _client and _service
    }

    [Test]
    public async Task Test2()
    {
        // Gets NEW _client and _service (not same as Test1)
    }
}

Multiple Instance Hooks

Multiple hooks execute in order (base class first):

csharp
public class BaseTests
{
    [Before(Test)]
    public void BaseSetup() => Console.WriteLine("1. Base setup");
}

public class DerivedTests : BaseTests
{
    [Before(Test)]
    public void DerivedSetup() => Console.WriteLine("2. Derived setup");

    [Test]
    public void Test()
    {
        // Executes: BaseSetup → DerivedSetup → Test
    }
}

Class Hooks

Run once per class. Must be static methods.

csharp
public class DatabaseTests
{
    private static PostgreSqlContainer _container = null!;
    private static string _connectionString = null!;

    [Before(Class)]
    public static async Task ClassSetup()
    {
        // Runs once before first test
        _container = new PostgreSqlBuilder().Build();
        await _container.StartAsync();
        _connectionString = _container.GetConnectionString();
    }

    [After(Class)]
    public static async Task ClassCleanup()
    {
        // Runs once after all tests
        await _container.DisposeAsync();
    }

    [Test]
    public async Task Test1() { /* Uses _connectionString */ }

    [Test]
    public async Task Test2() { /* Uses same _connectionString */ }
}

Use for:

  • Starting/stopping containers
  • Creating expensive shared resources
  • One-time setup that applies to all tests in class

Global Hooks

Apply to every test/class in the session. Must be static and placed in their own file (e.g., GlobalSetup.cs).

csharp
// GlobalSetup.cs
public static class GlobalHooks
{
    [BeforeEvery(Test)]
    public static void BeforeEveryTest(TestContext context)
    {
        // Runs before EVERY test in the session
        Console.WriteLine($"Starting: {context.Metadata.TestName}");
    }

    [AfterEvery(Test)]
    public static void AfterEveryTest(TestContext context)
    {
        // Runs after EVERY test in the session
        Console.WriteLine($"Completed: {context.Metadata.TestName}");
    }

    [BeforeEvery(Class)]
    public static void BeforeEveryClass(ClassHookContext context)
    {
        // Runs before first test of EVERY class
    }

    [Before(TestSession)]
    public static void SessionSetup()
    {
        // Runs once before entire test session
    }

    [After(TestSession)]
    public static void SessionCleanup()
    {
        // Runs once after entire test session
    }
}

Important: Global hooks should be easy to find as they affect the entire test suite.

Common Patterns

Shared Infrastructure Via Property Injection

For expensive shared resources such as Docker networks, containers, brokers, or shared databases, prefer TUnit property injection chains over manual construction.

csharp
public sealed class DockerNetwork : IAsyncInitializer, IAsyncDisposable
{
    public INetwork Instance { get; } = new NetworkBuilder().Build();

    public Task InitializeAsync() => Instance.CreateAsync();
    public ValueTask DisposeAsync() => Instance.DisposeAsync();
}

public sealed class PostgresDatabase : IAsyncInitializer, IAsyncDisposable
{
    [ClassDataSource<DockerNetwork>(Shared = SharedType.PerTestSession)]
    public required DockerNetwork DockerNetwork { get; init; }

    public PostgreSqlContainer Container => field ??= new PostgreSqlBuilder()
        .WithNetwork(DockerNetwork.Instance)
        .Build();

    public async Task InitializeAsync() => await Container.StartAsync();
    public async ValueTask DisposeAsync() => await Container.DisposeAsync();
}

Guidance:

  • let TUnit initialize dependencies in order
  • do not manually new the injected dependency inside the dependent fixture
  • do not manually dispose injected dependencies from the dependent fixture

Database Setup

csharp
public class RepositoryTests
{
    private static PostgreSqlContainer _db = null!;
    private KdsAiDbContext _dbContext = null!;

    [Before(Class)]
    public static async Task SetupDatabase()
    {
        _db = new PostgreSqlBuilder().Build();
        await _db.StartAsync();
    }

    [Before(Test)]
    public async Task SetupTest()
    {
        var options = new DbContextOptionsBuilder<KdsAiDbContext>()
            .UseNpgsql(_db.GetConnectionString())
            .Options;
        
        _dbContext = new KdsAiDbContext(options);
        await _dbContext.Database.MigrateAsync();
    }

    [After(Test)]
    public async Task CleanupTest()
    {
        await _dbContext.Database.EnsureDeletedAsync();
        await _dbContext.DisposeAsync();
    }

    [After(Class)]
    public static async Task CleanupDatabase()
    {
        await _db.DisposeAsync();
    }
}

HTTP Client Setup

csharp
public class ApiTests : TestsBase
{
    private HttpClient _client = null!;

    [Before(Test)]
    public void Setup()
    {
        _client = Factory.CreateClient();
    }

    [After(Test)]
    public void Cleanup()
    {
        _client.Dispose();
    }
}

Shared Test Data

csharp
public class ValidationTests
{
    private static List<TestCase> _testCases = null!;

    [Before(Class)]
    public static async Task LoadTestData()
    {
        // Load once for all tests
        var json = await File.ReadAllTextAsync("testdata.json");
        _testCases = JsonSerializer.Deserialize<List<TestCase>>(json)!;
    }

    [Test]
    public async Task Test1()
    {
        // Use _testCases
    }
}

Logging Setup

csharp
public static class GlobalHooks
{
    [BeforeEvery(Test)]
    public static void LogTestStart(TestContext context)
    {
        var testName = context.Metadata.TestName;
        var className = context.TestDetails.ClassType.Name;
        Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] Starting {className}.{testName}");
    }

    [AfterEvery(Test)]
    public static void LogTestEnd(TestContext context)
    {
        var testName = context.Metadata.TestName;
        var result = context.Result?.Status ?? "Unknown";
        Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] Finished {testName}: {result}");
    }
}

Best Practices

Hook Scope Selection

Use CaseHook TypeReason
Database connection[Before(Class)]Expensive, share across tests
HTTP client[Before(Test)]Fresh client per test
Test data seeding[Before(Test)]Isolation between tests
Container startup[Before(Class)]Slow, share container
Logging[BeforeEvery(Test)]Apply to all tests

Common Mistakes

❌ Wrong: Instance method for class hook

csharp
[Before(Class)]
public async Task Setup()  // Won't compile - must be static
{
    await Container.StartAsync();
}

✅ Correct: Static method for class hook

csharp
[Before(Class)]
public static async Task Setup()
{
    await Container.StartAsync();
}

❌ Wrong: Static field accessed in instance hook

csharp
private static HttpClient _client;

[Before(Test)]
public void Setup()
{
    // Don't set static in instance hook - creates shared state issues
    _client = Factory.CreateClient();
}

✅ Correct: Instance field in instance hook

csharp
private HttpClient _client = null!;

[Before(Test)]
public void Setup()
{
    _client = Factory.CreateClient();
}

Cleanup Best Practices

  1. Always cleanup what you setup:
csharp
[Before(Test)]
public async Task Setup()
{
    _resource = CreateResource();
}

[After(Test)]
public async Task Cleanup()
{
    await _resource.DisposeAsync();  // Always cleanup
}
  1. Null-check in cleanup:
csharp
[After(Test)]
public void Cleanup()
{
    _client?.Dispose();  // Handles case where Setup failed
}
  1. Use IAsyncDisposable when possible:
csharp
public class MyTests : TestsBase, IAsyncDisposable
{
    private MyService _service = null!;

    [Before(Test)]
    public async Task Setup()
    {
        _service = new MyService();
    }

    public async ValueTask DisposeAsync()
    {
        await _service.DisposeAsync();
    }
}

Execution Order

For a single test class:

  1. [BeforeEvery(TestSession)] (once)
  2. [BeforeEvery(Class)] (once per class)
  3. [Before(Class)] (once per class)
  4. For each test:
  5. [BeforeEvery(Test)]
  6. [Before(Test)] (base class first, then derived)
  7. Test executes
  8. [After(Test)] (derived class first, then base)
  9. [AfterEvery(Test)]
  10. [After(Class)] (once per class)
  11. [AfterEvery(TestSession)] (once)