Testing Anti-Patterns: The Flaky Tests That Cry Wolf

TL;DR The 6 testing mistakes that erode confidence: flaky tests, missing integration coverage, poor isolation, test pollution, brittle assertions, and slow test suites.

Flaky tests are worse than no tests. They train developers to ignore failures, retry until green, and merge anyway. The patterns in this article pass sometimes, fail sometimes, and gradually destroy team confidence in the test suite.

Common questions this answers

  • Why do my tests pass locally but fail in CI?
  • How do I isolate tests that share a database?
  • What makes integration tests flaky?
  • When should I mock vs. use real implementations?
  • How do I speed up a slow test suite?

Definition (what this means in practice)

Testing anti-patterns are test structures that reduce confidence in the test suite instead of increasing it. They create false positives (tests pass when code is broken), false negatives (tests fail when code works), or friction that slows development.

In practice, this means reviewing test isolation, assertion specificity, and the balance between unit and integration tests.

Terms used

  • Flaky test: passes or fails non-deterministically without code changes
  • Test isolation: tests don't affect each other's outcomes
  • Test pollution: shared state from one test affects another
  • Integration test: tests multiple components together
  • Test fixture: shared setup code for a group of tests

Reader contract

This article is for:

  • Engineers debugging intermittent test failures
  • Teams with test suites that take too long or fail randomly

You will leave with:

  • Recognition of 6 testing anti-patterns
  • Isolation strategies for database-dependent tests
  • Patterns for reliable integration tests

This is not for:

  • Testing framework basics (assumes xUnit/NUnit familiarity)
  • UI/E2E testing (focused on unit and integration tests)

Quick start (10 minutes)

If you do nothing else, check for these patterns:

  1. Tests using DateTime.Now or DateTime.UtcNow directly
  2. Tests sharing database state without cleanup
  3. Tests with Thread.Sleep or timing-dependent assertions
  4. Tests using static mutable state
  5. Tests with order dependencies
# Find timing-dependent code in tests
grep -rn "Thread.Sleep\|Task.Delay\|DateTime.Now" --include="*Test*.cs"

# Find static state in test classes
grep -rn "static.*=" --include="*Test*.cs" | grep -v "const\|readonly"

Anti-pattern 1: Timing-dependent tests

Tests that depend on timing are inherently flaky across different machines and loads.

The problem

// BAD: Timing-dependent assertion
[Fact]
public async Task CacheExpires_AfterTtl()
{
    var cache = new MemoryCache(new MemoryCacheOptions());
    cache.Set("key", "value", TimeSpan.FromMilliseconds(100));

    await Task.Delay(150);  // Flaky! Might not be enough on slow CI

    var result = cache.TryGetValue("key", out _);
    Assert.False(result);  // Sometimes fails
}

// BAD: DateTime.Now in production code tested without abstraction
public class TrialService
{
    public bool IsTrialExpired(User user)
    {
        return DateTime.UtcNow > user.TrialEndsAt;  // Untestable!
    }
}

[Fact]
public void IsTrialExpired_WhenAfterEndDate_ReturnsTrue()
{
    var user = new User { TrialEndsAt = DateTime.UtcNow.AddDays(-1) };
    var service = new TrialService();

    // What if this runs at exactly midnight?
    Assert.True(service.IsTrialExpired(user));
}

Why it fails

  • Task.Delay(100) might not be enough on loaded CI servers
  • DateTime.Now varies during test execution
  • Tests pass in isolation but fail when run in parallel
  • Flakiness increases under CI load

The fix

Use time abstractions and avoid real delays:

// GOOD: TimeProvider abstraction (.NET 8+)
public class TrialService(TimeProvider timeProvider)
{
    public bool IsTrialExpired(User user)
    {
        return timeProvider.GetUtcNow() > user.TrialEndsAt;
    }
}

[Fact]
public void IsTrialExpired_WhenAfterEndDate_ReturnsTrue()
{
    var fakeTime = new FakeTimeProvider(
        new DateTimeOffset(2024, 6, 15, 12, 0, 0, TimeSpan.Zero));
    var user = new User
    {
        TrialEndsAt = new DateTimeOffset(2024, 6, 14, 12, 0, 0, TimeSpan.Zero)
    };
    var service = new TrialService(fakeTime);

    Assert.True(service.IsTrialExpired(user));
}

