WEBSITE ĐANG PHÁT TRIỂN

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

Con số 80% - Nó nói cái gì?

Tôi còn nhớ cái ngày mà team tôi tự hào khoác tấm áo khác: "Team chúng ta achieve 90% code coverage rồi!". Tất cả mọi người vỗ tay, slack channel bùng nổ emoji rocket, và business đầu tôi cười như vừa mua xổ số trúng độc đắc.

Vài tháng sau - boom. Một con bug ngớ ngẩn (một null không được check trong error path) làm sập production ở 3 giờ sáng. Chúng tôi lục lại code, chạy lại test, bất ngờ nhất là: test suite của team đã cover cái dòng code đó. Nhưng test đó chỉ run qua dòng code, không assert gì cả - nó chỉ chạy vào, chạy ra, xong.

Đó là lúc tôi nhận ra: test coverage là một chiếc mũ bảo hiểm thô sơ. Mũ che được đầu, nhưng nó không protect bạn nếu bạn lái xe vào tường.


Coverage đo gì? Và nó không đo gì?

Hãy để tôi bình luận rõ ràng:

Coverage ĐOÀN MỀN THÔI là: "Phần nào của code được execute bởi các test?"

Coverage KHÔNG nói gì về:

  • Có bao nhiêu test case thực sự verify behavior không?
  • Có bao nhiêu assertion xảy ra trong tests?
  • Liệu tests có cover các edge cases, exception paths, null values?
  • Liệu code dó thực sự hoạt động đúng khi sản phẩm ở production?

Sự khác biệt giữa "run code" và "verify code" là tất cả. Nó giống như kiểm tra ô tô: bạn có thể start engine (code executed), nhưng đó không có nghĩa là bánh xe quay đúng, phanh work, hay cả chiếc xe không bốc cháy.


Pattern 1: Test Coverage Game - Cách People Cheat

Hãy nhìn cái cách mà một team có thể đạt 90% coverage nhưng vẫn có đầy bug:

Example 1: The Empty Test

public class OrderProcessor
{
    public decimal CalculateDiscount(int loyaltyPoints, decimal orderAmount)
    {
        if (loyaltyPoints < 0)
        {
            throw new ArgumentException("Invalid points");
        }

        if (orderAmount <= 0)
        {
            throw new ArgumentException("Invalid amount");
        }

        var discountPercentage = loyaltyPoints / 100m;
        return orderAmount * discountPercentage;
    }
}

Cách gaming coverage:

[TestClass]
public class OrderProcessorTests
{
    [TestMethod]
    public void CalculateDiscount_WithValidInputs_ReturnsValue()
    {
        var processor = new OrderProcessor();
        var result = processor.CalculateDiscount(500, 100m);
        // Notice: NO ASSERT! Just running the code...
    }

    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public void CalculateDiscount_NegativePoints_ThrowsException()
    {
        var processor = new OrderProcessor();
        processor.CalculateDiscount(-1, 100m);
    }
}

Cái coverage report sẽ show "95% coverage". Nhưng:

  • Test thứ nhất không assert kết quả nào cả
  • Không ai test discountPercentage calculation chính xác
  • Không ai test khi loyaltyPoints là 0, hay 999999, hay edge cases khác
  • Nếu tôi quên cast orderAmount thành decimal, test vẫn pass

Example 2: The False Positive

[TestMethod]
public void CalculateDiscount_WithValidInputs_ReturnsCorrectDiscount()
{
    var processor = new OrderProcessor();

    var result = processor.CalculateDiscount(500, 100m);

    // "Correct" expected value, nhưng tôi tính sai công thức
    Assert.AreEqual(50m, result);  // Expected 5m, nhưng tôi viết 50m
}

Nếu code calculate sai (hoặc tôi copy-paste formula từ cái chỗ khác), test vẫn pass. Coverage report show 100%, nhưng khi customer báo cáo discount sai, team tôi sẽ phải debugging.


Pattern 2: Real Testing - Meaningful Coverage

Cách tôi test nó hiện tại - focus vào behavior, không phải coverage%:

[TestClass]
public class OrderProcessorTests
{
    // Test 1: Basic happy path + ASSERT result
    [TestMethod]
    public void CalculateDiscount_With500Points_Returns5PercentDiscount()
    {
        var processor = new OrderProcessor();

        var result = processor.CalculateDiscount(500, 100m);

        Assert.AreEqual(5m, result, "500 points = 5% discount, 5% of 100 = 5");
    }

    // Test 2: Zero discount edge case
    [TestMethod]
    public void CalculateDiscount_With0Points_Returns0Discount()
    {
        var processor = new OrderProcessor();

        var result = processor.CalculateDiscount(0, 100m);

        Assert.AreEqual(0m, result);
    }

    // Test 3: Large discount cap
    [TestMethod]
    public void CalculateDiscount_With100000Points_CalculatesDiscount()
    {
        var processor = new OrderProcessor();

        var result = processor.CalculateDiscount(100000, 100m);

        // 100000 / 100 = 1000%, but sanity: does our method even cap it?
        Assert.AreEqual(1000m, result, "Verify large values behavior");
    }

