Making Async Messaging Tests Feel Synchronous Link to heading

Most async messaging tests read like plumbing diagrams. You’ve got handlers, timers, queues, and a prayer that your Thread.Sleep isn’t too short.

Here’s a cleaner way: make the test look like a request–response.

The Problem Link to heading

Event-driven systems are great in production… less so in tests. Naive tests often mean:

  • Multiple event handlers.
  • Arbitrary delays to “wait” for responses.
  • Brittle, race-condition-prone setups.

It’s messy, hard to read, and painful to debug.

The Pattern Link to heading

Wrap send + wait for correlated reply in one async method. Use a correlation ID and a TaskCompletionSource<T> so the test can await the exact reply it’s expecting.

public async Task<OrderCreatedAck> SendOrderCreatedEvent(string orderId)
{
    var corrId = Guid.NewGuid();
    var tcs = new TaskCompletionSource<OrderCreatedAck>(
        TaskCreationOptions.RunContinuationsAsynchronously);

    _waiters[corrId] = tcs;
    await _bus.Publish(new OrderCreated(corrId, orderId));

    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    await using var _ = cts.Token.Register(() =>
        tcs.TrySetException(new TimeoutException()));

    try { return await tcs.Task; }
    finally { _waiters.Remove(corrId, out _); }
}

private Task HandleInbound(object msg)
{
    if (msg is OrderCreatedAck ack &&
        _waiters.TryGetValue(ack.CorrelationId, out var tcs))
    {
        tcs.TrySetResult(ack);
    }
    return Task.CompletedTask;
}

Why It Works Link to heading

  • Readable — the test becomes:

    var ack = await SendOrderCreatedEvent("ORD-123");
    Assert.Equal("OK", ack.Status);
    
  • No arbitrary sleeps — only unblocks when the right message arrives.

  • Deterministic — works the same on your machine and in CI.

When It’s Too Much Link to heading

If you’ve only got one quick messaging test, this might be overkill. It shines when reused across multiple tests.

Takeaway Link to heading

If your test reads like your business flow instead of your message bus wiring, you’ve done it right.