WEBSITE ĐANG PHÁT TRIỂN

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.

Cái quyết định "thử nghiêm túc"

Cuối 2023, tôi quyết định áp dụng TDD đúng nghĩa - Red-Green-Refactor cycle, test trước khi viết code - cho một module mới trong hệ thống e-commerce đang build.

Lý do: Tôi đã đọc về TDD nhiều năm, biết lý thuyết, nhưng chưa bao giờ thực sự commit 100% trong một dự án thực. Lần này tôi muốn biết - thực sự có giá trị không, hay chỉ là lý thuyết đẹp?

Module target: Pricing engine - tính discount, tax, promotion cho cart. Business logic phức tạp, nhiều edge cases, critical (sai thì mất tiền).


Quay lại chuyện kỹ thuật: TDD cycle thực tế trông như thế nào

Với mỗi feature nhỏ, cycle là:

1. RED: Viết test mô tả behavior mong muốn - test fails vì chưa có implementation
2. GREEN: Viết implementation tối thiểu để test pass
3. REFACTOR: Clean up code trong khi giữ tests xanh

Ví dụ cụ thể với discount calculation:

// RED: Test trước
[Fact]
public void CalculateDiscount_VIPCustomerWithMinimumOrder_AppliesCorrectDiscount()
{
    // Arrange
    var engine = new PricingEngine();
    var cart = new Cart
    {
        CustomerId = "vip-customer-001",
        CustomerType = CustomerType.VIP,
        Items = new[] { new CartItem { Price = 500, Quantity = 2 } } // Total: 1000
    };

    // Act
    var result = engine.CalculateDiscount(cart);

    // Assert
    Assert.Equal(200, result.DiscountAmount); // 20% for VIP orders >= 500
    Assert.Equal(800, result.FinalPrice);
}

// GREEN: Implementation tối thiểu
public class PricingEngine
{
    public DiscountResult CalculateDiscount(Cart cart)
    {
        if (cart.CustomerType == CustomerType.VIP && cart.TotalPrice >= 500)
        {
            var discount = cart.TotalPrice * 0.2m;
            return new DiscountResult
            {
                DiscountAmount = discount,
                FinalPrice = cart.TotalPrice - discount
            };
        }
        return new DiscountResult { DiscountAmount = 0, FinalPrice = cart.TotalPrice };
    }
}

// REFACTOR: Sau khi test xanh, clean up (extract constants, rename, etc.)
public class PricingEngine
{
    private const decimal VipDiscountRate = 0.2m;
    private const decimal VipMinimumOrderAmount = 500m;

    public DiscountResult CalculateDiscount(Cart cart)
    {
        if (IsEligibleForVipDiscount(cart))
            return ApplyDiscount(cart, VipDiscountRate);

        return NoDiscount(cart);
    }

    private bool IsEligibleForVipDiscount(Cart cart) =>
        cart.CustomerType == CustomerType.VIP &&
        cart.TotalPrice >= VipMinimumOrderAmount;

    private DiscountResult ApplyDiscount(Cart cart, decimal rate)
    {
        var discount = Math.Round(cart.TotalPrice * rate, 2, MidpointRounding.AwayFromZero);
        return new DiscountResult
        {
            DiscountAmount = discount,
            FinalPrice = cart.TotalPrice - discount
        };
    }

    private static DiscountResult NoDiscount(Cart cart) =>
        new() { DiscountAmount = 0, FinalPrice = cart.TotalPrice };
}

Kết quả sau 6 tháng: Honest assessment

Cái TDD làm tốt:

1. Business logic clarification. Khi phải viết test trước, tôi buộc phải hiểu rõ requirement trước khi code. Câu hỏi như "discount áp dụng trước hay sau tax?" phải trả lời ngay - không thể "implement xong rồi tính".

2. Design quality. Code TDD thường có better interface design vì bạn viết code từ perspective của caller (test), không phải implementer.

3. Regression safety. Sau 6 tháng, tôi có 320 tests cho pricing engine. Mỗi lần thêm feature mới, tôi confident rằng không break existing behavior.

4. Documentation. Tests là living documentation - đọc test là biết system làm gì.

Cái TDD không làm tốt (theo kinh nghiệm tôi):

1. Speed ở đầu. Tốc độ development drop ~20-30% trong 2 tháng đầu khi chưa quen. Với deadline tight, đây là vấn đề thực.

2. Không phù hợp mọi loại code. TDD shine với business logic phức tạp. Với infrastructure code (controllers, migrations, DI setup), nó awkward và không nhiều value.

3. UI và integration code. TDD thuần cho UI rất khó và thường không worth it. Integration tests (end-to-end) không fit TDD cycle tốt.

4. Team adoption friction. Tôi áp dụng cho bản thân. Khi đưa cả team làm - 2/5 developers thấy productive hơn, 3/5 thấy slower và frustrating.


Kinh nghiệm từ dự án thực: Pragmatic TDD

Sau 6 tháng, tôi không dùng "TDD thuần" nữa. Tôi dùng cái tôi gọi là "Pragmatic TDD":

Dùng TDD cho:

  • Core business logic (pricing, rules engine, financial calculations)
  • Complex algorithms với nhiều edge cases
  • Public API của services/libraries

Không dùng TDD cho:

  • CRUD operations đơn giản
  • UI components
  • Infrastructure code
  • Prototype/exploratory code
// TDD zone - business logic phức tạp
public class PromotionEngine
{
    // Test trước cho từng rule
    public bool IsEligibleForPromotion(Order order, Promotion promotion) { ... }
    public decimal CalculatePromotionDiscount(Order order, Promotion promotion) { ... }
}

// Test-after zone - simple CRUD
public class PromotionRepository : IPromotionRepository
{
    // Viết integration test sau, không TDD
    public async Task<Promotion> GetByCodeAsync(string code) { ... }
    public async Task SaveAsync(Promotion promotion) { ... }
}

Triết lý

TDD không phải tôn giáo. Đây là một tool - powerful tool, nhưng chỉ powerful khi dùng đúng chỗ.

Cái tôi giữ lại từ TDD: Tư duy "test-first" cho business logic. Ngay cả khi không viết test trước, tôi hỏi: "Nếu phải test cái này, tôi sẽ test thế nào?" Câu hỏi đó thường cải thiện design trước khi viết code.

"Make it work, make it right, make it fast" - nhưng với business logic quan trọng, thứ tự là: Make it tested, make it work, make it right.


Bạn đã gặp tình huống này chưa?

Bạn đã thử TDD chưa? Kinh nghiệm của bạn thế nào - nó có work trong môi trường thực tế không? Tôi đặc biệt muốn nghe từ những bạn đã fail với TDD và tại sao 👇


/Son Do - believe in basic

#1percentbetter #TDD #Testing #CleanCode #dotnet #SoftwareEngineering


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

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.