Step hooks
Configure BDTestOptions through assembly configuration, or before starting an explicit run. The examples below belong in that configuration.
Step hooks run code around every step. Typical uses are logging, taking screenshots on failure and collecting diagnostics.
Run hooks
Register IStepHook instances in IBDTestConfiguration.Configure(BDTestOptions options), or on options before starting a standalone run:
options.StepHooks.Add(new StepLogger());
public sealed class StepLogger : BDTest.Interfaces.IStepHook
{
public Task BeforeStepAsync(BDTest.Contracts.StepResult step, System.Threading.CancellationToken cancellationToken)
{
Console.WriteLine($"Starting: {step.StepText}");
return Task.CompletedTask;
}
public Task AfterStepAsync(BDTest.Contracts.StepResult step, System.Threading.CancellationToken cancellationToken)
{
Console.WriteLine($"Finished: {step.Status}");
return Task.CompletedTask;
}
}
Hooks can run concurrently for different tests. Keep their shared state thread-safe. Registrations are frozen when the run starts; changing options afterwards affects only later runs.
Per-test hooks
Override OnBeforeStep(BDTest.Contracts.StepResult step, CancellationToken cancellationToken) and OnAfterStep(BDTest.Contracts.StepResult step, CancellationToken cancellationToken) on your test class or base class. They return Task and can use the test's Context:
public abstract class UiTestBase : TUnitBDTestBase<UiContext>
{
public override async Task OnAfterStep(BDTest.Contracts.StepResult step, CancellationToken cancellationToken)
{
if (step.Status == Status.Failed)
{
// Your own helper that saves a screenshot and returns an http(s) URL.
var screenshotUrl = await Context.Screenshots.CaptureAndUploadAsync(cancellationToken);
ScenarioHtmlWriter.Image(screenshotUrl);
}
}
}
Order
For each step that is reached, BDTest awaits:
- run
BeforeStepAsynchooks, in registration order, - the test's
OnBeforeStep, - the step,
- the test's
OnAfterStep, - run
AfterStepAsynchooks, in registration order.
Hook registrations remain fixed for the lifetime of the run, including retries.
Exceptions in hooks
- Before hooks stop at the first exception.
- Every after hook is still attempted if a before hook, the step or another after hook throws or is cancelled.
- The original execution exception takes precedence. Secondary after-hook exceptions are logged.
- Otherwise the first after-hook exception decides the step result, using the usual exception classification and retry rules.
Snapshot semantics
Each hook receives a fresh immutable BDTest.Contracts.StepResult. Retaining a
before snapshot does not turn it into an after result. Before hooks see Pending,
no end time, and the current text before lazy argument/text evaluation. After hooks
see the completed execution snapshot. Collections cannot be changed through the
result; use the test context, attachment writer or HTML writer to record evidence.
What after hooks see
After hooks receive the step's Status, Exception, Output, StartTime, EndTime and TimeTaken.
- Timing includes before hooks and the step, and excludes after hooks.
- The final result includes output written by hooks; a snapshot already delivered to a hook remains unchanged.
- All after hooks observe the execution result before after-hook failures are applied.
When hooks run
| Situation | Hooks run? |
|---|---|
| Normal step | Yes |
| Step skipped by a skip-step rule | Yes |
| Each retry attempt | Yes |
| Step not reached because an earlier step failed | No |
| Whole scenario skipped before execution | No |
Cancellation and migration
IStepHook.BeforeStepAsync and AfterStepAsync now take a CancellationToken after the immutable StepResult. Add that parameter to existing implementations and pass it to cancellable operations. The result remains report data only; it does not hold a live token. The token-aware base overrides forward to their existing single-argument overrides by default, preserving those overrides.
Both step and scenario timeouts cover before hooks, the body and after hooks. After cancellation, cleanup receives a cancelled token and each operation has a 250 millisecond grace period. Unfinished work prevents retries and later outline rows, and its late captured output is suppressed. Cleanup exceptions do not replace an existing execution failure.