Files
pos-system/microservices/.agent/skills/development-lifecycle/references/test-failures.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

7.7 KiB

Test Failures Guide

Common test failure patterns and how to fix them in .NET microservices.

Mock Setup Issues

NullReferenceException in tests

Symptom:

System.NullReferenceException: Object reference not set to an instance of an object

Common Cause: Mock not properly configured

Solution:

// ❌ BAD: Mock returns null by default
var repository = Substitute.For<IOrderRepository>();
var order = await repository.GetByIdAsync(orderId); // returns null!
order.Status; // NullReferenceException

// ✅ GOOD: Configure mock to return data
var repository = Substitute.For<IOrderRepository>();
var expectedOrder = new Order(/* ... */);
repository.GetByIdAsync(orderId).Returns(expectedOrder);

var order = await repository.GetByIdAsync(orderId);
Assert.NotNull(order);

UnitOfWork mock setup

Problem: SaveChangesAsync not properly mocked

Solution:

// ✅ Complete mock setup
var repository = Substitute.For<IOrderRepository>();
var unitOfWork = Substitute.For<IUnitOfWork>();

// Important: Link repository to unitOfWork
repository.UnitOfWork.Returns(unitOfWork);

// Configure SaveChangesAsync behavior
unitOfWork.SaveChangesAsync(Arg.Any<CancellationToken>())
    .Returns(Task.FromResult(1));

// Now handler can use it
var handler = new CreateOrderHandler(repository);
await handler.Handle(command, CancellationToken.None);

// Verify SaveChangesAsync was called
await unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());

Async/Await Issues

Test hangs or times out

Common Causes:

  1. Missing await
  2. Deadlock from .Result or .Wait()
  3. Infinite loop

Solutions:

// ❌ BAD: Forgetting await
[Fact]
public async Task TestMethod()
{
    var result = handler.Handle(command, ct); // Missing await!
    Assert.NotNull(result); // Wrong - this is Task, not result
}

// ✅ GOOD: Proper await
[Fact]
public async Task TestMethod()
{
    var result = await handler.Handle(command, ct);
    Assert.NotNull(result);
}

// ❌ BAD: Blocking async code
var result = repository.GetByIdAsync(id).Result; // Deadlock risk!

// ✅ GOOD: Await properly
var result = await repository.GetByIdAsync(id);

Assertion Failures

Expected vs Actual mismatch

Symptom:

Assert.Equal() Failure
Expected: 5
Actual:   0

Debugging:

// ❌ Unclear what went wrong
Assert.Equal(5, order.Items.Count);

// ✅ Better: Add message
Assert.Equal(5, order.Items.Count, 
    $"Expected 5 items but got {order.Items.Count}");

// ✅ Best: Use specific assertions
Assert.NotEmpty(order.Items);
Assert.Equal(5, order.Items.Count);
Assert.All(order.Items, item => Assert.NotNull(item.ProductId));

Collection comparison failures

Problem: Comparing collections incorrectly

Solutions:

// ❌ BAD: Reference comparison
var expected = new List<int> { 1, 2, 3 };
var actual = service.GetNumbers();
Assert.Equal(expected, actual); // May fail even if content same

// ✅ GOOD: Value comparison
Assert.Equal(expected.Count, actual.Count);
Assert.All(expected, item => Assert.Contains(item, actual));

// ✅ Or use FluentAssertions
actual.Should().BeEquivalentTo(expected);

Database Test Issues

Test fails due to database state

Problem: Tests interfere with each other

Solutions:

// ✅ Use in-memory database per test
public class OrderRepositoryTests : IDisposable
{
    private readonly DbContextOptions<OrderContext> _options;
    private readonly OrderContext _context;
    
    public OrderRepositoryTests()
    {
        _options = new DbContextOptionsBuilder<OrderContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
        
        _context = new OrderContext(_options);
    }
    
    public void Dispose()
    {
        _context.Database.EnsureDeleted();
        _context.Dispose();
    }
}

// ✅ Or use Testcontainers for real database
public class OrderRepositoryIntegrationTests : IAsyncLifetime
{
    private PostgreSqlContainer _container;
    
    public async Task InitializeAsync()
    {
        _container = new PostgreSqlBuilder().Build();
        await _container.StartAsync();
    }
    
    public async Task DisposeAsync()
    {
        await _container.DisposeAsync();
    }
}

Integration Test Issues

TestServer authentication failures

Problem: Requests return 401 Unauthorized

Solutions:

// ✅ Setup test authentication
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove real auth
            services.RemoveAll<IAuthenticationService>();
            
            // Add test auth
            services.AddAuthentication("Test")
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "Test", options => { });
        });
    }
}

// Usage in test
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Test-User-Id", userId.ToString());

Port already in use

Error:

Address already in use: bind

Solutions:

// ✅ Let TestServer choose random port
var factory = new WebApplicationFactory<Program>()
    .WithWebHostBuilder(builder =>
    {
        builder.UseUrls(); // Empty = random port
    });

// ✅ Or use different port per test class
builder.UseUrls("http://localhost:0"); // 0 = random port

Flaky Tests

Tests pass/fail intermittently

Common Causes:

  1. Race conditions
  2. Time-dependent logic
  3. Shared state
  4. External dependencies

Solutions:

// ❌ BAD: Time-dependent test
[Fact]
public void TestCreatedDate()
{
    var order = new Order();
    Assert.Equal(DateTime.UtcNow, order.CreatedAt); // Flaky!
}

// ✅ GOOD: Test with tolerance
[Fact]
public void TestCreatedDate()
{
    var before = DateTime.UtcNow;
    var order = new Order();
    var after = DateTime.UtcNow;
    
    Assert.InRange(order.CreatedAt, before, after);
}

// ✅ Or inject time provider
public class Order
{
    public DateTime CreatedAt { get; }
    
    public Order(ITimeProvider timeProvider)
    {
        CreatedAt = timeProvider.UtcNow;
    }
}

// In test: mock time
var timeProvider = Substitute.For<ITimeProvider>();
timeProvider.UtcNow.Returns(new DateTime(2024, 1, 1));

Test Coverage Issues

Low coverage on critical code

Problem: Important logic not tested

Solutions:

# Generate coverage report
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura

# View HTML report
reportgenerator -reports:coverage.cobertura.xml -targetdir:coveragereport
open coveragereport/index.html

# Or use coverlet
dotnet add package coverlet.collector
dotnet test --collect:"XPlat Code Coverage"

Focus on:

  • Domain logic (business rules)
  • Command/Query handlers
  • Critical paths (checkout, payment, etc.)

Quick Troubleshooting Checklist

When tests fail:

  • Check mock setup (returns correct values?)
  • Verify async/await (no missing await?)
  • Check assertions (expected vs actual clear?)
  • Isolate test (run alone, not in suite)
  • Check test output logs
  • Add debug logging
  • Use debugger with breakpoints

For integration tests:

  • Check TestServer configuration
  • Verify authentication setup
  • Check database state (clean between tests?)
  • Review test ordering (independent?)
  • Check external dependencies (mocked?)