// GOOD: Test cache expiration without real delays
[Fact]
public void CacheExpires_AfterTtl()
{
    var fakeTime = new FakeTimeProvider();
    var options = new MemoryCacheOptions { Clock = fakeTime };
    var cache = new MemoryCache(options);

    cache.Set("key", "value", TimeSpan.FromMinutes(5));

    // Advance time without waiting
    fakeTime.Advance(TimeSpan.FromMinutes(6));

    var result = cache.TryGetValue("key", out _);
    Assert.False(result);
}

Detection

grep -rn "DateTime.Now\|DateTime.UtcNow" --include="*.cs" | grep -v "Test"
grep -rn "Thread.Sleep\|Task.Delay" --include="*Test*.cs"

Anti-pattern 2: Shared database state

Tests that share database state without proper isolation affect each other.

The problem

// BAD: Tests share the same database
public class OrderServiceTests : IClassFixture<DatabaseFixture>
{
    private readonly AppDbContext _db;

    public OrderServiceTests(DatabaseFixture fixture)
    {
        _db = fixture.Context;
    }

    [Fact]
    public async Task CreateOrder_SavesOrder()
    {
        var service = new OrderService(_db);
        await service.CreateOrderAsync(new Order { Id = 1, Total = 100 });

        var order = await _db.Orders.FindAsync(1);
        Assert.NotNull(order);
    }

    [Fact]
    public async Task GetOrders_ReturnsAllOrders()
    {
        var service = new OrderService(_db);

        var orders = await service.GetOrdersAsync();

        Assert.Empty(orders);  // Fails if CreateOrder test ran first!
    }
}

Why it fails

  • Test execution order is not guaranteed
  • Parallel test execution creates race conditions
  • Data from one test affects assertions in another
  • Tests pass individually but fail when run together

The fix

Use per-test isolation with transactions or database reset:

// GOOD: Transaction rollback per test
public class OrderServiceTests : IAsyncLifetime
{
    private readonly AppDbContext _db;
    private readonly IDbContextTransaction _transaction;

    public OrderServiceTests()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlServer(_connectionString)
            .Options;
        _db = new AppDbContext(options);
    }

    public async Task InitializeAsync()
    {
        _transaction = await _db.Database.BeginTransactionAsync();
    }

    public async Task DisposeAsync()
    {
        await _transaction.RollbackAsync();  // Undo all changes
        await _transaction.DisposeAsync();
        await _db.DisposeAsync();
    }

    [Fact]
    public async Task CreateOrder_SavesOrder()
    {
        var service = new OrderService(_db);
        await service.CreateOrderAsync(new Order { Total = 100 });

        var order = await _db.Orders.FirstOrDefaultAsync();
        Assert.NotNull(order);
    }
}

// GOOD: Fresh database per test with Testcontainers
public class OrderServiceTests : IAsyncLifetime
{
    private PostgreSqlContainer _postgres = null!;
    private AppDbContext _db = null!;

    public async Task InitializeAsync()
    {
        _postgres = new PostgreSqlBuilder().Build();
        await _postgres.StartAsync();

        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseNpgsql(_postgres.GetConnectionString())
            .Options;

        _db = new AppDbContext(options);
        await _db.Database.MigrateAsync();
    }

    public async Task DisposeAsync()
    {
        await _db.DisposeAsync();
        await _postgres.DisposeAsync();
    }

    [Fact]
    public async Task CreateOrder_SavesOrder()
    {
        // Fresh database, no pollution from other tests
        var service = new OrderService(_db);
        await service.CreateOrderAsync(new Order { Total = 100 });

        Assert.Single(await _db.Orders.ToListAsync());
    }
}

Isolation strategies

Strategy Speed Isolation Best For
In-memory database Fast Per-test Simple CRUD tests
Transaction rollback Fast Per-test Most integration tests
Database reset Medium Per-class Complex scenarios
Testcontainers Slower Per-test/class Production parity

Detection

