Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    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

    C#
    [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

    CaseSignalExpected result
    Successdependency completesvalue returned
    Caller cancellationrequest token canceledcancellation preserved
    Service budgetfake time advancestimeout classification
    One item failscontrolled exceptiondocumented all-or-partial result
    Cancellation while queuedtoken canceled before permitqueued work never enters dependency
    Failure after permitdependency throwspermit released
    Large inputmany controlled itemsconcurrency 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.

    Sign in to mark lessons done and keep your place in the course.Sign in