7.7 KiB
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:
- Missing
await - Deadlock from
.Resultor.Wait() - 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:
- Race conditions
- Time-dependent logic
- Shared state
- 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?)