// Red flags:
// - IClassFixture<DbContext> without transaction rollback
// - Static database connection string
// - No cleanup in test dispose
// - Tests with hardcoded IDs

Anti-pattern 3: Missing integration tests

Unit tests with heavy mocking miss integration bugs that only appear when components connect.

The problem

// Unit test passes, but integration is broken
[Fact]
public async Task CreateOrder_CallsRepository()
{
    var mockRepo = new Mock<IOrderRepository>();
    mockRepo.Setup(r => r.AddAsync(It.IsAny<Order>()))
        .Returns(Task.CompletedTask);

    var service = new OrderService(mockRepo.Object);
    await service.CreateOrderAsync(new Order());

    mockRepo.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
    // Test passes! But SaveChangesAsync is never called...
}

// The actual bug
public class OrderRepository(AppDbContext db) : IOrderRepository
{
    public async Task AddAsync(Order order)
    {
        await db.Orders.AddAsync(order);
        // Forgot to call SaveChangesAsync!
    }
}

Why it fails

  • Mocks verify interface contracts, not implementation correctness
  • Integration bugs exist at component boundaries
  • Mock setup can be wrong in ways that match test expectations
  • High mock coverage with low actual coverage

The fix

Balance unit tests with integration tests:

// GOOD: Integration test that catches the bug
public class OrderServiceIntegrationTests : IAsyncLifetime
{
    private AppDbContext _db = null!;

    public async Task InitializeAsync()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        _db = new AppDbContext(options);
    }

    public Task DisposeAsync()
    {
        return _db.DisposeAsync().AsTask();
    }

    [Fact]
    public async Task CreateOrder_PersistsToDatabase()
    {
        var repo = new OrderRepository(_db);
        var service = new OrderService(repo);

        await service.CreateOrderAsync(new Order { Total = 100 });

        // Use new context to verify persistence
        using var verifyContext = new AppDbContext(_db.Options);
        var orders = await verifyContext.Orders.ToListAsync();
        Assert.Single(orders);  // This fails, catching the bug!
    }
}

// GOOD: WebApplicationFactory for full integration
public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public OrdersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task PostOrder_ReturnsCreated()
    {
        var order = new { Total = 100 };

        var response = await _client.PostAsJsonAsync("/api/orders", order);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);

        // Verify it was actually persisted
        var getResponse = await _client.GetAsync(
            response.Headers.Location);
        Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
    }
}

Test pyramid guidance

         /\
        /  \  E2E (few, slow, high confidence)
       /----\
      /      \  Integration (moderate, medium speed)
     /--------\
    /          \  Unit (many, fast, focused)
   /------------\
  • Unit tests: Individual functions, business logic, algorithms
  • Integration tests: Database operations, HTTP endpoints, service interactions
  • E2E tests: Critical user journeys only

Detection

# High mock count might indicate missing integration tests
grep -rn "new Mock<" --include="*Test*.cs" | wc -l

# Check for WebApplicationFactory usage
grep -rn "WebApplicationFactory" --include="*Test*.cs"

Anti-pattern 4: Brittle assertions

Assertions that depend on implementation details break with valid refactoring.

The problem

// BAD: Assert on exact string format
[Fact]
public void FormatPrice_ReturnsFormattedString()
{
    var result = FormatPrice(1234.56m);

    Assert.Equal("$1,234.56", result);  // Fails with different locale
}

// BAD: Assert on entire object
[Fact]
public async Task GetUser_ReturnsUser()
{
    var user = await _service.GetUserAsync(1);

    Assert.Equal(new User
    {
        Id = 1,
        Name = "John",
        Email = "john@example.com",
        CreatedAt = new DateTime(2024, 1, 1),  // Brittle!
        UpdatedAt = new DateTime(2024, 6, 15),  // Brittle!
        LastLoginAt = new DateTime(2024, 6, 15, 10, 30, 0)  // Brittle!
    }, user);
}

// BAD: Assert on collection order when order doesn't matter
[Fact]
public async Task GetTags_ReturnsTags()
{
    var tags = await _service.GetTagsAsync();

    Assert.Equal(new[] { "alpha", "beta", "gamma" }, tags);  // Order matters?
}

