Skip to main content

Exception classification

Configure BDTestOptions through assembly configuration, or before starting an explicit run. The examples below belong in that configuration.

Steps, scenarios and scenario outline examples share one exception classifier. When a step throws, BDTest picks a status from the exception:

ExceptionStep and scenario statusRetried?
NotImplementedException, including subclassesNotImplementedNo
A registered success typePassedNo
A registered skip typeSkipped; remaining unexecuted steps are also SkippedNo
A registered inconclusive typeInconclusiveNo
A timeoutFailed, with TimedOut = trueIf configured and cancelled work settles
Explicit or adapter cancellationFailed, with an OperationCanceledException and TimedOut = falseNo
Anything elseFailedYes, if configured

Execution cancellation and timeouts take precedence over registered exception types. For ordinary exceptions, the other rows are checked in the order shown. Only failed outcomes are eligible for retries. A skip keeps the statuses of steps that already ran.

In every case BDTest records the classification and rethrows the original exception, so your test runner applies its own outcome rules. For example, TUnit's Skip.Test(...) throws SkipTestException: BDTest records Skipped and TUnit reports a skipped test. NUnit's Assert.Pass() throws SuccessException: BDTest records Passed and NUnit reports a pass.

Timeouts and explicit or adapter cancellation always produce Failed, even when their exception types are registered. An unrelated user-thrown OperationCanceledException follows the ordinary classification rules.

Inheritance and wrappers​

  • A registered type matches that type and its subclasses.
  • Before matching, BDTest unwraps TargetInvocationException and AggregateException with exactly one inner exception, recursively.
  • An AggregateException with several inner exceptions, or none, stays intact. It is Failed unless the aggregate type or a base type is registered. BDTest never picks one inner exception and hides the others.
  • Classification does not replace the exception. Propagation and failure diagnostics keep the wrappers and their inner exceptions.

Registering exception types​

For built-in adapters, add custom types in IBDTestConfiguration.Configure for the test assembly:

Illustrative fragment
public sealed class TestConfiguration : IBDTestConfiguration
{
public void Configure(BDTestOptions options)
{
options.SuccessExceptionTypes.Add(typeof(MyPassException));
options.SkipExceptionTypes.Add(typeof(MySkipException));
options.InconclusiveExceptionTypes.Add(typeof(MyInconclusiveException));
}
}

The assembly run freezes these registrations when it is first created. Each run owns its exception registrations.

For an explicit run, register types on BDTestOptions before starting it:

Illustrative fragment
var options = new BDTestOptions();
options.SuccessExceptionTypes.Add(typeof(MySuccessException));
options.SkipExceptionTypes.Add(typeof(MySkipException));
options.InconclusiveExceptionTypes.Add(typeof(MyInconclusiveException));
using var run = BDTestRun.Start(options);

Start(options) freezes these registrations; later changes affect only new runs. Start() has no registrations.

Built-in registrations​

The framework adapters register their own types on their assembly run during initialization, before its options are frozen:

AdapterSuccessSkipInconclusive
TUnit—SkipTestExceptionInconclusiveTestException
NUnitSuccessExceptionIgnoreExceptionInconclusiveException
MSTest——AssertInconclusiveException
xUnit—SkipException—

Standalone use registers nothing.

Skip versus inconclusive​

Use a skip type for "this scenario intentionally did not run here". Skips are reported as Skipped and are never retried. Use an inconclusive type for "this scenario ran but could not decide". See Skipping and Statuses.

Framework outcome maps​

Each built-in adapter exposes an immutable OutcomeMap of type BDTest.Settings.FrameworkOutcomeMap. Register that declaration when selecting an explicit run, so classification and runner notifications belong to that run:

Illustrative fragment
var options = new BDTestOptions();
options.RegisterFrameworkOutcomes(NUnitBDTestBase<object>.OutcomeMap);
using var run = BDTestRun.Start(options);

Use the matching base class for MSTest, TUnit, or xUnit. Re-registering a map adds no duplicate exception types. Registration preserves existing custom types. If several maps are registered, their exception types accumulate and the last map supplies the runner notifications. Start(options) freezes both the exception types and the selected notifications. Explicit runs never read adapter mappings from global settings. Start() still starts with no mappings.

For a custom adapter, declare a map once:

Illustrative fragment
var map = new FrameworkOutcomeMap(
reason => Runner.Skip(reason),
reason => Runner.Inconclusive(reason))
.Success<MySuccessException>()
.Skip<MySkipException>()
.Inconclusive<MyInconclusiveException>();
options.RegisterFrameworkOutcomes(map);

Every fluent method returns a new map; retain its return value. ReportSkip(reason) and ReportInconclusive(reason) invoke the declared runner callbacks. An omitted inconclusive callback falls back to the skip callback. BDTest invokes the owning explicit run's skip callback after finalizing a wholly skipped scenario or outline. Framework outcome exceptions thrown inside steps retain their original propagation.

Built-in adapters register their declarations on their assembly run's options.

Runner differences​

The shared suite in TestUtilities/AdapterConformanceTests.cs runs in all four adapter test projects. CI runs scripts/Test-AdapterConformance.ps1 to compare BDTest statuses with actual TRX outcomes. It also tests concurrent explicit runs with different frozen mappings. Intentional failures are enabled only in that script; normal test runs catch and verify those exceptions.

AdapterSuccess exceptionFramework skipFramework inconclusiveWholly skipped scenario
NUnitSuccessException: PassedIgnoreException: SkippedInconclusiveException: InconclusiveAssert.Ignore
MSTestNone; normal completionUses AssertInconclusiveException: InconclusiveAssertInconclusiveException: InconclusiveAssert.Inconclusive
TUnitNone; normal completionSkipTestException: SkippedInconclusiveTestException: InconclusiveSkipTestException
xUnit v3None; normal completionSkipException: SkippedFalls back to skip: SkippedAssert.Skip

MSTest 4.5.1 has no separate runtime skip assertion. Its inconclusive results map to NotExecuted in default TRX output, as do NUnit inconclusive results. TUnit 1.75.0 reports its native InconclusiveTestException as Failed in TRX, while BDTest records Inconclusive. The suite asserts that difference explicitly. xUnit v3 supports runtime skip but has no distinct inconclusive or success exception. Unmapped failures and NotImplementedException produce runner failures; BDTest records them as Failed and NotImplemented, respectively.