Categories
Agent Tooling (71)
- .agents/skills/agent-browser/references/authentication.md
- .agents/skills/agent-browser/references/commands.md
- .agents/skills/agent-browser/references/profiling.md
- .agents/skills/agent-browser/references/proxy-support.md
- .agents/skills/agent-browser/references/session-management.md
- .agents/skills/agent-browser/references/snapshot-refs.md
- .agents/skills/agent-browser/references/video-recording.md
- .agents/skills/agent-browser/SKILL.md
- .agents/skills/aspire-deployment/references/aws.md
- .agents/skills/aspire-deployment/references/azure.md
- .agents/skills/aspire-deployment/references/cicd.md
- .agents/skills/aspire-deployment/references/docker-compose.md
- .agents/skills/aspire-deployment/references/javascript.md
- .agents/skills/aspire-deployment/references/kubernetes.md
- .agents/skills/aspire-deployment/references/preflight.md
- .agents/skills/aspire-deployment/SKILL.md
- .agents/skills/aspire-monitoring/references/diagnostics-bridge.md
- .agents/skills/aspire-monitoring/references/monitoring.md
- .agents/skills/aspire-monitoring/references/playwright-handoff.md
- .agents/skills/aspire-monitoring/SKILL.md
- .agents/skills/aspire-orchestration/references/agent-workflows.md
- .agents/skills/aspire-orchestration/references/app-commands.md
- .agents/skills/aspire-orchestration/references/detection.md
- .agents/skills/aspire-orchestration/references/resource-management.md
- .agents/skills/aspire-orchestration/references/safety-guardrails.md
- .agents/skills/aspire-orchestration/SKILL.md
- .agents/skills/aspire/references/aspire-13-3-breaking-changes.md
- .agents/skills/aspire/SKILL.md
- .agents/skills/aspireify/references/apphost-wiring.md
- .agents/skills/aspireify/references/csharp-authoring.md
- .agents/skills/aspireify/references/docker-compose.md
- .agents/skills/aspireify/references/full-solution-apphosts.md
- .agents/skills/aspireify/references/javascript-apps.md
- .agents/skills/aspireify/references/opentelemetry.md
- .agents/skills/aspireify/references/scan-and-propose.md
- .agents/skills/aspireify/references/service-defaults.md
- .agents/skills/aspireify/references/typescript-authoring.md
- .agents/skills/aspireify/references/validation.md
- .agents/skills/aspireify/SKILL.md
- .agents/skills/caveman-compress/README.md
- .agents/skills/caveman-compress/SECURITY.md
- .agents/skills/caveman-compress/SKILL.md
- .agents/skills/caveman-help/README.md
- .agents/skills/caveman-help/SKILL.md
- .agents/skills/caveman-stats/README.md
- .agents/skills/caveman-stats/SKILL.md
- .agents/skills/caveman/README.md
- .agents/skills/caveman/SKILL.md
- .agents/skills/csharp-developer/SKILL.md
- .agents/skills/dotnet-inspect/SKILL.md
- .agents/skills/efcore-data-layer/SKILL.md
- .agents/skills/git/SKILL.md
- .agents/skills/microsoft-code-reference/SKILL.md
- .agents/skills/microsoft-docs/SKILL.md
- .agents/skills/microsoft-skill-creator/references/skill-templates.md
- .agents/skills/microsoft-skill-creator/SKILL.md
- .agents/skills/skill-creator/references/output-patterns.md
- .agents/skills/skill-creator/references/workflows.md
- .agents/skills/skill-creator/SKILL.md
- .agents/skills/tunit-testing/references/aspire-integration.md
- .agents/skills/tunit-testing/references/aspnetcore-integration.md
- .agents/skills/tunit-testing/references/assertions.md
- .agents/skills/tunit-testing/references/data-driven.md
- .agents/skills/tunit-testing/references/infrastructure-and-troubleshooting.md
- .agents/skills/tunit-testing/references/lifecycle-hooks.md
- .agents/skills/tunit-testing/SKILL.md
- AGENTS.md
- backend/AGENTS.md
- backend/tests/AGENTS.md
- Front/AGENT.md
- opencode.jsonc
Lifecycle Hooks
Comprehensive guide to setup and cleanup hooks in TUnit tests.
Table of Contents
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:
// 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:
// 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.
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):
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.
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).
// 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.
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
newthe injected dependency inside the dependent fixture - do not manually dispose injected dependencies from the dependent fixture
Database Setup
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
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
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
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
[Before(Class)]
public async Task Setup() // Won't compile - must be static
{
await Container.StartAsync();
}✅ Correct: Static method for class hook
[Before(Class)]
public static async Task Setup()
{
await Container.StartAsync();
}❌ Wrong: Static field accessed in instance hook
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
private HttpClient _client = null!;
[Before(Test)]
public void Setup()
{
_client = Factory.CreateClient();
}Cleanup Best Practices
- Always cleanup what you setup:
[Before(Test)]
public async Task Setup()
{
_resource = CreateResource();
}
[After(Test)]
public async Task Cleanup()
{
await _resource.DisposeAsync(); // Always cleanup
}- Null-check in cleanup:
[After(Test)]
public void Cleanup()
{
_client?.Dispose(); // Handles case where Setup failed
}- Use IAsyncDisposable when possible:
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:
[BeforeEvery(TestSession)](once)[BeforeEvery(Class)](once per class)[Before(Class)](once per class)- For each test:
[BeforeEvery(Test)][Before(Test)](base class first, then derived)- Test executes
[After(Test)](derived class first, then base)[AfterEvery(Test)][After(Class)](once per class)[AfterEvery(TestSession)](once)