Why it fails

  • Locale changes break string formatting assertions
  • Timestamp fields change between test runs
  • Implementation changes order without changing correctness
  • Tests fail for invalid reasons, training developers to ignore failures

The fix

Assert on what matters, ignore what doesn't:

// GOOD: Assert on format structure, not exact output
[Fact]
public void FormatPrice_ReturnsFormattedString()
{
    var result = FormatPrice(1234.56m);

    Assert.StartsWith("$", result);
    Assert.Contains("1", result);
    Assert.Contains("234", result);
    Assert.Contains("56", result);
}

// Or use culture-invariant formatting in tests
[Fact]
public void FormatPrice_WithInvariantCulture_ReturnsFormattedString()
{
    var result = FormatPrice(1234.56m, CultureInfo.InvariantCulture);

    Assert.Equal("$1,234.56", result);
}

// GOOD: Assert on relevant properties only
[Fact]
public async Task GetUser_ReturnsUserWithCorrectId()
{
    var user = await _service.GetUserAsync(1);

    Assert.NotNull(user);
    Assert.Equal(1, user.Id);
    Assert.Equal("John", user.Name);
    // Don't assert on timestamps unless they're the point of the test
}

// GOOD: Assert on set membership when order doesn't matter
[Fact]
public async Task GetTags_ReturnsTags()
{
    var tags = await _service.GetTagsAsync();

    Assert.Equal(3, tags.Count);
    Assert.Contains("alpha", tags);
    Assert.Contains("beta", tags);
    Assert.Contains("gamma", tags);
}

// Or use collection equivalence
[Fact]
public async Task GetTags_ReturnsTags()
{
    var tags = await _service.GetTagsAsync();

    var expected = new[] { "alpha", "beta", "gamma" };
    Assert.True(tags.OrderBy(t => t).SequenceEqual(expected.OrderBy(t => t)));
}

Assertion guidelines

Scenario Bad Good
String format Exact match Pattern match or invariant culture
Timestamps Exact time Approximate or ignore
Collections Order-sensitive Set membership
Objects All properties Relevant properties only
Exceptions Message match Exception type

Detection

// Red flags:
Assert.Equal("exact string", result);  // Locale-dependent?
Assert.Equal(new DateTime(...), entity.CreatedAt);  // Exact time?
Assert.Equal(expected, collection);  // Order matters?

Anti-pattern 5: Test pollution through static state

Static state leaks between tests, causing order-dependent failures.

The problem

// BAD: Static state pollutes between tests
public static class GlobalConfig
{
    public static string ApiKey { get; set; } = "default";
}

public class ServiceTests
{
    [Fact]
    public void Service_UsesCustomApiKey()
    {
        GlobalConfig.ApiKey = "custom-key";
        var service = new Service();

        Assert.Equal("custom-key", service.GetApiKey());
        // Forgot to reset!
    }

    [Fact]
    public void Service_UsesDefaultApiKey()
    {
        var service = new Service();

        Assert.Equal("default", service.GetApiKey());
        // Fails if previous test ran first!
    }
}

// BAD: Singleton registered in test DI container
public class IntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public void Test1_ModifiesSingleton()
    {
        var singleton = _factory.Services.GetRequiredService<ISingleton>();
        singleton.Value = "test1";
        // Singleton persists across tests!
    }

    [Fact]
    public void Test2_ExpectsCleanSingleton()
    {
        var singleton = _factory.Services.GetRequiredService<ISingleton>();
        Assert.Null(singleton.Value);  // Fails!
    }
}

Why it fails

  • Test order is not deterministic
  • Parallel execution makes pollution random
  • Tests pass in isolation, fail together
  • Debugging requires running specific test combinations

The fix

Avoid static state or reset it properly:

// GOOD: Instance state instead of static
public class ServiceTests
{
    [Fact]
    public void Service_UsesCustomApiKey()
    {
        var config = new Config { ApiKey = "custom-key" };
        var service = new Service(config);

        Assert.Equal("custom-key", service.GetApiKey());
    }

