In this lesson, you will learn how to transition from simply labeling tests as "flaky" to systematically isolating and eliminating the root causes of non-deterministic behavior in your C# test suite.
The Anatomy of Flakiness
A flaky test passes and fails intermittently without any changes to the underlying code. In a mature WebApplicationFactory or Testcontainers environment, flakiness is rarely "random." It is almost always a symptom of hidden side effects, such as race conditions, shared mutable state, or non-deterministic ordering in asynchronous background tasks.
Systematic Investigation
When a test fails intermittently, stop relying on "re-running the pipeline." Instead, follow these steps:
- Isolation via Quarantine: Isolate the failing test into a specific suite and execute it in a loop (e.g., 100 times) within your local environment. If it doesn't fail locally, the issue likely resides in the environment configuration (like resource contention on your CI runner).
- Audit Async Synchronization: Often, tests fail because of an assertion that checks a database state before an
IHostedServicehas finished processing a background task. UsePollyor a polling mechanism with a timeout to wait for the expected state instead of usingThread.Sleep(). - Identify Shared State: Ensure that
Testcontainersare cleaned up correctly between runs. If tests share a database schema or a cached object, verify that every test triggers a clean state using the transaction strategies we previously discussed.
Example: Identifying Race Conditions
If a test verifying a message queue processor fails intermittently, it is likely because the assertion executes before the consumer consumes the message.
// Instead of a fragile Task.Delay(1000)
var result = await WaitUntilAsync(async () =>
await repository.GetByIdAsync(id),
expected: "Processed",
timeout: TimeSpan.FromSeconds(5));
Assert.Equal("Processed", result.Status);
By replacing arbitrary delays with event-driven or polling-based assertions, you remove the reliance on the execution speed of the CI server, turning a non-deterministic result into a deterministic one. Once you identify that a test is consistently failing under specific high-load conditions, you can refactor the system architecture to remove the race condition entirely.
Key Takeaway: Flakiness is a data-gathering exercise; replace arbitrary sleeps with state-polling and ensure absolute isolation of test infrastructure to convert non-deterministic failures into repeatable debugging scenarios.