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

355 lines
7.7 KiB
Markdown

# 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:**
```csharp
// ❌ 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:**
```csharp
// ✅ 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:**
```csharp
// ❌ 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:**
```csharp
// ❌ 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:**
```csharp
// ❌ 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:**
```csharp
// ✅ 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:**
```csharp
// ✅ 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:**
```csharp
// ✅ 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:**
```csharp
// ❌ 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:**
```bash
# 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?)
---
## Related Resources
- [Build Errors Catalog](build-errors.md)
- [Debugging Guide](debugging-guide.md)
- [dotnet-senior-tester skill](../../dotnet-senior-tester/SKILL.md)
- Main Skill: [development-lifecycle](../SKILL.md)