355 lines
7.7 KiB
Markdown
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)
|