Course contentsShow
← C#
Lesson 250 of 1,9082. Object-Oriented Design in C#Free lesson

Designing for Testable Asynchronous Workflows

You will inject time-abstracting services into your objects to test time-dependent logic reliably.

In this lesson, you will learn how to make time-dependent asynchronous code deterministic and testable by decoupling your logic from the system clock.

The Problem with Task.Delay

When you write asynchronous workflows—such as a retry policy or a background task that waits for five minutes—you likely reach for Task.Delay(TimeSpan). While convenient, this creates a "flaky" test suite. If your unit test needs to verify that an operation retries after a delay, your test has to actually pause for that duration. This makes your suite slow, fragile, and non-deterministic.

Abstracting Time with Interfaces

To test these workflows effectively, we must stop asking the operating system "what time is it?" or "wait for X seconds" directly inside our classes. Instead, we create a service to represent the passage of time.

Consider this ITimeProvider interface:

public interface ITimeProvider
{
    Task Delay(TimeSpan duration);
    DateTimeOffset UtcNow { get; }
}

By injecting this into your class, your production code uses a real-world implementation that calls Task.Delay. However, in your unit tests, you can inject a "Mock" or "Fake" implementation.

Why this changes everything

When writing your test, your mock ITimeProvider doesn't actually need to wait. It can simply store the duration requested or advance a virtual clock.

Imagine testing a PollingService that waits 10 seconds between checks. With an injected ITimeProvider, your test becomes:

  1. Initialize the service with a MockTimeProvider.
  2. Trigger the asynchronous method.
  3. Assert that the MockTimeProvider received a request to delay for 10 seconds.
  4. Complete the task instantly in the test environment.

This allows you to verify logic that spans hours or days in mere milliseconds, ensuring your asynchronous workflows are robust, predictable, and lightning-fast. You are no longer testing the system clock; you are testing your design's reaction to the passage of time.

Key Takeaway: Never rely on the system clock directly in your asynchronous logic; inject a time-abstracting service to control the flow of time and ensure your tests are deterministic.