Phát Triển Hướng Test (TDD)
Luật Sắt: KHÔNG CÓ CODE SẢN XUẤT NÀO ĐƯỢC VIẾT MÀ KHÔNG CÓ MỘT TEST THẤT BẠI TRƯỚC.
Tại Sao TDD Là Không Thể Thương Lượng
Test-Driven Development (TDD) là thực hành bị tranh luận nhiều nhất và bị bỏ qua nhiều nhất trong kỹ thuật phần mềm. Các lập trình viên lập luận rằng viết test trước là chậm, rằng nó không tự nhiên với một số loại code nhất định, rằng deadline quá gần, hoặc rằng họ sẽ thêm test sau.
Superpowers xem tất cả các lập luận này là sự hợp lý hóa và không coi bất kỳ lý do nào trong số đó là lý do chấp nhận được để bỏ qua kỷ luật test-trước.
Đây là vấn đề cốt lõi của việc viết code trước: khi bạn viết code trước test, test được viết để khớp với code, không phải để xác minh ý định. Bạn kết thúc bằng việc test những gì code làm, không phải những gì nó nên làm. Test trở thành một tài liệu tham khảo hơn là đảm bảo tính đúng đắn. Các bug có trong code gốc được mã hóa vào các test như hành vi mong đợi.
Khi bạn viết test trước:
- Bạn buộc phải chỉ định hành vi chính xác bạn muốn trước khi quyết định cách thực hiện nó
- Bạn phát hiện các vấn đề interface trước khi bạn xây dựng implementation (rẻ để sửa)
- Test thất bại là bằng chứng rằng test của bạn thực sự đang test gì đó
- Test đạt sau khi implementation là bằng chứng rằng bạn đã xây dựng những gì bạn có ý định
TDD không chậm hơn. Nó là việc loại bỏ phần chậm: thời gian dành để debug code chưa bao giờ được chỉ định đúng cách.
Chu Kỳ RED-GREEN-REFACTOR
TDD tuân theo một chu kỳ ba giai đoạn đơn giản cho mỗi phần chức năng:
┌─────────────────────────────────────────────────────────────┐
│ │
│ RED ──────────────────→ GREEN ──────────────→ REFACTOR │
│ │
│ Viết một test Làm cho nó đạt với Cải thiện │
│ thất bại cho code đơn giản nhất code mà │
│ hành vi yêu cầu. có thể. không thay │
│ đổi hành vi. │
└─────────────────────────────────────────────────────────────┘
RED: Viết Một Test Thất Bại
Viết một test mô tả chính xác một phần hành vi yêu cầu. Chạy nó. Xem nó thất bại.
Bước này không phải tùy chọn. Nếu bạn viết một test và nó đạt ngay lập tức mà không có implementation, một trong hai điều là đúng:
- Chức năng đã tồn tại (và bạn nên kiểm tra tại sao)
- Test của bạn sai — nó không thực sự test những gì bạn nghĩ nó đang test
Một test đạt trước khi implementation là vô giá trị. Giai đoạn red tồn tại để xác nhận test của bạn là thật.
// RED: Test này thất bại vì calculateDiscount chưa tồn tại
test('áp dụng giảm giá 10% cho đơn hàng trên $100', () => {
const order = { total: 150, items: [] };
expect(calculateDiscount(order)).toBe(15);
});
GREEN: Làm Cho Nó Đạt (Đơn Giản Thôi)
Viết code đơn giản nhất làm cho test thất bại đạt. Không phải code thanh lịch nhất. Không phải code có thể mở rộng nhất. Code đơn giản nhất.
Quy tắc này ngăn việc over-engineering trong giai đoạn implementation. Bạn không được phép xây dựng cho các trường hợp chưa có test. Nếu bạn nghĩ "điều gì sẽ xảy ra khi giảm giá 20% cho đơn hàng trên $500?" — viết test cho trường hợp đó trước, rồi làm cho nó đạt.
// GREEN: Implementation đơn giản nhất làm cho test đạt
function calculateDiscount(order: Order): number {
if (order.total > 100) {
return order.total * 0.10;
}
return 0;
}
REFACTOR: Cải Thiện Mà Không Phá Vỡ
Khi test đạt, bạn có một lưới an toàn. Bây giờ bạn có thể cải thiện code: đặt tên tốt hơn, trích xuất một hàm helper, loại bỏ trùng lặp, cải thiện khả năng đọc — bất cứ điều gì cải thiện chất lượng mà không thay đổi hành vi.
Chạy các test sau mỗi lần refactor. Nếu chúng vẫn đạt, bạn chưa phá vỡ gì. Nếu một test thất bại, hãy hoàn tác thay đổi cuối cùng.
// REFACTOR: Đặt tên rõ ràng hơn và các hằng số
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.10;
function calculateDiscount(order: Order): number {
const qualifiesForDiscount = order.total > DISCOUNT_THRESHOLD;
return qualifiesForDiscount ? order.total * DISCOUNT_RATE : 0;
}
Test vẫn đạt. Code sạch hơn. Chu kỳ hoàn tất.
Phải Làm Gì Khi Code Được Viết Trước Test
Tình huống này sẽ phát sinh. Một AI agent sẽ viết code implementation trước các test. Một lập trình viên dưới áp lực deadline sẽ làm như vậy. Câu hỏi là phải làm gì khi điều đó xảy ra.
Superpowers nói rõ ràng:
XÓA NÓ. BẮT ĐẦU LẠI.
Không phải "thêm test bây giờ." Không phải "vậy là đủ rồi." Hãy xóa code implementation và viết test trước.
Điều này có vẻ khắc nghiệt cho đến khi bạn hiểu tại sao. Code được viết mà không có test thất bại trước tiên không có thông số đã được xác minh. Implementation mã hóa các giả định chưa bị thách thức. Khi bạn viết test sau thực tế, bạn đang viết một test mà bạn biết sẽ đạt — có nghĩa là bạn không khám phá bất cứ điều gì. Bạn chỉ kiểm tra rằng code của bạn làm những gì code của bạn làm.
Kỷ luật test-trước không chỉ là về việc có các test. Đó là về quá trình chỉ định hành vi trước khi thực hiện nó. Các test được thêm sau bỏ qua bước này.
NẾU: Code sản xuất được viết mà không có test thất bại trước đó
THÌ:
1. Xóa code sản xuất
2. Commit việc xóa
3. Viết một test thất bại
4. Implement theo test
5. Tiếp tục
Bảng Hợp Lý Hóa
Khi áp lực bỏ qua TDD tăng lên, đây là những lập luận phổ biến và lý do tại sao chúng không có giá trị:
| Lý Do | Thực Tế |
|---|---|
| "Tôi sẽ thêm test sau" | Test thêm sau test những gì code làm, không phải những gì nó nên làm. "Sau" hiếm khi đến. |
| "Code này quá đơn giản để cần test" | Code đơn giản cũng có bug. Test mất 2 phút. |
| "Deadline là ngày mai" | Code chưa được test giao nhanh tạo ra bug mất nhiều thời gian sửa hơn là viết test. |
| "Tôi không thể viết test cho loại code này" | Bạn có thể. Nếu nó thực sự khó test, đó là tín hiệu thiết kế code sai. |
| "Chúng ta sẽ code khám phá trước, rồi chính thức hóa sau" | Code khám phá trở thành code sản xuất. Xóa nó hoặc test nó — không có lựa chọn thứ ba. |
| "AI tạo ra nó, nên có lẽ ổn" | Code do AI tạo ra có bug. Code do AI tạo ra mà không có test có bug chưa được phát hiện. |
| "Thêm test bây giờ sẽ làm chúng ta chậm lại" | Thêm test sau khi tìm thấy bug trong production còn chậm hơn nhiều. |
| "Test điều này đòi hỏi mock quá nhiều thứ" | Quá nhiều mock có nghĩa là thiết kế có quá nhiều phụ thuộc. Sửa thiết kế. |
Xác Minh Trước Khi Hoàn Thành
Trước khi tuyên bố bất kỳ tính năng nào hoàn chỉnh, giao thức xác minh Superpowers yêu cầu một cổng 5 bước:
┌────────────────────────────────────────────────────────────────┐
│ CỔNG XÁC MINH │
│ │
│ Bước 1: XÁC ĐỊNH — Liệt kê mọi test liên quan đến tính năng │
│ Bước 2: CHẠY — Thực thi bộ test (đừng chỉ giả định) │
│ Bước 3: ĐỌC — Đọc output thực tế, từng dòng │
│ Bước 4: XÁC NHẬN — Xác nhận mỗi test đạt trong output │
│ Bước 5: TUYÊN BỐ — Chỉ bây giờ mới nói tính năng hoàn chỉnh │
└────────────────────────────────────────────────────────────────┘
Bước 1: XÁC ĐỊNH
Liệt kê mọi file test và test case bao phủ tính năng đang được hoàn thiện. Không chỉ các test bạn viết hôm nay — bất kỳ test hiện có nào chạm vào code bị ảnh hưởng.
Bước 2: CHẠY
Thực sự chạy bộ test. Không phải trong đầu bạn. Không phải giả định. Chạy nó.
npm test -- --coverage --testPathPattern=order
Bước 3: ĐỌC
Đọc output thực tế. Mỗi dòng. Không chỉ "nó có đạt hay thất bại tổng thể không." Nhìn vào:
- Test nào đã chạy
- Test nào đạt
- Test nào bị bỏ qua
- Số liệu coverage
Bước 4: XÁC NHẬN
Đối với mỗi test trong danh sách XÁC ĐỊNH của bạn, xác nhận nó xuất hiện trong output và hiển thị là đạt. Nếu một test bị thiếu trong output, nó đã không chạy. Nếu một test đang thất bại, tính năng chưa hoàn chỉnh.
Bước 5: TUYÊN BỐ
Chỉ sau khi hoàn thành bước 1–4 mới AI (hoặc lập trình viên) được phép nói: "Tính năng này hoàn chỉnh."
Nói "nó sẽ hoạt động" không phải là hoàn thành bước 5. Chỉ output test thực tế mới hoàn thành bước 5.
Các Dấu Hiệu Đỏ Cần Dừng Lại
Nếu bất kỳ tình huống nào trong số này phát sinh trong quá trình TDD, dừng công việc ngay lập tức và báo cáo:
| Dấu Hiệu Đỏ | Ý Nghĩa |
|---|---|
| Một test đạt trước khi implementation tồn tại | Test có lẽ sai |
| Test đạt nhưng tính năng không hoạt động đúng | Test đang test sai thứ |
| Mọi test trong bộ đều đạt sau một lần refactor lớn ngay lần chạy đầu tiên | Đáng ngờ — xác minh không có test nào vô tình bị bỏ qua |
| Coverage test giảm khi thêm tính năng mới | Đang tạo ra các khoảng trống |
| Một test đòi hỏi thay đổi 15 test khác để làm cho nó đạt | Thay đổi thiết kế quá lớn; chia thành các bước nhỏ hơn |
| AI tuyên bố test đạt mà không hiển thị output | Không chấp nhận được — yêu cầu output thực tế trước khi tiếp tục |
TDD Trong Bối Cảnh Các Kế Hoạch Superpowers
Như đã ghi chú trong Viết Kế Hoạch, mỗi nhiệm vụ implementation trong một kế hoạch phải được đi trước bởi một nhiệm vụ test. Cấu trúc kế hoạch thực thi TDD ở cấp độ lập kế hoạch:
Nhiệm vụ N: Viết test thất bại cho [hành vi] ← RED
Nhiệm vụ N+1: Implement [hành vi] ← GREEN
[Refactor xảy ra trong Nhiệm vụ N+1 hoặc như Nhiệm vụ N+2 nếu phạm vi cho phép]
Khi thực thi một kế hoạch, nếu một subagent được điều phối đến Nhiệm vụ N+1 (implementation) mà không có Nhiệm vụ N (test) đã được hoàn thành trước, subagent phải từ chối nhiệm vụ và báo cáo là BLOCKED. Một nhiệm vụ implementation mà không có test thất bại trước đó là một lỗi kế hoạch.
Chạy TDD Với Superpowers
Để gọi skill TDD một cách rõ ràng:
/test-driven-development Tôi cần implement xác thực người dùng
AI sẽ hướng dẫn bạn qua chu kỳ RED-GREEN-REFACTOR, thực thi yêu cầu test-thất-bại-trước, và áp dụng cổng xác minh 5 bước trước khi đánh dấu bất cứ điều gì là hoàn chỉnh.
TDD là sự khác biệt giữa việc tin rằng code của bạn hoạt động và biết rằng nó hoạt động. Trong phát triển phần mềm chuyên nghiệp, niềm tin không phải là tiêu chuẩn chấp nhận được. Bằng chứng mới là.