TUnit
The adapter uses a configured assembly run. Configure BDTestOptions with [assembly: BDTestConfiguration(typeof(TestConfiguration))]. Use TUnitBDTestAssembly.GetRun(typeof(MyTests).Assembly) for reports and call TUnitBDTestAssembly.Dispose(assembly) after all tests and exports finish. Explicit-run overrides remain supported.
TUnit is the recommended framework for BDTest, and the one used in the examples throughout this documentation.
dotnet add package BDTest.TUnit
BDTest.TUnit targets netstandard2.0;net8.0 and depends on TUnit.Core. Reference the TUnit package in the test project.
Composition with an existing base class
Implement IBDTestTUnitTest and return a stable host created for the actual test instance. No attribute, manual lifecycle calls, or separate hook registration is needed. The adapter registers global TUnit hooks for both composition and inheritance.
using BDTest;
using BDTest.Settings;
using TUnit.Core;
public class ComposedBasketTests : CompanyTestBase, IBDTestTUnitTest
{
internal readonly BDTestRun Run;
private readonly BDTestScenario<BasketContext> _bdt;
public ComposedBasketTests()
{
var options = new BDTestOptions();
options.RegisterFrameworkOutcomes(TUnitBDTestLifecycle.OutcomeMap);
Run = BDTestRun.Start(options);
_bdt = TUnitBDTestScenario.For<BasketContext>(this, Run);
}
public BDTestBase BDTestHost => _bdt;
[Before(HookType.Test)]
public async Task PrepareBasket()
{
await _bdt.InitializeContextAsync();
Console.WriteLine("Preparing basket");
}
[Test]
public async Task AddingAnItem()
{
await _bdt.Given(() => _bdt.Context.Empty())
.When(() => _bdt.Context.Add("Book", 12.50m))
.Then(() => _bdt.Context.VerifyTotal(12.50m));
}
}
public class ComposedBasketReports
{
// Runs after the adapter's global completion hook. This example owns one run
// per test instance; write its report and dispose it only after completion.
[AfterEvery(HookType.Test, Order = int.MaxValue)]
public static async Task SaveReport(TestContext context)
{
if (context.Metadata.TestDetails.ClassInstance is ComposedBasketTests test)
{
try
{
await File.WriteAllTextAsync($"{test.Run.ReportId}.html",
BDTest.Helpers.BDTestHtmlHelper.GetTestHtmlReport(new BDTestRunDescriptor(), test.Run));
}
finally { test.Run.Dispose(); }
}
}
}
Choose the owning explicit run when creating the host. Register TUnitBDTestLifecycle.OutcomeMap before starting the run so framework exceptions and whole-scenario skips reach TUnit. The adapter never disposes your run. A run can instead be shared by an assembly or suite; its owner must wait for every test's completion before reporting and disposing it. Alternatively, TUnitBDTestScenario.For<TContext>(this) uses the configured assembly run and its outcome map; retain IBDTestTUnitTest registration and dispose through TUnitBDTestAssembly after all tests finish.
TUnit supplies the actual test method and its unique execution ID, including data rows and repeats. Story, scenario text, tags, and retry attributes come from the owning instance and method. Read _bdt.Context inside steps so retries see the replacement context. Configure step and scenario callbacks on BDTestOptions when using composition; virtual instance hooks remain available on the inheritance API.
Writing tests with inheritance
Inherit from TUnitBDTestBase<TContext>:
using BDTest;
[Story(AsA = "shopper", IWant = "to add items to my basket", SoThat = "I can buy them")]
public class BasketTests : TUnitBDTestBase<BasketContext>
{
private BasketSteps Basket => new(Context);
[Test]
[ScenarioText("Adding an item updates the total")]
public async Task AddingAnItem()
{
await Given(() => Basket.IsEmpty())
.When(() => Basket.Add("Book", 12.50m))
.Then(() => Basket.TheTotalIs(12.50m));
}
}
TUnit runs tests in parallel by default. Each test gets its own Context.
Running
TUnit uses Microsoft.Testing.Platform. Run the test project directly:
dotnet run --project MyApp.AcceptanceTests
dotnet run --project MyApp.AcceptanceTests -- --treenode-filter "/*/*/BasketTests/*"
Contexts may have constructor dependencies: override CreateContextAsync(ContextCreation) to construct them. BDTest awaits IBDTestContextLifetime.InitializeAsync() before scenario steps and disposes owned contexts at teardown, preferring IAsyncDisposable over IDisposable. Call await InitializeContextAsync() when async fixture setup needs an initialized context before any scenario. See Test context for factories, retries and ownership.
What the adapter does
- Execution ID:
TestContext.Current.Id. - Exceptions: registers
SkipTestException(fromSkip.Test(...)) as skip andInconclusiveTestExceptionas inconclusive, on the frozen assembly run before tests start. - Opt-in:
IBDTestTUnitTest.BDTestHostselects the host and its run.TUnitBDTestBase<TContext>implements this interface automatically. - Setup: a global
[BeforeEvery(HookType.Test, Order = int.MinValue)]hook callsBeginTestand exports BDTest'sAsyncLocalvalues before instance setup. - Teardown: a global
[AfterEvery(HookType.Test, Order = int.MaxValue - 1)]hook awaits context disposal and completes the same host after instance teardown, including setup/body/teardown failures. Completion also clears output when no scenario runs. - Skips: a whole-scenario skip throws
SkipTestException, so TUnit reports the test as skipped.
These hooks work with both package targets (netstandard2.0;net8.0); they do not depend on event-receiver stages that are only available on newer TUnit targets. Reserve Order = int.MaxValue for checks and report/disposal hooks that must run after BDTest completion. Ordinary setup and teardown hooks can keep their default order.
To migrate from manual composition, implement IBDTestTUnitTest, return your existing host, and remove manual BeginTest/EndTest calls. To migrate from TUnitBDTestBase<TContext>, create a composition host with the same run, return it through the interface, and qualify builder/context calls with that host. Existing Setup, TearDown, BDTestTUnitSetup, and BDTestTUnitTearDown methods remain callable for custom harnesses; native TUnit execution uses the shared global lifecycle instead.
Reports after the run
Add BDTest.Reporting at the same version as the adapter before using these export APIs.
See reporting package migration.
Use an assembly-level [After] hook:
using BDTest.Helpers;
public class BDTestReports
{
[After(HookType.Assembly)]
public static async Task AfterAllTests()
{
var assembly = typeof(BDTestReports).Assembly;
try
{
var run = BDTest.TUnitBDTestAssembly.GetRun(assembly);
await File.WriteAllTextAsync("BDTest-Report.html", BDTestHtmlHelper.GetTestHtmlReport(null, run));
}
finally { BDTest.TUnitBDTestAssembly.Dispose(assembly); }
}
}
TUnit 1.75.0 reports a propagated InconclusiveTestException as Failed in TRX,
while BDTest records Inconclusive. This difference is covered by the
adapter conformance suite.
The adapter supplies the runner's exact test method to BeginTest(id, methodInfo). Scenario text, information, tags and retry attributes use cached method metadata, including when a helper constructs the scenario after an await. See custom adapter migration.
Cancellation
The inherited base and the framework-specific composition factory automatically forward
TUnit.Core.TestContext.Current.Execution.CancellationToken to scenario and outline execution, token-aware steps, and hooks, even when no
BDTest timeout is configured. This API is verified against TUnit 1.75.0. Use
TestContext.Current.Execution.Cancel(), framework timeouts, or runner cancellation to signal the framework token.
BDTestAsync(callerToken) combines your token with the framework token; passing a caller
token does not disable runner cancellation. Either source stops execution before unreached
steps or retries start. BDTest records Failed with an OperationCanceledException and a
cancellation reason; framework cancellation alone does not set BDTest's TimedOut flag.
The enclosing framework may also report its own timeout or cancellation outcome.
Use the framework-specific factory for automatic forwarding. A core-only
BDTestScenario.For<TContext>(this, run) host has no framework token; pass the runner token
to BDTestAsync(token) explicitly when retaining that host. Steps must observe their token;
BDTest cannot interrupt arbitrary synchronous code. See timeouts.