Execution Hooks
Added in NUnit 4.5
Execution Hooks provide structured, ordered, exception-aware extension points around each core test lifecycle phase. They complement (and can wrap) Action Attributes while staying focused on execution.
Key differences to Action Attributes:
- Execution Hooks focus on the immediate test invocation phases (before/after setup, test, teardown and test action callbacks).
- Only overridden hook methods are registered, keeping runtime overhead low.
- After-hooks run in reverse order (stack behavior) to naturally unwind resources paired with the corresponding before-hooks.
When to use Execution Hooks
Execution Hooks should be used when there is a need to:
- Time, log, trace or audit test phases precisely.
- Inject state around each SetUp/TearDown.
- Pair resource acquisition/release symmetrically (BeforeX/AfterX).
- React to exceptions thrown by setup, test or teardown methods.
- Integrate with or augment existing Action Attributes behavior at a finer granularity.
Getting started
Derive from ExecutionHookAttribute and override only the methods that are relevant:
| Method | Triggered immediately | Applies To |
|---|---|---|
BeforeEverySetUpHook |
Before each [SetUp] or [OneTimeSetUp] method |
All fixture & base fixture setup methods |
AfterEverySetUpHook |
After each [SetUp] or [OneTimeSetUp] method |
All fixture & base fixture setup methods |
BeforeTestHook |
Before the test method | The test method |
AfterTestHook |
After the test method | The test method |
BeforeEveryTearDownHook |
Before each [TearDown] or [OneTimeTearDown] method |
All fixture & base fixture teardown methods |
AfterEveryTearDownHook |
After each [TearDown] or [OneTimeTearDown] method |
All fixture & base fixture teardown methods |
BeforeTestActionBeforeTestHook |
Before an ITestAction.BeforeTest(ITest) executes |
Each applicable Action Attribute |
BeforeTestActionAfterTestHook |
After an ITestAction.BeforeTest(ITest) executes |
Each applicable Action Attribute |
AfterTestActionBeforeTestHook |
Before an ITestAction.AfterTest(ITest) executes |
Each applicable Action Attribute |
AfterTestActionAfterTestHook |
After an ITestAction.AfterTest(ITest) executes |
Each applicable Action Attribute |
This derived attribute can be applied at the method, class, or assembly level.
Each hook receives a HookData instance:
Context: ATestContextsnapshot (current test, properties, etc.).HookedMethod: TheMethodInfoAdapterof the method currently executing (e.g., the specific[SetUp], test, or[TearDown]).Exception: Non-null only for after-hooks when the hooked method threw.
Use these fields for logging, conditional logic, or adaptive cleanup.
Example: Measure Time for Setup
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class TimeMeasurementHookAttribute : ExecutionHookAttribute
{
private readonly Dictionary<string, DateTime> _starts = new();
public override void BeforeEverySetUpHook(HookData hookData)
{
_starts[hookData.Context.Test.FullName] = DateTime.UtcNow;
}
public override void AfterEverySetUpHook(HookData hookData)
{
var key = hookData.Context.Test.FullName;
if (_starts.TryGetValue(key, out var start))
{
var elapsed = DateTime.UtcNow - start;
TestContext.WriteLine($"[Timing] " +
$"{hookData.Context.Test.MethodName} " +
$"took {elapsed.TotalMilliseconds:F1} ms");
}
}
}
Usage:
[TestFixture]
[TimeMeasurementHook]
public class SampleTests
{
[SetUp]
public void HeavySetUp() { /* ... */ }
[Test]
public void FastTest() { /* ... */ }
}
Example: Logging All Phases
[AttributeUsage(AttributeTargets.Method)]
public sealed class LogAllHooksAttribute : ExecutionHookAttribute
{
private void Log(string phase, HookData data, bool withException = false)
{
var name = data.Context.Test.MethodName;
var exInfo = withException && data.Exception != null ?
$" (EX: {data.Exception.GetType().Name})" : string.Empty;
TestContext.WriteLine($"[{phase}] {name}{exInfo}");
}
public override void BeforeEverySetUpHook(HookData d)
=> Log("BeforeEverySetUp", d);
public override void AfterEverySetUpHook(HookData d)
=> Log("AfterEverySetUp", d, withException: true);
public override void BeforeTestHook(HookData d)
=> Log("BeforeTest", d);
public override void AfterTestHook(HookData d)
=> Log("AfterTest", d, withException: true);
public override void BeforeEveryTearDownHook(HookData d)
=> Log("BeforeEveryTearDown", d);
public override void AfterEveryTearDownHook(HookData d)
=> Log("AfterEveryTearDown", d, withException: true);
}
Exception Handling
In general:
- If an Execution Hook throws an exception, NUnit treats it in the same way as if the hooked method had thrown it. For example, an exception from a before/after setup hook is handled the same way as an exception from the setup method itself.
- Independent if a before hook method or the hooked method itself is throwing an exception it is always guaranteed that the after hook method is called and contains the exception details within the
HookData.
Behavior illustrated by hooking a test method:
- If a
BeforeTestHookthrows, the test method body is skipped, but itsAfterTestHookstill runs (withHookData.Exceptionset) allowing cleanup/logging. - If an
AfterTestHookthrows, NUnit still proceeds with TearDown phases (remaining hooks and teardown methods run). - Failures inside a test body still trigger all
AfterTestHookexecutions. - Setup/TearDown exceptions are reported by the corresponding after-hook with
HookData.Exceptionpopulated.
Ordering Semantics
If multiple attributes are applied:
- Before-hooks (
BeforeEverySetUpHook,BeforeTestHook, etc.) execute in the order, attributes were applied (declaration order on the method/class/assembly). - After-hooks execute in reverse order, enabling natural stacking:
- Attribute A before, Attribute B before - Attribute B after, then Attribute A after.
- When multiple attributes appear at different scopes (assembly, class, method), hooks from broader scopes run before narrower scopes for "before" phases, and after narrower scopes for "after" phases (as shown in sequence tests).
Scope: Method vs Class vs Assembly
Because the AttributeUsage targets can be chosen on the derived attribute, control over where hooks can be applied is provided:
- Method: affects only that test method.
- Class: affects all tests within the fixture and inherited base fixture methods.
- Assembly: affects every test in the assembly.
Hooks from broader scopes wrap those from narrower scopes. For a single test method with an assembly-level and a method-level TimingHook:
- Assembly
BeforeTestHookruns first. - Method
BeforeTestHookruns. - Test executes.
- Method
AfterTestHookruns. - Assembly
AfterTestHookruns.