Define the observable contract
An asynchronous test should control the condition that allows progress instead of guessing how long work takes. TaskCompletionSource exposes a task whose outcome the owner can complete deliberately. Here that pending task is the dependency awaited by DoubleAsync. Until the gate has a result, the operation cannot compute its doubled value. This makes an incomplete-before-completion observation deterministic without sleeping, measuring a stopwatch, or assuming a particular thread-pool schedule. It is ordinary .NET task composition, not a simulated async engine or a custom testing framework.
Walk through the real program
The gate is created without a result and DoubleAsync starts awaiting its Task. The first line therefore prints before:False. SetResult supplies seven, allowing the operation to finish with fourteen. Awaiting that operation prints result:14, and the final completed-state observation is after:True. RunContinuationsAsynchronously avoids running registered continuations inline as part of completion, but it does not promise one operating-system thread per continuation. The expected output depends on the explicit gate and await relationships, not on physical timing or which execution resource the browser uses to resume work.
Why the design is testable
A controlled dependency can represent a pending read, acknowledgment, or service response without using the real infrastructure in a small rule test. Keep the scope honest: completing a TaskCompletionSource does not prove that the real adapter receives, parses, or delivers its data correctly. It supplies a chosen outcome at a defined boundary. The caller must still own and await every started task. In a genuine test runner, use its supported asynchronous test shape so the runner observes completion and failures instead of treating an unawaited operation as already successful.
A boundary that matters
Do not replace the gate with a short delay and assert that IsCompleted must still be false at an arbitrary instant. Scheduler speed and runtime load can invalidate that assumption. Likewise, setting the gate and immediately requiring the continuation to have finished would introduce a race; the sample awaits the operation before its final observation. Complete a TaskCompletionSource once or use its Try completion APIs when a race is part of the design. A second SetResult is an error, not a harmless instruction to overwrite an existing asynchronous result.
Practice: isolate one behavior
First supply nine instead of seven and predict result eighteen with the same false-before and true-after observations. Hint: the completion structure stays fixed while the carried value changes. Next move the first IsCompleted observation after awaiting the operation and explain why it must then be true, rather than treating the original label as a permanent property. Keep each ordering experiment separate so a changed output can be attributed to one moved boundary, not to timing guesses or several simultaneous edits.
Practice: challenge another assumption
For another exercise, complete the gate with a deliberate InvalidOperationException and handle the failure around awaiting DoubleAsync. Predict that no successful doubled result is printed. Hint: the fault travels through the awaited dependency and the caller's owned task. Then create a separate canceled-gate scenario with a documented owning token and distinguish it from the fault. Do not convert every non-successful outcome into a normal zero value. In a real runner, verify each outcome using its actual async assertions rather than blocking with Result or Wait.
Review the evidence
The gate gives this test a causal explanation: pending input prevents completion, an explicit result permits progress, and await observes the finished outcome. That is stronger than a test that happens to pass after waiting a few milliseconds. Use controlled completion for orchestration behavior, while retaining separate integration evidence for the real asynchronous boundary. Keep task ownership, completion order, cancellation, failure, and scheduling claims distinct. The baseline's three lines prove those explicit dependencies without promising a thread identity or an exact duration. A controlled pending gate can also verify that a required effect has not happened yet, then release progress and inspect the eventual effect without introducing a timing race. If the input task is already complete before the operation starts, await can finish without suspension, so the initial pending expectation must be arranged deliberately.
- The pending input makes the original before observation deterministically false.
- The caller awaits the result before asserting final completion.
- Controlled tasks test orchestration, not the real external adapter.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
var gate = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
Task<int> operation = DoubleAsync(gate.Task);
Console.WriteLine($"before:{operation.IsCompleted}");
gate.SetResult(7);
Console.WriteLine($"result:{await operation}");
Console.WriteLine($"after:{operation.IsCompleted}");
async Task<int> DoubleAsync(Task<int> input) => (await input) * 2;
Expected output
before:False result:14 after:True
Control the awaited dependency directly; do not assert timer precision or thread identity.