Skip to main content

Test Lifecycle Overview

Understanding TUnit's complete test lifecycle helps you write effective tests and place setup/cleanup logic in the right place. TUnit provides multiple mechanisms for hooking into the lifecycle:

  1. Hook Attributes ([Before], [After], etc.) - Method-based hooks
  2. Event Receivers (interfaces like ITestStartEventReceiver) - Object-based event subscriptions
  3. Initialization Interfaces (IAsyncInitializer, IAsyncDiscoveryInitializer) - Async object setup
  4. Disposal Interfaces (IDisposable, IAsyncDisposable) - Resource cleanup

This page provides a complete visual overview of when each mechanism executes.

Complete Lifecycle Diagram​

Phase 1: Test Discovery​

Before any tests execute, TUnit discovers all tests and prepares data sources.

Discovery Phase Details​

StepWhat Happens
[Before(TestDiscovery)]Hook runs once before discovery begins
Scan AssembliesFind all methods with [Test] attribute
Create Data SourcesInstantiate ClassDataSource<T>, resolve MethodDataSource, etc.
Property InjectionResolve and cache property values for data sources
IAsyncDiscoveryInitializerInitialize objects that need to be ready during discovery
[After(TestDiscovery)]Hook runs once after discovery completes
OnTestRegisteredEvent 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​

Here's the exact order of operations for a single test:

OrderWhat HappensType
1[Before(TestSession)]Hook (once per session)
2IFirstTestInTestSessionEventReceiverEvent (once per session)
3[BeforeEvery(Assembly)] / [Before(Assembly)]Hooks (once per assembly)
4IFirstTestInAssemblyEventReceiverEvent (once per assembly)
5[BeforeEvery(Class)] / [Before(Class)]Hooks (once per class)
6IFirstTestInClassEventReceiverEvent (once per class)
7Create test class instanceConstructor runs
8Set property values on instanceCached values applied
9IAsyncInitializer.InitializeAsync()All tracked objects initialized
10[BeforeEvery(Test)]Hook
11ITestStartEventReceiver (Early)Event
12[Before(Test)]Hook (instance method)
13ITestStartEventReceiver (Late)Event
14Test Body ExecutionYour test code runs
15ITestEndEventReceiver (Early)Event
16[After(Test)]Hook (instance method)
17ITestEndEventReceiver (Late)Event
18[AfterEvery(Test)]Hook
19IAsyncDisposable / IDisposableTest instance disposed
20Cleanup tracked objectsRef count decremented, dispose if 0
21ILastTestInClassEventReceiverEvent (after last test in class)
22[After(Class)] / [AfterEvery(Class)]Hooks (after last test in class)
23ILastTestInAssemblyEventReceiverEvent (after last test in assembly)
24[After(Assembly)] / [AfterEvery(Assembly)]Hooks (after last test in assembly)
25ILastTestInTestSessionEventReceiverEvent (after last test in session)
26[After(TestSession)]Hook (once per session)

Initialization Interfaces​

IAsyncInitializer vs IAsyncDiscoveryInitializer​

InterfaceWhen It RunsUse Case
IAsyncDiscoveryInitializerDuring test discoveryLoading data for test case generation
IAsyncInitializerDuring 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​

SharedTypeWhen Disposed
None (default)After each test
PerClassAfter last test in the class
PerAssemblyAfter last test in the assembly
PerTestSessionAfter test session ends
KeyedWhen all tests using that key complete

Property Injection Lifecycle​

Key Points​

  1. Property values are resolved once during test registration
  2. Shared objects (PerClass, PerAssembly, etc.) are created once and reused
  3. Each test gets a new instance of the test class
  4. Cached values are set on each new test instance
  5. IAsyncInitializer runs after [Before(Class)] hooks

Event Receiver Interfaces​

All Event Receiver Interfaces​

InterfaceWhen FiredContext
ITestRegisteredEventReceiverAfter test discoveredTestRegisteredContext
IFirstTestInTestSessionEventReceiverBefore first test in sessionTestSessionContext
IFirstTestInAssemblyEventReceiverBefore first test in assemblyAssemblyHookContext
IFirstTestInClassEventReceiverBefore first test in classClassHookContext
ITestStartEventReceiverWhen test beginsTestContext
ITestEndEventReceiverWhen test completesTestContext
ITestSkippedEventReceiverWhen test is skippedTestContext
ILastTestInClassEventReceiverAfter last test in classClassHookContext
ILastTestInAssemblyEventReceiverAfter last test in assemblyAssemblyHookContext
ILastTestInTestSessionEventReceiverAfter last test in sessionTestSessionContext

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​

LevelBeforeAfterMethod 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​

AttributeScope
[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.

PhaseBehavior
Before hooksFail fast (exception stops execution)
After hooksRun all, collect exceptions
DisposalAlways runs, exceptions collected