# Lifecycle Hooks

Comprehensive guide to setup and cleanup hooks in TUnit tests.

## Table of Contents

- [Hook Levels](#hook-levels)
- [Hook Method Signatures](#hook-method-signatures)
- [Instance Hooks](#instance-hooks)
- [Class Hooks](#class-hooks)
- [Global Hooks](#global-hooks)
- [Common Patterns](#common-patterns)
- [Best Practices](#best-practices)

## Hook Levels

TUnit provides hooks at different levels:

| Hook Type | Scope | When It Runs | Method Type |
|-----------|-------|--------------|-------------|
| `[Before(Test)]` | Per test | Before each test | Instance |
| `[After(Test)]` | Per test | After each test | Instance |
| `[Before(Class)]` | Per class | Once before first test in class | Static |
| `[After(Class)]` | Per class | Once after all tests in class | Static |
| `[BeforeEvery(Test)]` | Global | Before every test in session | Static |
| `[AfterEvery(Test)]` | Global | After every test in session | Static |
| `[Before(TestSession)]` | Session | Once before test session | Static |
| `[After(TestSession)]` | Session | Once after test session | Static |

## 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 Case | Hook Type | Reason |
|----------|-----------|--------|
| 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
}
```

2. **Null-check in cleanup**:
```csharp
[After(Test)]
public void Cleanup()
{
    _client?.Dispose();  // Handles case where Setup failed
}
```

3. **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:
   - `[BeforeEvery(Test)]`
   - `[Before(Test)]` (base class first, then derived)
   - **Test executes**
   - `[After(Test)]` (derived class first, then base)
   - `[AfterEvery(Test)]`
5. `[After(Class)]` (once per class)
6. `[AfterEvery(TestSession)]` (once)
