Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions docs/articles/nunit/extending-nunit/Execution-Hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Execution Hooks

Execution Hooks provide structured, ordered, exception-aware extension points around each core test lifecycle phase. They complement (and can wrap) [Action Attributes](Action-Attributes.md) 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`: A `TestContext` snapshot (current test, properties, etc.).
- `HookedMethod`: The `MethodInfoAdapter` of 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

[!code-csharp[ExecutionHookAttributeExample](~/snippets/Snippets.NUnit/ExecutionHookExamples.cs#TimingHookAttribute)]

Usage:

[!code-csharp[ExecutionHookAttributeExample](~/snippets/Snippets.NUnit/ExecutionHookExamples.cs#Usage)]

## Example: Logging All Phases

[!code-csharp[ExecutionHookAttributeExample](~/snippets/Snippets.NUnit/ExecutionHookExamples.cs#LoggingAllPhases)]

## 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 `BeforeTestHook` throws, the test method body is skipped, but its `AfterTestHook` still runs (with `HookData.Exception` set) allowing cleanup/logging.
- If an `AfterTestHook` throws, NUnit still proceeds with TearDown phases (remaining hooks and teardown methods run).
- Failures inside a test body still trigger all `AfterTestHook` executions.
- Setup/TearDown exceptions are reported by the corresponding after-hook with `HookData.Exception` populated.

## 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:

1. Assembly `BeforeTestHook` runs first.
2. Method `BeforeTestHook` runs.
3. Test executes.
4. Method `AfterTestHook` runs.
5. Assembly `AfterTestHook` runs.

## See Also

- [Action Attributes](~/articles/nunit/extending-nunit/Action-Attributes.md)
- [Custom Attributes](~/articles/nunit/extending-nunit/Custom-Attributes.md)
- [Framework Extensibility](~/articles/nunit/extending-nunit/Framework-Extensibility.md)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The NUnit Framework is the part of NUnit that is referenced by user tests. It co
Attributes, Constraints and Asserts as well as the code that discovers and executes tests. Most extensions to exactly
how tests are recognized and how they execute are Framework extensions.

In this documentation, we refer to three different types of Framework extension:
In this documentation, we refer to four different types of Framework extension:

[Custom Attributes](Custom-Attributes.md) allow creation of new types of tests and suites, new sources of data and
modification of the environment in which a test runs as well as its final result.
Expand All @@ -18,6 +18,9 @@ to many tests.
[Custom Constraints](Custom-Constraints.md) allow the user to define new constraints for use in tests along with the
associated fluent syntax that allows them to be used with `Assert.That`.

[Execution Hooks](Execution-Hooks.md) are a NUnit extensibility feature that lets custom code run
at precise moments in the lifecycle of every test method.

## Links to Blog Posts

### On Custom constraints
Expand Down
2 changes: 2 additions & 0 deletions docs/articles/nunit/extending-nunit/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@
href: ITestBuilder-Interface.md
- name: Custom Constraints
href: Custom-Constraints.md
- name: Execution Hooks
href: Execution-Hooks.md

78 changes: 78 additions & 0 deletions docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using NUnit.Framework;
using NUnit.Framework.Internal.ExecutionHooks;

Check failure on line 2 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'ExecutionHooks' does not exist in the namespace 'NUnit.Framework.Internal' (are you missing an assembly reference?)

namespace Snippets.NUnit
{
public class ExecutionHookExamples
{
#region TimingHookAttribute
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class TimeMeasurementHookAttribute : ExecutionHookAttribute

Check failure on line 10 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'ExecutionHookAttribute' could not be found (are you missing a using directive or an assembly reference?)
{
private readonly Dictionary<string, DateTime> _starts = new();

public override void BeforeEverySetUpHook(HookData hookData)

Check failure on line 14 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'HookData' could not be found (are you missing a using directive or an assembly reference?)
{
_starts[hookData.Context.Test.FullName] = DateTime.UtcNow;
}

public override void AfterEverySetUpHook(HookData hookData)

Check failure on line 19 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'HookData' could not be found (are you missing a using directive or an assembly reference?)
{
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");
}
}
}
#endregion

#region Usage
[TestFixture]
[TimeMeasurementHook]
public class SampleTests
{
[SetUp]
public void HeavySetUp() { /* ... */ }

[Test]
public void FastTest() { /* ... */ }
}
#endregion

#region LoggingAllPhases
[AttributeUsage(AttributeTargets.Method)]
public sealed class LogAllHooksAttribute : ExecutionHookAttribute

Check failure on line 48 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'ExecutionHookAttribute' could not be found (are you missing a using directive or an assembly reference?)
{
private void Log(string phase, HookData data, bool withException = false)

Check failure on line 50 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'HookData' could not be found (are you missing a using directive or an assembly reference?)
{
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)

Check failure on line 58 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'HookData' could not be found (are you missing a using directive or an assembly reference?)
=> Log("BeforeEverySetUp", d);

public override void AfterEverySetUpHook(HookData d)

Check failure on line 61 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'HookData' could not be found (are you missing a using directive or an assembly reference?)
=> Log("AfterEverySetUp", d, withException: true);

public override void BeforeTestHook(HookData d)

Check failure on line 64 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'HookData' could not be found (are you missing a using directive or an assembly reference?)
=> Log("BeforeTest", d);

public override void AfterTestHook(HookData d)

Check failure on line 67 in docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs

View workflow job for this annotation

GitHub Actions / Build/Test Snippets

The type or namespace name 'HookData' could not be found (are you missing a using directive or an assembly reference?)
=> 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);
}
#endregion
}
}
Loading