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