Skip to main content

Hooks

Hooks let you run code at specific points in the test lifecycle using [Before] / [BeforeEvery] and [After] / [AfterEvery] attributes. Most simple setup belongs in the constructor; use hooks for async operations or shared resource management.

For the full execution order, see Test Lifecycle.

Hook Method Signatures​

Hook methods can be synchronous or asynchronous:

[Before(Test)]
public void SynchronousSetup() // ✅ Valid
{
_value = 99;
}

[Before(Test)]
public async Task AsyncSetup() // ✅ Valid
{
_response = await new HttpClient().GetAsync("https://localhost/ping");
}

[After(Test)]
public void SynchronousCleanup() // ✅ Valid
{
_resource?.Dispose();
}

[After(Test)]
public async Task AsyncCleanup() // ✅ Valid
{
await NotifyTestFinished();
}
  • Hooks can be void (synchronous) or async Task (asynchronous)
  • async void hooks are not allowed and will cause a compiler error

Hook Parameters​

Hooks can optionally accept a context object and/or a CancellationToken:

[Before(Test)]
public async Task Setup(TestContext context, CancellationToken cancellationToken)
{
Console.WriteLine($"Setting up: {context.Metadata.TestName}");
await SomeLongRunningOperation(cancellationToken);
}

Valid parameter combinations:

  • No parameters: public void Hook() { }
  • Context only: public void Hook(TestContext context) { }
  • CancellationToken only: public async Task Hook(CancellationToken ct) { }
  • Both: public async Task Hook(TestContext context, CancellationToken ct) { }

In setup hooks, pass the injected CancellationToken to setup operations, including operations returned directly from a hook. It observes the hook's [Timeout] or TUnitSettings.Default.Timeouts.DefaultHookTimeout when no explicit timeout is set. context.Execution.CancellationToken represents test cancellation and does not include that hook timeout, so using it for setup can leave the operation running after the hook's token is cancelled.

TUnit0075 warns about directly passing the test context's token to an operation awaited or returned by a [Before(Test)] or [BeforeEvery(Test)] hook, including conditional branches and overrides of virtual setup hooks. Reading test cancellation state, such as context.Execution.CancellationToken.IsCancellationRequested, is still valid.

Context types by hook level:

Hook LevelContext Type
[Before(Test)] / [After(Test)]TestContext
[Before(Class)] / [After(Class)]ClassHookContext
[Before(Assembly)] / [After(Assembly)]AssemblyHookContext
[Before(TestSession)] / [After(TestSession)]TestSessionContext
[Before(TestDiscovery)] / [After(TestDiscovery)]BeforeTestDiscoveryContext

Checking Test Results in Cleanup​

A common pattern in [After] hooks is checking whether the test failed:

[After(Test)]
public async Task Cleanup(TestContext context, CancellationToken cancellationToken)
{
if (context.Execution.Result?.State == TestState.Failed)
{
await CaptureScreenshot(cancellationToken);
}
}

Setup Hooks: [Before] and [BeforeEvery]​

[Before(HookType)]​

LevelMethod TypeScope
[Before(Test)]InstanceBefore each test in the declaring class. Base class hooks run first (bottom-up).
[Before(Class)]StaticOnce before the first test in the declaring class.
[Before(Assembly)]StaticOnce before the first test in the assembly.
[Before(TestSession)]StaticOnce before the first test in the session.
[Before(TestDiscovery)]StaticOnce before any tests are discovered.

[BeforeEvery(HookType)]​

All [BeforeEvery] methods must be static. Place them in a dedicated file (e.g., GlobalHooks.cs) since they globally affect the test suite.

LevelScope
[BeforeEvery(Test)]Before every test in the session
[BeforeEvery(Class)]Before the first test of every class
[BeforeEvery(Assembly)]Before the first test of every assembly

Setup Example​

using TUnit.Core;

namespace MyTestProject;

public class MyTestClass
{
private int _value;
private static HttpResponseMessage? _pingResponse;

[Before(Class)]
public static async Task Ping()
{
_pingResponse = await new HttpClient().GetAsync("https://localhost/ping");
}

[Before(Test)]
public async Task Setup()
{
await Task.CompletedTask;
_value = 99;
}

[Test]
public async Task MyTest()
{
await Assert.That(_value).IsEqualTo(99);
await Assert.That(_pingResponse?.StatusCode)
.IsNotNull()
.And.IsEqualTo(HttpStatusCode.OK);
}
}

Cleanup Hooks: [After] and [AfterEvery]​

TUnit also supports IDisposable and IAsyncDisposable on test classes, but [After] attributes are preferred — they support multiple methods and collect exceptions from all of them, throwing lazily afterwards.

[After(HookType)]​

LevelMethod TypeScope
[After(Test)]InstanceAfter each test. Current class hooks run first (top-down).
[After(Class)]StaticOnce after the last test in the declaring class.
[After(Assembly)]StaticOnce after the last test in the assembly.
[After(TestSession)]StaticOnce after the last test in the session.
[After(TestDiscovery)]StaticOnce after tests are discovered.

[AfterEvery(HookType)]​

All [AfterEvery] methods must be static. Place them in their own file (e.g., GlobalHooks.cs).

info

Use [AfterEvery(...)] for global cleanup logic that should run after every test/class/assembly/session, regardless of where the test is defined.

LevelScope
[AfterEvery(Test)]After every test in the session
[AfterEvery(Class)]After the last test of every class
[AfterEvery(Assembly)]After the last test of every assembly

Cleanup Example​

using TUnit.Core;

namespace MyTestProject;

public class MyTestClass
{
[After(Class)]
public static async Task KillChromedrivers()
{
await Task.CompletedTask;

foreach (var process in Process.GetProcessesByName("chromedriver.exe"))
{
process.Kill();
}
}

[After(Test)]
public async Task AfterEachTest()
{
await new HttpClient().GetAsync($"https://localhost/test-finished-notifier?testName={TestContext.Current!.Metadata.TestName}");
}

[Test]
public async Task MyTest()
{
// Do something
}
}

Common Mistakes​

  • Instance vs static mismatch — [Before(Class)] and higher must be static. [Before(Test)] must be an instance method. The compiler will error if you mix these up.
  • async void — Not allowed. Use async Task for async hooks, or void for synchronous hooks.
  • Blocking on async — Never call .Wait() or .Result inside a hook. Use async Task instead.
  • Expensive per-test setup — If setup is expensive (HTTP clients, DB connections), use [Before(Class)] to run it once, or use [ClassDataSource<T>] for automatic lifecycle management.

AsyncLocal​

Setting AsyncLocal values in [Before] hooks is supported. Call context.AddAsyncLocalValues() to propagate them into the test framework:

[BeforeEvery(Class)]
public static void BeforeClass(ClassHookContext context)
{
_myAsyncLocal.Value = "Some Value";
context.AddAsyncLocalValues();
}

See Also​