    [Fact]
    public void Service_UsesDefaultApiKey()
    {
        var config = new Config();  // Fresh instance
        var service = new Service(config);

        Assert.Equal("default", service.GetApiKey());
    }
}

// GOOD: Reset static state in fixture
public class ServiceTests : IDisposable
{
    private readonly string _originalApiKey;

    public ServiceTests()
    {
        _originalApiKey = GlobalConfig.ApiKey;
    }

    public void Dispose()
    {
        GlobalConfig.ApiKey = _originalApiKey;  // Always restore
    }

    [Fact]
    public void Service_UsesCustomApiKey()
    {
        GlobalConfig.ApiKey = "custom-key";
        // ...
    }
}

// GOOD: Fresh WebApplicationFactory per test class
public class IntegrationTests : IAsyncLifetime
{
    private WebApplicationFactory<Program> _factory = null!;

    public Task InitializeAsync()
    {
        _factory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    // Fresh singleton per test class
                    services.AddSingleton<ISingleton, Singleton>();
                });
            });
        return Task.CompletedTask;
    }

    public async Task DisposeAsync()
    {
        await _factory.DisposeAsync();
    }
}

Detection

grep -rn "static.*{ get; set; }" --include="*.cs"
grep -rn "static.*=" --include="*.cs" | grep -v "const\|readonly"

Anti-pattern 6: Slow test suites

Slow tests discourage running them, leading to broken builds and delayed feedback.

The problem

// BAD: Unnecessary database hit in unit test
[Fact]
public void CalculateDiscount_WithGoldMember_Returns20Percent()
{
    using var db = new AppDbContext(GetRealDatabaseOptions());
    var member = db.Members.First(m => m.Tier == "Gold");

    var discount = _calculator.CalculateDiscount(member);

    Assert.Equal(0.20m, discount);
}

// BAD: Real HTTP calls in tests
[Fact]
public async Task ExternalApi_ReturnsData()
{
    using var client = new HttpClient();
    var response = await client.GetAsync("https://api.external.com/data");
    // Network latency, rate limits, external failures
}

// BAD: Unnecessary test setup
[Fact]
public void FormatName_ConcatenatesFirstAndLast()
{
    // Why does formatting test need a full user with address?
    var user = new UserBuilder()
        .WithAddress(new AddressBuilder()
            .WithStreet("123 Main St")
            .WithCity("Seattle")
            .Build())
        .WithPaymentMethod(new PaymentMethodBuilder()
            .WithCardNumber("4111111111111111")
            .Build())
        .Build();

    var result = FormatName(user.FirstName, user.LastName);

    Assert.Equal("John Doe", result);
}

Why it fails

  • Developers skip running tests locally
  • CI feedback takes too long
  • PRs wait for slow test suites
  • Flaky tests retry, making suites even slower

The fix

Minimize I/O and setup in tests:

// GOOD: Pure unit test, no database
[Fact]
public void CalculateDiscount_WithGoldMember_Returns20Percent()
{
    var member = new Member { Tier = MemberTier.Gold };

    var discount = _calculator.CalculateDiscount(member);

    Assert.Equal(0.20m, discount);
}

// GOOD: Mock external HTTP calls
[Fact]
public async Task GetExternalData_ParsesResponse()
{
    var handler = new MockHttpMessageHandler("""
        { "data": "test" }
        """);
    using var client = new HttpClient(handler);
    var service = new ExternalApiService(client);

    var result = await service.GetDataAsync();

    Assert.Equal("test", result.Data);
}

// GOOD: Minimal test setup
[Fact]
public void FormatName_ConcatenatesFirstAndLast()
{
    var result = FormatName("John", "Doe");

    Assert.Equal("John Doe", result);
}

// GOOD: Parallel test execution
// xunit.runner.json
{
    "parallelizeTestCollections": true,
    "maxParallelThreads": -1
}

Test speed targets

Test Type Target Red Flag
Unit test < 10ms > 100ms
Integration test < 1s > 5s
Full suite < 5min > 15min

Detection

# Find slow tests (xUnit)
dotnet test --logger "console;verbosity=detailed" 2>&1 | grep "Duration"

# Profile test execution
dotnet test --blame-hang --blame-hang-timeout 30s

