WEBSITE ĐANG PHÁT TRIỂN

Test coverage 80% - con số có ý nghĩa gì và không có ý nghĩa gì

80% test coverage có thể là tuyệt vời hoặc hoàn toàn vô nghĩa - tùy vào cái 80% đó cover cái gì. Tôi đã thấy team có 95% coverage vẫn bị production bug nghiêm trọng, và team có 60% coverage gần như không bao giờ có regression.

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


Bài viết liên quan

Xem thêm
Clean Code, Testing & Engineering Excellence

Vibe coding vui đấy, nhưng production thì không đùa được

Vibe coding với AI cực kỳ năng suất - nhưng 53% code AI sinh ra không qua nổi security review. Bài này tôi phân tích những lỗ hổng phổ biến nhất trong AI-generated code và checklist thực tế để vừa vibe vừa không mất ngủ vì production. Anh bạn tôi - một solo founder đang build SaaS app với Cursor - gọi điện lúc 11 giờ đêm tuần trước. "Ông ơi, tôi vừa nhận email từ khách hàng. Họ nói thấy data của người khác trong account của họ." Anh ấy đã dùng Cursor để build toàn bộ cái app trong 3 tuần. AI viết hết - backend, frontend, database schema, authentication. Năng suất khủng khiếp. Anh ấy tự hào lắm. Ra production mới phát hiện: AI đã generate thiếu Row Level Security (RLS) cho Supabase. Mọi user đều đọc được data của nhau. Đây không phải câu chuyện cá biệt. Vibe coding - cái trend "để AI viết hết, mình chỉ describe" - đang thay đổi hoàn toàn cách developer làm việc. Cursor, GitHub Copilot, Claude, v0.dev... Tôi dùng hàng ngày. Năng suất tăng 3-5x là thật. Thời gian từ idea đến MVP giảm từ tuần xuống ngày là thật. Nhưng có một thứ AI rất kém: security sense.

Clean Code, Testing & Engineering Excellence

TDD trong dự án thực - tôi đã thử và đây là kết quả

TDD (Test-Driven Development) thường được khen ngợi như silver bullet. Tôi áp dụng thực sự trong 6 tháng trên một production project và có kết quả... mixed. Đây là honest assessment từ người đã làm.