Cái ngày coverage 95% không cứu được ai
Năm 2021, một anh bạn tôi - tech lead của một team fintech - rất tự hào về coverage của team: 95%. Tôi hỏi: "Impressive. Nhưng test những cái gì?"
Anh chưa kịp trả lời thì tuần sau, production bug: logic tính lãi suất sai, ảnh hưởng vài trăm tài khoản.
Post-mortem: Coverage 95% - nhưng test tập trung vào getters/setters, constructors, và CRUD operations đơn giản. Business logic tính lãi suất phức tạp: không có test.
95% coverage. 0% coverage trên phần quan trọng nhất.
Đây là hiện tượng tôi gọi là "coverage theater" - diễn kịch coverage.
Quay lại chuyện kỹ thuật: Coverage metric đo cái gì?
Code coverage đo tỷ lệ code lines (hoặc branches, statements) được execute khi chạy test. Không hơn, không kém.
// Code này có 100% line coverage nếu test gọi method
public decimal CalculateDiscount(decimal price, string customerType)
{
if (customerType == "VIP")
return price * 0.2m;
return price * 0.1m;
}
// Test này đạt 100% line coverage
[Fact]
public void CalculateDiscount_VIP_ReturnsCorrectValue()
{
var result = CalculateDiscount(100, "VIP");
// Không có Assert! ← Coverage vẫn tính là 100%
// Vì lines được execute hết
}
Coverage không đo:
- Assertions có meaningful không
- Edge cases được cover không
- Business rules được verify không
- Integration giữa components có đúng không
Kinh nghiệm từ dự án thực
Sau nhiều năm, tôi có một framework đơn giản hơn: "Coverage by risk" thay vì "coverage by lines".
Tier 1: Core business logic - 90%+ coverage bắt buộc
// E-commerce: Order processing, pricing, inventory
public class PricingEngine
{
// ✅ Phải test kỹ: Nhiều edge cases, business critical
public decimal CalculateFinalPrice(
decimal basePrice,
IEnumerable<Discount> discounts,
TaxConfiguration taxConfig,
CustomerSegment customer)
{
// ...complex logic...
}
}
Tests cho Tier 1 phải cover:
- Happy path
- Boundary values (0, negative, max)
- Edge cases trong business rules
- Combinations của conditions
Tier 2: Integration points - 70%+ coverage
// Repository layer, service orchestration
public class OrderService
{
// Test các integration scenarios chính
// Không cần test mọi combination
public async Task<Order> CreateOrderAsync(CreateOrderCommand command) { ... }
}
Tier 3: Infrastructure code - 50%+ coverage hoặc integration test thay thế
// Controllers, middleware, DI configuration
// Thường covered bởi integration/E2E tests
[ApiController]
public class OrderController : ControllerBase
{
// Unit test không nhiều value ở đây
// Integration test với TestServer tốt hơn
}
Tier 4: Plumbing code - không cần coverage
// Getters/setters, data models thuần túy, DI registration
public class OrderDto
{
public int Id { get; set; }
public string Status { get; set; }
// ... không có logic → không cần test
}
Cách setup trong .NET để enforce coverage đúng chỗ
<!-- Directory.Build.props - apply cho toàn project -->
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<!-- Coverage threshold theo namespace -->
<!-- Business logic: 90% -->
<!-- Infrastructure: 60% -->
<!-- Overall: 75% -->
<Threshold>75</Threshold>
<ThresholdType>line</ThresholdType>
<ThresholdStat>total</ThresholdStat>
<!-- Exclude plumbing code -->
<ExcludeByAttribute>System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage</ExcludeByAttribute>
<ExcludeByFile>**/*Dto.cs,**/*Config.cs,**/*Options.cs</ExcludeByFile>
</PropertyGroup>
// Mark infrastructure code là excluded
[ExcludeFromCodeCoverage]
public class EmailNotificationService : INotificationService
{
// External service - test bằng integration test, không unit test
}
Bảng so sánh: Metrics tốt vs metrics xấu
| Metric | Vấn đề | Metric tốt hơn |
|---|---|---|
| Overall line coverage | Dễ game bằng test không assert | Branch coverage + mutation score |
| Coverage % duy nhất | Không biết cover cái gì | Coverage breakdown by tier |
| Coverage gate (pass/fail) | Team viết test chỉ để pass | Coverage + test failure rate |
| Code coverage report | Nhìn xong không biết làm gì | Coverage delta per PR |
Metric tôi dùng thực tế:
- Coverage delta: Mỗi PR phải không làm coverage giảm
- Uncovered business logic: Alert khi business logic không có test
- Test failure rate: Test xanh liên tục có thể là sign of weak tests
Triết lý
Coverage là proxy metric, không phải goal. Goal là: "Code của chúng ta có behave đúng không?"
Con số 80% hay 90% không trả lời câu hỏi đó. Nhưng "Tất cả business rules quan trọng đều có test verify behavior" thì có.
Tôi không nói coverage vô ích. Tôi nói coverage là useful chỉ khi bạn hiểu nó đang đo cái gì - và bạn đang coverage đúng chỗ quan trọng.
Bạn đã gặp tình huống này chưa?
Team bạn đang dùng coverage metric thế nào? Có ai đã từng "game" coverage để pass CI mà không thực sự improve quality? Chia sẻ để tôi nghe - tôi chắc chắn mình không phải người duy nhất gặp :)
/Son Do - believe in basic
#1percentbetter #Testing #CleanCode #dotnet #SoftwareQuality