    // Test 4: Exception cases with CLEAR intent
    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public void CalculateDiscount_NegativePoints_ThrowsException()
    {
        var processor = new OrderProcessor();
        processor.CalculateDiscount(-1, 100m);
    }

    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public void CalculateDiscount_ZeroAmount_ThrowsException()
    {
        var processor = new OrderProcessor();
        processor.CalculateDiscount(100, 0m);
    }

    // Test 5: Decimal precision
    [TestMethod]
    public void CalculateDiscount_WithDecimalAmount_ReturnsCorrectDecimal()
    {
        var processor = new OrderProcessor();

        var result = processor.CalculateDiscount(250, 99.99m);

        Assert.AreEqual(24.9975m, result, "Verify decimal precision");
    }
}

Notice gì?

  1. Every test has a meaningful name - tôi đọc tên test, tôi hiểu behavior ngay
  2. Every test has at least one clear assertion - không empty tests
  3. Test covers edge cases - zero, large values, decimals, exceptions
  4. Tests verify behavior, not just execution - tôi kiểm chứa output chính xác, không phải "code ran"

Coverage report sẽ show ~85-90%, nhưng mỗi dòng code đều verified, không phải just executed.


Con số 80% có ý nghĩa gì, thực ra?

Từ experience của tôi (20 năm làm developer, 7+ năm architecture):

  • 70-80% coverage: Nếu tests được viết mindfully = pretty solid. Significant bugs rare.
  • 90%+ coverage: Time investment diminishing fast. Chắc chắn bạn test công việc quan trọng, nhưng chấp nhận rằng 1-2% còn lại có thể cover những cái edge case nobody ever hits.
  • 100% coverage: Vanity metric. Tôi chưa gặp codebase nào mà 100% coverage + 0 bugs. MIT không.

Bạn biết tại sao 80% được recommend không? Vì nó là sweet spot:

  • Effort: Reasonable. Không phải burn out team để achieve 2-3% nữa.
  • Confidence: Enough. Nếu coverage là 80% + tests được viết tốt, con cải lớn sẽ được catch.
  • Practical: Bạn cover critical paths (happy path, error handling) và most common edge cases.

Cách "Gaming" Coverage - Warnings

Nếu team tôi định achieve 100% coverage, tôi sẽ warning:

Behavior Red Flag
Tests without assertions "Just running code"
Test names like Test_1(), Test_2() No semantic meaning
1000+ coverage tools, 0 failing tests False sense of security
Tests duplicated for different coverage numbers Copy-paste tests
Coverage goal enforced in CI, but bug rate increases Wrong metric

Meaningful Coverage - Checklist

Tôi đánh giá test quality bằng cách này:

  • [ ] Readable test names - MethodName_Condition_ExpectedResult pattern
  • [ ] Minimal setup, clear arrange - Tests shouldn't take 20 lines just to instantiate objects
  • [ ] Single assertion per test (or same concept, multiple assertions) - Easy to see what failed
  • [ ] Real values, not mocks for everything - Integration tests exist
  • [ ] Behavior-focused assertions - Assert.AreEqual(expectedValue, actualValue) with meaning
  • [ ] Edge cases covered - null, empty, zero, boundary values
  • [ ] Error paths tested - Exceptions, validations
  • [ ] Regression tests - When bug found, add test to prevent repeat

Practical Advice từ Years of War

Bạn muốn team có solid test suite? Làm cái này:

  1. Target coverage 70-85%, không chase 100% - Allocate engineers' finite time khôn ngoan
  2. Quality > Quantity - 50 high-quality tests > 500 empty tests
  3. Test by risk, not by line - Cover công việc quan trọng (payments, auth, core business logic) trước
  4. Never enforce coverage without visibility - Bạn phải review tests, không phải just run report
  5. Use coverage as diagnostic tool - "Cái dòng code này không covered, tại sao?" không phải "Tôi phải cover 100%"

Kết luận

Con số 80% coverage không có ý nghĩa gì nếu nó chỉ là chỉ số - nhưng nó có ý nghĩa rất lớn nếu nó là output của mindful testing.

Tôi tin rằng testing tốt là một trong những siêu năng lực của một senior engineer. Nó không phải viết test cho report, nó là viết test để protect code bạn, team bạn, và customers bạn.

Coverage là tool. Metric là tool. Behavior verification - đó mới là mục đích. Khi bạn hiểu sự khác biệt, bạn sẽ viết tests mà team bạn thực sự tin tưởng.


Questions for you:

  • Codebase hiện tại của bạn coverage bao nhiêu? Bạn có tự tin với test suite đó không, hay bạn chỉ tự tin với cái số?
  • Bạn từng encounter tình huống nào là tests pass nhưng code fail ở production?

Share ở comments - tôi muốn nghe story của bạn.

/Son Do - believe in basic

#1percentbetter #cleancode #testing #dotnet #csharp #unittesting #softwareexcellence


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.

Clean Code, Testing & Engineering Excellence

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.