Copy/paste artifact: test project configuration

// xunit.runner.json
{
    "parallelizeTestCollections": true,
    "maxParallelThreads": -1,
    "diagnosticMessages": false,
    "internalDiagnosticMessages": false,
    "preEnumerateTheories": false
}
<!-- Test project .csproj -->
<PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
    <PackageReference Include="xunit" Version="2.*" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.*" />
    <PackageReference Include="Moq" Version="4.*" />
    <PackageReference Include="FluentAssertions" Version="7.*" />
    <PackageReference Include="Testcontainers.PostgreSql" Version="4.*" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.*" />
</ItemGroup>

Copy/paste artifact: testing code review checklist

Testing Code Review Checklist

1. Timing and determinism
   - [ ] No DateTime.Now/UtcNow without abstraction
   - [ ] No Thread.Sleep or Task.Delay for timing
   - [ ] No tests dependent on execution order
   - [ ] TimeProvider used for time-dependent code

2. Database isolation
   - [ ] Transaction rollback or fresh database per test
   - [ ] No shared state between test classes
   - [ ] Cleanup in DisposeAsync
   - [ ] No hardcoded IDs that might conflict

3. Test coverage balance
   - [ ] Unit tests for business logic
   - [ ] Integration tests for database operations
   - [ ] API tests for HTTP endpoints
   - [ ] Not over-mocking (testing mock setup)

4. Assertion quality
   - [ ] Assert relevant properties only
   - [ ] Culture-invariant string comparisons
   - [ ] Order-independent collection assertions
   - [ ] Meaningful assertion messages

5. Static state
   - [ ] No static mutable fields
   - [ ] Static state reset in disposal
   - [ ] Fresh DI container per test where needed

6. Performance
   - [ ] Unit tests under 100ms
   - [ ] No unnecessary I/O in unit tests
   - [ ] Parallel execution enabled
   - [ ] Minimal test setup

Common failure modes

  1. Flaky CI: Tests pass locally, fail randomly in CI due to timing or load
  2. Test blindness: Developers ignore failures because tests "always fail"
  3. Mock trap: 100% mock coverage, 0% confidence in real behavior
  4. Order dependence: Suite only passes when tests run in specific order
  5. Slow suite: 30-minute test suite that nobody runs locally

Checklist

  • Time abstraction used for DateTime-dependent code
  • Database tests use transaction rollback or fresh database
  • Integration tests exist for critical paths
  • Assertions focus on behavior, not implementation
  • No static mutable state in tests
  • Test suite runs in under 5 minutes

FAQ

Should I mock the database in unit tests?

For pure business logic, yes. For repository or service tests that involve queries, use in-memory database or test containers for better confidence.

How do I fix a flaky test?

First, identify the source of non-determinism: timing, shared state, external dependencies, or test order. Then eliminate it using the patterns in this article.

When should I use Testcontainers vs. in-memory database?

Use in-memory for simple CRUD tests. Use Testcontainers when you need production-database behavior (specific SQL features, concurrent access patterns, actual constraints).

How many integration tests should I have?

Enough to cover critical paths: authentication, main workflows, data persistence. Not so many that the suite takes hours.

Should I test private methods?

No. Test behavior through public interfaces. If a private method needs testing, it might belong in a separate class.

What to do next

Run your test suite 10 times in a row. Any test that fails even once is flaky and needs investigation.

For more on building production-quality ASP.NET Core applications, read Dependency Injection Anti-Patterns.

If you want help improving your test suite, reach out via Contact.

References

Author notes

Decisions:

  • Recommend transaction rollback for database isolation. Rationale: fast, reliable, works with most scenarios.
  • Recommend TimeProvider over custom ITimeProvider. Rationale: built into .NET 8+, well-tested.
  • Recommend integration tests alongside unit tests. Rationale: mocks can't catch integration bugs.

Observations:

  • Teams add Thread.Sleep after first timing failure, making tests slower and still flaky.
  • Flaky tests labeled "known flaky" and ignored for months.
  • High mock coverage giving false confidence while integration is broken.
  • Test suites growing slower until nobody runs them locally.