Asynchronous steps
Any Given, When, Then, And or But accepts a synchronous call or a call that returns Task. Mix them freely:
[Test]
public async Task CreateAndFetchAccount()
{
await Given(() => Accounts.ANewCustomer()) // void
.When(() => Accounts.CreateAccountAsync()) // Task
.Then(() => Accounts.FetchAccountAsync()) // Task<T>
.And(() => Assertions.TheAccountIsActive()); // void
}
Test methods return Task and await the chain. Expression-bodied tests work too:
[Test]
public async Task CreateAccount() => await When(() => Accounts.CreateAccountAsync())
.Then(() => Assertions.TheAccountIsActive());
Do not call .Result, .Wait() or .GetAwaiter().GetResult() on a scenario. Blocking a single-threaded synchronization context can deadlock. The synchronous .BDTest() method has been removed for this reason; see Migrating to async-only execution.
Use .BDTestAsync(cancellationToken) to cancel a chain or outline. Token-aware steps receive a token combining execution cancellation, any adapter token and applicable timeouts. With no timeout, cancellation bounds waiting for yielding asynchronous work; it cannot interrupt a synchronous or non-yielding prefix. See Timeouts and cancellation for cleanup and retry behavior.
ValueTask and other awaitables
Step delegates can return Task, Task<T>, ValueTask or ValueTask<T>. BDTest awaits each result once before the next step starts, and reports asynchronous exceptions. Return the operation directly; no .AsTask() is needed:
await When(() => Repository.SaveAsync()) // Task or ValueTask
.Then(() => Assertions.TheItemWasSaved());
ValueTask<T>results are awaited and their values ignored.- Cancellation-aware delegates can take a
CancellationTokenand returnTask,ValueTaskorValueTask<T>. See Timeouts. - Ordinary
asynclambdas select theFunc<Task>overload. This relies on overload priority, so you need C# 13 or newer (.NET SDK 9 or newer). - Lambdas can have statement bodies:
Then(async () => { await a; await b; }). Such steps show their source text in reports; see Step text.
For configured awaitables (ConfigureAwait(...)) and custom awaitables, write an async Task helper or an async lambda that awaits the operation. Returning one of those directly can bind to Action and discard its asynchronous work.
Avoid hiding asynchronous work in a synchronous or async void helper, and do not cast asynchronous steps to Action. BDTest cannot wait for work it cannot see.
Execution order and threads
BDTestAsync() and direct await run steps in order. Without a timeout, BDTest does not schedule a thread-pool task for each scenario or step. Execution begins inline, and each asynchronous step must finish before the next one starts. When every step completes synchronously, the whole scenario can complete on the calling thread.
Synchronization contexts
Without a timeout, BDTest preserves the caller's synchronization context across steps, hooks, retries and outline rows.
- An asynchronous step resumes on that context unless its own code opts out, for example with
ConfigureAwait(false). - Later steps still start on the caller's context.
- Operations with an active timeout, including hooks, run on the thread pool so the timeout can bound synchronous work. Already-cancelled cleanup also runs on the thread pool with a bounded grace period.
When running under a UI or STA context, await the async entry points and keep the context processing messages.
Sharing data between steps
Use the test's Context object to pass data from one step to the next. Changes to an AsyncLocal<T> value inside a step do not flow to later steps or back to the caller.