Test Lifecycle Overview
TUnit provides multiple mechanisms for hooking into the test lifecycle:
- Hook Attributes (
[Before],[After], etc.) - Method-based hooks - Event Receivers (interfaces like
ITestStartEventReceiver) - Object-based event subscriptions - Initialization Interfaces (
IAsyncInitializer,IAsyncDiscoveryInitializer) - Async object setup - Disposal Interfaces (
IDisposable,IAsyncDisposable) - Resource cleanup
Complete Lifecycle Diagram
Phase 1: Test Discovery
Before any tests execute, TUnit discovers all tests and prepares data sources.
Discovery Phase Details
| Step | What Happens |
|---|---|
[Before(TestDiscovery)] | Hook runs once before discovery begins |
| Scan Assemblies | Find all methods with [Test] attribute |
| Create Data Sources | Instantiate ClassDataSource<T>, resolve MethodDataSource, etc. |
| Property Injection | Resolve and cache property values for data sources |
IAsyncDiscoveryInitializer | Initialize objects that need to be ready during discovery |
[After(TestDiscovery)] | Hook runs once after discovery completes |
OnTestRegistered | Event fires for each test after registration |
Discovery vs Execution
IAsyncInitializer does NOT run during discovery. Only IAsyncDiscoveryInitializer runs at discovery time.
Use IAsyncDiscoveryInitializer when your data source needs async initialization to generate test cases (e.g., loading test data from a database).
Phase 2: Test Execution
Per-Test Execution Flow
Complete Test Execution Order
Exact order of operations for a single test:
| Order | What Happens | Type |
|---|---|---|
| 1 | [Before(TestSession)] | Hook (once per session) |
| 2 | IFirstTestInTestSessionEventReceiver | Event (once per session) |
| 3 | [BeforeEvery(Assembly)] / [Before(Assembly)] | Hooks (once per assembly) |
| 4 | IFirstTestInAssemblyEventReceiver | Event (once per assembly) |
| 5 | [BeforeEvery(Class)] / [Before(Class)] | Hooks (once per class) |
| 6 | IFirstTestInClassEventReceiver | Event (once per class) |
| 7 | Create test class instance | Constructor runs |
| 8 | Set property values on instance | Cached values applied |
| 9 | IAsyncInitializer.InitializeAsync() | All tracked objects initialized |
| 10 | [BeforeEvery(Test)] | Hook |
| 11 | ITestStartEventReceiver (Early) | Event |
| 12 | [Before(Test)] | Hook (instance method) |
| 13 | ITestStartEventReceiver (Late) | Event |
| 14 | Test Body Execution | Your test code runs |
| 15 | ITestEndEventReceiver (Early) | Event |
| 16 | [After(Test)] | Hook (instance method) |
| 17 | ITestEndEventReceiver (Late) | Event |
| 18 | [AfterEvery(Test)] | Hook |
| 19 | IAsyncDisposable / IDisposable | Test instance disposed |
| 20 | Cleanup tracked objects | Ref count decremented, dispose if 0 |
| 21 | ILastTestInClassEventReceiver | Event (after last test in class) |
| 22 | [After(Class)] / [AfterEvery(Class)] | Hooks (after last test in class) |
| 23 | ILastTestInAssemblyEventReceiver | Event (after last test in assembly) |
| 24 | [After(Assembly)] / [AfterEvery(Assembly)] | Hooks (after last test in assembly) |
| 25 | ILastTestInTestSessionEventReceiver | Event (after last test in session) |
| 26 | [After(TestSession)] | Hook (once per session) |
Initialization Interfaces
IAsyncInitializer vs IAsyncDiscoveryInitializer
| Interface | When It Runs | Use Case |
|---|---|---|
IAsyncDiscoveryInitializer | During test discovery | Loading data for test case generation |
IAsyncInitializer | During test execution (after [Before(Class)]) | Starting containers, DB connections |
Initialization Order
Objects are initialized depth-first (deepest nested objects first):
// If TestClass has PropertyA, and PropertyA has PropertyB...
// Initialization order: PropertyB → PropertyA → TestClass
Disposal Interfaces
When Disposal Happens
Disposal by Sharing Type
| SharedType | When Disposed |
|---|---|
None (default) | After each test |
PerClass | After last test in the class |
PerAssembly | After last test in the assembly |
PerTestSession | After test session ends |
Keyed | When all tests using that key complete |
Property Injection Lifecycle
Key Points
- Property values are resolved once during test registration
- Shared objects (
PerClass,PerAssembly, etc.) are created once and reused - Each test gets a new instance of the test class
- Cached values are set on each new test instance
IAsyncInitializerruns after[Before(Class)]hooks
Event Receiver Interfaces
All Event Receiver Interfaces
| Interface | When Fired | Context |
|---|---|---|
ITestRegisteredEventReceiver | After test discovered | TestRegisteredContext |
IFirstTestInTestSessionEventReceiver | Before first test in session | TestSessionContext |
IFirstTestInAssemblyEventReceiver | Before first test in assembly | AssemblyHookContext |
IFirstTestInClassEventReceiver | Before first test in class | ClassHookContext |
ITestStartEventReceiver | When test begins | TestContext |
ITestEndEventReceiver | When test completes | TestContext |
ITestSkippedEventReceiver | When test is skipped | TestContext |
ILastTestInClassEventReceiver | After last test in class | ClassHookContext |
ILastTestInAssemblyEventReceiver | After last test in assembly | AssemblyHookContext |
ILastTestInTestSessionEventReceiver | After last test in session | TestSessionContext |
Early vs Late Stage
For ITestStartEventReceiver and ITestEndEventReceiver:
public class MyAttribute : Attribute, ITestStartEventReceiver
{
// Early = runs BEFORE [Before(Test)]
// Late (default) = runs AFTER [Before(Test)]
public EventReceiverStage Stage => EventReceiverStage.Early;
public ValueTask OnTestStart(TestContext context) => ValueTask.CompletedTask;
}
Hook Attributes Reference
All Hook Types
| Level | Before | After | Method Type |
|---|---|---|---|
| Test Discovery | [Before(TestDiscovery)] | [After(TestDiscovery)] | Static |
| Test Session | [Before(TestSession)] | [After(TestSession)] | Static |
| Assembly | [Before(Assembly)] | [After(Assembly)] | Static |
| Class | [Before(Class)] | [After(Class)] | Static |
| Test | [Before(Test)] | [After(Test)] | Instance |
Before vs BeforeEvery
| Attribute | Scope |
|---|---|
[Before(Class)] | Once for this class only |
[BeforeEvery(Class)] | Before every class in session |
[Before(Test)] | Before each test in this class |
[BeforeEvery(Test)] | Before every test in session |
Quick Reference
┌─ DISCOVERY ──────────────────────────────────────────────────────┐
│ [Before(TestDiscovery)] │
│ → Scan assemblies for [Test] methods │
│ → Create data sources, inject properties │
│ → IAsyncDiscoveryInitializer.InitializeAsync() │
│ [After(TestDiscovery)] │
│ → ITestRegisteredEventReceiver.OnTestRegistered (per test) │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌─ TEST SESSION ───────────────────────────────────────────────────┐
│ [Before(TestSession)] → IFirstTestInTestSessionEventReceiver │
│ │ │
│ ├─ [Before(Assembly)] → IFirstTestInAssemblyEventReceiver │
│ │ │ │
│ │ ├─ [Before(Class)] → IFirstTestInClassEventReceiver │
│ │ │ │ │
│ │ │ │ ┌─ PER TEST ─────────────────────────────────────┐ │
│ │ │ │ │ Create instance (constructor) │ │
│ │ │ │ │ Set property values │ │
│ │ │ │ │ IAsyncInitializer.InitializeAsync() │ │
│ │ │ │ │ [BeforeEvery(Test)] │ │
│ │ │ │ │ ITestStartEventReceiver (Early) │ │
│ │ │ │ │ [Before(Test)] │ │
│ │ │ │ │ ITestStartEventReceiver (Late) │ │
│ │ │ │ │ ─────────── TEST BODY ─────────── │ │
│ │ │ │ │ ITestEndEventReceiver (Early) │ │
│ │ │ │ │ [After(Test)] │ │
│ │ │ │ │ ITestEndEventReceiver (Late) │ │
│ │ │ │ │ [AfterEvery(Test)] │ │
│ │ │ │ │ IAsyncDisposable / IDisposable │ │
│ │ │ │ │ Cleanup tracked objects │ │
│ │ │ │ └─────────────────────────────────────────────────┘ │
│ │ │ │ │
│ │ │ ├─ ILastTestInClassEventReceiver → [After(Class)] │
│ │ │ │
│ │ ├─ ILastTestInAssemblyEventReceiver → [After(Assembly)] │
│ │ │
│ ├─ ILastTestInTestSessionEventReceiver → [After(TestSession)] │
└───────────────────────────────────────────────────────────────────┘
Exception Handling
Cleanup Always Runs
All [After] hooks, ITestEndEventReceiver events, and disposal methods run even if earlier ones fail. Exceptions are collected and thrown together.
| Phase | Behavior |
|---|---|
| Before hooks | Fail fast (exception stops execution) |
| After hooks | Run all, collect exceptions |
| Disposal | Always runs, exceptions collected |
Related Pages
- Hooks - Detailed guide to
[Before]and[After]hooks - Event Subscribing - Event receiver interfaces
- Property Injection - Property injection and
IAsyncInitializer - Dependency Injection - DI integration
- Test Context - Accessing test information