Module 2 · Concurrency, Failure Semantics, and Deterministic Tests · Lesson 4 of 4
Deterministic Tests for Async and Cancellation
Deterministic tests for async code
Async tests become flaky when they depend on wall-clock delays, thread scheduling, or races that the test cannot control. Test the protocol by controlling completion signals.
Use TaskCompletionSource as a gate
[Fact]
public async Task Cancels_dependency_when_request_is_cancelled()
{
var entered = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
var dependency = new FakeDependency(async token =>
{
entered.SetResult();
await Task.Delay(Timeout.InfiniteTimeSpan, token);
return "unreachable";
});
using var cts = new CancellationTokenSource();
var operation = sut.RunAsync(dependency, cts.Token);
await entered.Task;
cts.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(
() => operation);
}RunContinuationsAsynchronously avoids executing arbitrary continuations inline on the thread that completes the source.
Test concurrency as an invariant
A fake dependency can increment an in-flight counter on entry, update a recorded maximum, wait on a controlled release gate, and decrement in finally. Feed more items than the limit and assert that the observed maximum never exceeds it.
Also prove that permits return after failure and cancellation. The happy path alone will not catch a missing finally.
Test time through abstraction
When application code owns timers, inject TimeProvider and advance fake time. When a dependency owns its timeout, stub the dependency to produce the timeout outcome. Avoid multi-second Task.Delay calls in unit tests.
Assert outcomes, not scheduling
Do not assert that continuation A always runs before continuation B unless the public contract requires that order. Assert returned values, cancellation identity, maximum concurrency, logged classification, and cleanup.
Essential test matrix
| Case | Signal | Expected result |
|---|---|---|
| Success | dependency completes | value returned |
| Caller cancellation | request token canceled | cancellation preserved |
| Service budget | fake time advances | timeout classification |
| One item fails | controlled exception | documented all-or-partial result |
| Cancellation while queued | token canceled before permit | queued work never enters dependency |
| Failure after permit | dependency throws | permit released |
| Large input | many controlled items | concurrency and queue policies hold |
Interview explanation
A convincing test strategy avoids “sleep and hope.” It controls when work starts, completes, fails, and receives cancellation. The test finishes in milliseconds and fails for a contract violation, not because a build agent was busy.