Retries
Configure BDTestOptions through assembly configuration, or before starting an explicit run. The examples below belong in that configuration.
BDTest can retry a scenario that fails. Retries happen inside one execution: the report keeps one entry per scenario, together with the history of every attempt.
Only eligible failures are retried. Passed, skipped and inconclusive results, and exceptions registered as success, are never retried. Explicit or adapter cancellation stops retries. A timeout can retry only when cancelled work and callbacks settle within the cancellation grace period; unfinished work also stops remaining outline rows to avoid racing a replacement context.
[BDTestRetry]
Apply [BDTestRetry(count)] to a test method, a test class or the whole assembly. count is the number of retries after the first attempt.
[Test]
[BDTestRetry(2)] // up to 3 attempts in total
public async Task CallsAFlakyService()
{
await When(() => Api.CallTheService())
.Then(() => Assertions.TheCallSucceeded());
}
[assembly: BDTest.BDTestRetry(1)]
BDTest selects one retry policy before the first attempt: the adapter-supplied test method attribute, then the concrete fixture class attribute, then its assembly attribute, then run retry rules. The first attribute found replaces every less-specific default, including a larger run limit. [BDTestRetry(0)] disables retries even when run rules would retry. Method identity comes from the adapter, including private, overloaded and async methods; BDTest does not inspect the call stack.
Run retry rules
Retry based on the exception, for example for known transient errors:
options.RetryTestRules.Add(
exception => exception is HttpRequestException,
retryLimit: 3);
Run rules apply only when no method, class or assembly retry attribute exists. Their exception filters are evaluated against each failed attempt; when several rules match, the largest retryLimit applies. The selected policy remains fixed throughout the scenario.
If a retry predicate throws, retrying stops. The original test exception remains the thrown exception and the reported scenario/attempt failure. Scenario output includes a Retry predicate failed; retrying stopped diagnostic with the predicate error. An attribute policy does not evaluate run retry predicates.
What happens between attempts
Before each retry, BDTest:
- resets the scenario and its steps to
NotRun, - runs methods on the test class marked
[BDTestRetryTearDown], - calls the test class's
OnBeforeRetry()override, - disposes the previous owned context, creates and initializes a new
Context(and runsConfigureContextagain), - runs methods marked
[BDTestRetrySetUp], - runs the steps again.
public class OrderTests : TUnitBDTestBase<OrderContext>
{
[BDTestRetryTearDown]
public async Task CleanUpPartialOrder() => await Orders.DeleteAsync(Context.OrderId);
[BDTestRetrySetUp]
public void RecreateClient() => Context.Client = new HttpClient();
public override Task OnBeforeRetry()
{
Console.WriteLine("Retrying...");
return Task.CompletedTask;
}
}
An exception in any retry hook fails the scenario and ends retrying, even if the original exception would otherwise have a custom outcome. BDTest throws ErrorOccurredDuringRetryActionException with the hook's exception as InnerException, and records a failed attempt in the retry history. See Retry hook errors. Step hooks run again on every attempt. Each attempt also gets a fresh scenario timeout budget.
Retry history
| Member | Description |
|---|---|
Scenario.RetryCount | Number of retries performed. |
Scenario.Attempts | Every completed attempt in order, including the first and the last. |
Step.IsRetry | Whether the step ran as part of a retry. |
Each immutable ScenarioAttemptResult has a one-based Number, Status, Exception (type, message and stack trace), StartTime, EndTime and TimeTaken. ScenarioResult.Status, Steps and TimeTaken describe the final attempt.
JSON reports include the history, and older reports load with an empty history. Both the HTML report and the report server show a Retry history section for retried scenarios, so a failure stays visible after a passing retry.
With NUnit, each attempt's steps and step hooks run with isolated assertion state through NUnit's public exception constraint API. A final passing attempt lets NUnit report a pass, including after Assert.That or Assert.Multiple failed earlier. A failure outside the scenario still fails the test; warnings from scenario attempts remain visible. If the final attempt throws after recording other assertion failures, NUnit reports both; earlier attempts discard those assertion diagnostics. Retry conditions receive the original exception. No teardown result override is used. TUnit's own [Retry] attribute retries the whole test method instead; use either, not both.
Each attempt retains its own immutable Steps, including arguments, exception diagnostics and attachments, even after retry resets the runtime steps.
OnBeforeRetry(CancellationToken) forwards to the existing parameterless override by default. Override the token overload to cancel asynchronous retry preparation. Methods marked [BDTestRetrySetUp] or [BDTestRetryTearDown] may accept one CancellationToken parameter. These hooks receive caller/framework cancellation; the new scenario timeout starts after retry preparation. Cancellation prevents subsequent retry hooks and Context replacement from starting, and uncooperative retry preparation is subject to the same cancellation grace period and output isolation as steps.