Tìm hiểu prompt tạo test cho Claude Code giúp sinh các unit test có "tín hiệu cao" bằng cách tập trung vào ranh giới, bất biến và chế độ lỗi thay vì chỉ các trường hợp bình thường.

Các bộ test tự sinh thường trông ấn tượng: hàng chục test, nhiều mã thiết lập, và tên hàm nào cũng xuất hiện. Nhưng nhiều test trong số đó chỉ là kiểm tra “nó hoạt động khi mọi thứ bình thường”. Chúng dễ qua, hiếm khi bắt được lỗi, và vẫn tốn thời gian để đọc và bảo trì.
Với một prompt tạo test Claude Code thông thường, mô hình có xu hướng phản chiếu các ví dụ đầu vào nó thấy. Bạn nhận được các biến thể trông khác nhau nhưng kiểm thử cùng một hành vi. Kết quả là một bộ test lớn nhưng che phủ mỏng ở những chỗ quan trọng.
Test có tín hiệu cao thì khác. Đó là tập nhỏ các test có thể đã bắt được sự cố tháng trước. Chúng sẽ fail khi hành vi thay đổi theo cách rủi ro, và vẫn ổn định khi refactor vô hại xảy ra. Một test có tín hiệu cao có thể giá trị bằng hai mươi kiểm tra “trả về giá trị mong đợi”.
Tạo test theo happy-path có vài triệu chứng rõ rệt:
Hãy tưởng tượng một hàm áp dụng mã giảm giá. Test happy-path xác nhận rằng “SAVE10” giảm giá. Lỗi thực tế ẩn ở chỗ khác: giá bằng 0 hoặc âm, mã hết hạn, các cạnh làm tròn, hoặc giới hạn giảm giá tối đa. Đó là những trường hợp gây ra tổng tiền sai, khách hàng giận dữ, và rollback lúc nửa đêm.
Mục tiêu là chuyển từ “nhiều test hơn” sang “test tốt hơn” bằng cách nhắm vào ba mục tiêu: ranh giới, chế độ lỗi và bất biến.
Nếu bạn muốn unit test có tín hiệu cao, hãy ngừng yêu cầu “nhiều test hơn” và bắt đầu yêu cầu ba loại cụ thể này. Đây là cốt lõi của một prompt tạo test Claude Code mà tạo ra coverage hữu dụng thay vì một đống kiểm tra "hoạt động trên input bình thường".
Ranh giới là mép của những gì mã chấp nhận hoặc sinh ra. Nhiều lỗi thực tế là off-by-one, trạng thái rỗng, hoặc timeout mà không bao giờ xuất hiện trong happy path.
Nghĩ theo tối thiểu và tối đa (0, 1, độ dài tối đa), rỗng vs có ("", [], nil), off-by-one (n-1, n, n+1), và giới hạn thời gian (gần ngưỡng cắt).
Ví dụ: nếu API chấp nhận “tối đa 100 mục”, hãy test 100 và 101, đừng chỉ test 3.
Chế độ lỗi là những cách hệ thống có thể vỡ: input sai, phụ thuộc mất, kết quả một phần, hoặc lỗi từ upstream. Test chế độ lỗi tốt kiểm tra hành vi dưới áp lực, không chỉ output trong điều kiện lý tưởng.
Ví dụ: khi gọi DB thất bại, hàm có trả về lỗi rõ ràng và tránh ghi dữ liệu một phần không?
Bất biến là những sự thật luôn phải đúng trước và sau một cuộc gọi. Chúng biến tính đúng đắn mơ hồ thành các assert rõ ràng.
Ví dụ:
Khi bạn tập trung vào ba mục này, bạn có ít test hơn, nhưng mỗi test mang nhiều tín hiệu hơn.
Nếu bạn yêu cầu test quá sớm, bạn thường nhận được một đống kiểm tra lịch sự “hoạt động như mong đợi”. Cách khắc phục đơn giản là viết một hợp đồng nhỏ trước, rồi sinh test từ hợp đồng đó. Đây là cách nhanh nhất để biến prompt tạo test Claude Code thành thứ tìm được lỗi thật.
Một hợp đồng hữu ích đủ ngắn để đọc trong một hơi. Hướng tới 5–10 dòng trả lời ba câu hỏi: vào gì, ra gì, và gì khác thay đổi.
Viết hợp đồng bằng ngôn ngữ thường, không phải code, và chỉ bao gồm những gì bạn có thể kiểm thử.
Khi bạn có điều đó, quét để tìm nơi thực tế có thể phá vỡ giả định. Những chỗ đó trở thành ranh giới (min/max, zero, overflow, chuỗi rỗng, trùng lặp) và chế độ lỗi (timeout, permission denied, vi phạm ràng buộc unique, input bị hỏng).
Ví dụ cụ thể cho một tính năng như reserveInventory(itemId, qty):
Hợp đồng có thể nói qty phải là số nguyên dương, hàm phải là nguyên tử, và không bao giờ tạo tồn kho âm. Điều đó ngay lập tức gợi ý test có tín hiệu cao: qty = 0, qty = 1, qty lớn hơn tồn có sẵn, gọi đồng thời, và một lỗi DB ép dừng giữa chừng.
Nếu bạn dùng công cụ vibe-coding như Koder.ai, workflow tương tự áp dụng: viết hợp đồng trong chat trước, rồi sinh test tấn công trực tiếp ranh giới, chế độ lỗi, và danh sách “không được xảy ra”.
Dùng prompt Claude Code này khi bạn muốn ít test hơn nhưng mỗi test đều có tác dụng. Bước quan trọng là ép phải có kế hoạch test trước, rồi chỉ sinh code test sau khi bạn duyệt kế hoạch.
You are helping me write HIGH-SIGNAL unit tests.
Context
- Language/framework: <fill in>
- Function/module under test: <name + short description>
- Inputs: <types, ranges, constraints>
- Outputs: <types + meaning>
- Side effects/external calls: <db, network, clock, randomness>
Contract (keep it small)
1) Preconditions: <what must be true>
2) Postconditions: <what must be true after>
3) Error behavior: <how failures are surfaced>
Task
PHASE 1 (plan only, no code):
A) Propose 6-10 tests max. Do not include “happy path” unless it protects an invariant.
B) For each test, state: intent, setup, input, expected result, and WHY it is high-signal.
C) Invariants: list 3-5 invariants and how each will be asserted.
D) Boundary matrix: propose a small matrix of boundary values (min/max/empty/null/off-by-one/too-long/invalid enum).
E) Failure modes: list negative tests that prove safe behavior (no crash, no partial write, clear error).
Stop after PHASE 1 and ask for approval.
PHASE 2 (after approval):
Generate the actual test code with clear names and minimal mocks.
Một mẹo thực tế là yêu cầu boundary matrix ở dạng bảng gọn để các khoảng hổng hiển nhiên:
| Dimension | Valid edge | Just outside | “Weird” value | Expected behavior |
|---|---|---|---|---|
| length | 0 | -1 | 10,000 | error vs clamp vs accept |
Nếu Claude đề xuất 20 test, hãy phản hồi. Yêu cầu hợp nhất các trường hợp giống nhau và chỉ giữ những test có thể bắt lỗi thực sự (off-by-one, sai kiểu lỗi, mất dữ liệu im lặng, bất biến bị phá).
Bắt đầu với một hợp đồng nhỏ, cụ thể cho hành vi bạn muốn. Dán chữ ký hàm, mô tả ngắn về inputs và outputs, và bất kỳ test hiện có (dù chỉ là happy-path). Điều này giữ mô hình bám sát vào điều mã thực sự làm, không phải đoán mò.
Tiếp theo, yêu cầu một bảng rủi ro trước khi yêu cầu bất kỳ code test nào. Yêu cầu ba cột: boundary cases (mép của input hợp lệ), failure modes (input sai, dữ liệu thiếu, timeout), và invariants (luật phải luôn đúng). Thêm một câu mỗi hàng: “tại sao điều này có thể hỏng.” Một bảng đơn giản lộ ra khoảng trống nhanh hơn một đống file test.
Sau đó chọn tập test nhỏ nhất mà mỗi test có mục đích bắt lỗi khác nhau. Nếu hai test fail vì cùng một lý do, giữ test mạnh hơn.
Một quy tắc chọn thực tế:
Cuối cùng, yêu cầu một giải thích ngắn cho mỗi test: test sẽ bắt lỗi gì nếu nó fail. Nếu giải thích mơ hồ (“xác nhận hành vi”), test có lẽ là tín hiệu thấp.
Bất biến là luật phải luôn đúng bất kể input hợp lệ nào. Với kiểm thử dựa trên bất biến, bạn viết quy tắc bằng ngôn ngữ thường trước, rồi biến nó thành assert có thể fail rõ ràng.
Chọn 1–2 bất biến thực sự bảo vệ bạn khỏi lỗi thật. Bất biến tốt thường về an toàn (không mất dữ liệu), nhất quán (cùng input → cùng output), hoặc giới hạn (không vượt quá cap).
Viết bất biến như một câu ngắn, rồi quyết định bằng chứng test có thể quan sát được: giá trị trả về, dữ liệu lưu, sự kiện phát ra, hoặc các cuộc gọi tới phụ thuộc. Assert mạnh kiểm tra cả kết quả và side effect, vì nhiều lỗi ẩn trong “trả về OK nhưng ghi sai”.
Ví dụ, hàm áp mã giảm giá vào đơn hàng:
Giờ mã hóa thành assert quan sát được:
expect(result.total).toBeGreaterThanOrEqual(0)
expect(db.getOrder(orderId).discountCents).toBe(originalDiscountCents)
Tránh assert mơ hồ như “trả về kết quả mong đợi”. Assert quy tắc cụ thể (không âm), và side effect cụ thể (giảm giá chỉ lưu một lần).
Với mỗi bất biến, thêm một ghi chú ngắn trong test về dữ liệu sẽ vi phạm nó. Điều này giữ test khỏi drift thành kiểm tra happy-path sau này.
Một mẫu đơn giản bền theo thời gian:
Test có tín hiệu cao thường là những test xác nhận mã của bạn fail an toàn. Nếu mô hình chỉ sinh test happy-path, bạn hầu như không biết feature xử lý thế nào khi input và phụ thuộc xấu.
Bắt đầu bằng việc quyết định “an toàn” nghĩa là gì với feature này. Nó trả về lỗi kiểu có cấu trúc? Nó fallback về giá trị mặc định? Nó thử lại một lần rồi dừng? Viết hành vi mong đợi đó ra trong một câu, rồi buộc test chứng minh nó.
Khi yêu cầu Claude Code sinh test chế độ lỗi, giữ mục tiêu chặt: phủ các cách hệ thống có thể hỏng, và assert phản ứng chính xác bạn muốn. Một câu hữu ích: “Ưu tiên ít test hơn với assert mạnh hơn thay vì nhiều test nông.”
Các nhóm lỗi giúp tạo test tốt:
Ví dụ: endpoint tạo user và gọi service email để gửi thư chào mừng. Test low-value kiểm tra “trả 201”. Test high-signal kiểm tra rằng nếu service email timeout, bạn hoặc (a) vẫn tạo user và trả 201 với flag “email_pending”, hoặc (b) trả 503 rõ ràng và không tạo user. Chọn một hành vi, rồi assert cả response lẫn side effect.
Cũng test việc không rò rỉ. Nếu validation fail, đảm bảo không có gì được ghi vào DB. Nếu phụ thuộc trả payload bị hỏng, đảm bảo bạn không ném exception chưa xử lý hoặc trả stack trace thô.
Bộ test tín hiệu thấp thường xuất hiện khi mô hình được thưởng vì số lượng. Nếu prompt Claude Code của bạn yêu cầu “20 unit test”, bạn thường nhận được các biến thể nhỏ trông có vẻ đầy đủ nhưng không bắt gì thêm.
Bẫy phổ biến:
Ví dụ: hàm “create user”. Mười test happy-path có thể thay email và vẫn bỏ qua điều quan trọng: từ chối email trùng lặp, xử lý password rỗng, và đảm bảo ID trả về là duy nhất và ổn định.
Các rào chắn giúp review:
Giả sử feature: áp mã giảm giá khi checkout.
Hợp đồng (nhỏ và testable): cho subtotal của giỏ tính bằng cents và một coupon tùy chọn, trả về tổng cuối cùng bằng cents. Luật: coupon phần trăm làm tròn xuống cent gần nhất, coupon cố định trừ một khoản cố định, và tổng không bao giờ xuống dưới 0. Coupon có thể không hợp lệ, hết hạn, hoặc đã dùng.
Đừng hỏi “tests cho applyCoupon()”. Hãy yêu cầu kiểm thử ranh giới, chế độ lỗi, và bất biến gắn với hợp đồng này.
Chọn inputs dễ làm hỏng phép toán hoặc validation: chuỗi coupon rỗng, subtotal = 0, subtotal ngay dưới và trên ngưỡng chi tiêu tối thiểu, fixed discount lớn hơn subtotal, và phần trăm như 33% gây làm tròn.
Giả sử lookup coupon có thể fail và trạng thái có thể sai: service coupon down, coupon hết hạn, hoặc coupon đã được redeem bởi user này. Test phải chứng minh điều gì xảy ra tiếp (coupon bị từ chối với lỗi rõ ràng, tổng không đổi).
Một tập test tối thiểu, có tín hiệu cao (5 test) và mục đích mỗi test:
Nếu các test này pass, bạn đã che gần hết các điểm dễ vỡ mà không nhồi bộ test bằng các happy-path giống nhau.
Trước khi chấp nhận output của mô hình, kiểm tra nhanh. Mục tiêu là test mỗi cái bảo vệ bạn khỏi một lỗi cụ thể, có khả năng xảy ra.
Dùng checklist này làm rào:
Mẹo sau khi sinh: đổi tên test theo mẫu “should <hành vi> when <điều kiện biên>” và “should not <kết quả xấu> when <sai lầm>”. Nếu không đổi tên gọn, test không đủ tập trung.
Nếu bạn xây với Koder.ai, checklist này cũng phù hợp với snapshot và rollback: sinh test, chạy chúng, và rollback nếu bộ test mới chỉ tạo nhiễu mà không cải thiện coverage.
Xem prompt như một khung tái sử dụng, không phải yêu cầu một lần. Lưu một blueprint prompt (một prompt ép ranh giới, chế độ lỗi, và bất biến) và dùng lại cho mọi hàm, endpoint, hay flow UI mới.
Một thói quen đơn giản nâng kết quả nhanh: yêu cầu một câu cho mỗi test giải thích lỗi nó sẽ bắt. Nếu câu đó chung chung, test có lẽ là nhiễu.
Giữ một danh sách sống động các bất biến miền cho sản phẩm. Đừng để trong đầu. Thêm vào khi bạn tìm ra lỗi thật.
Một workflow nhẹ bạn có thể lặp:
Nếu bạn xây app qua chat, chạy chu kỳ này trong Koder.ai (koder.ai) để hợp đồng, kế hoạch và test sinh ra cùng một chỗ. Khi refactor làm thay đổi hành vi bất ngờ, snapshot và rollback giúp so sánh và lặp đến khi bộ test tín hiệu cao ổn định.
Default: aim for a small set that would catch a real bug.
A quick cap that works well is 6–10 tests per unit (function/module). If you need more, it usually means your unit is doing too much or your contract is unclear.
Happy-path tests mostly prove that your example still works. They tend to miss the stuff that breaks in production.
High-signal tests target:
Start with a tiny contract you can read in one breath:
Then generate tests from that contract, not from examples alone.
Test these first:
Pick one or two per input dimension so each test covers a unique risk.
A good failure-mode test proves two things:
If there’s a database write involved, always check what happened in storage after the failure.
Default approach: turn the invariant into an assertion on observable outcomes.
Examples:
expect(total).toBeGreaterThanOrEqual(0)Prefer checking both and , because many bugs hide in “returned OK but wrote the wrong thing.”
It’s worth keeping a happy-path test when it protects an invariant or a critical integration.
Good reasons to keep one:
Otherwise, trade it for boundary/failure tests that catch more classes of bugs.
Push for PHASE 1: plan only first.
Require the model to provide:
Only after you approve the plan should it generate code. This prevents “20 look-alike tests” output.
Default: mock only the boundary you don’t own (DB/network/clock), and keep everything else real.
To avoid over-mocking:
If a test breaks on refactor but behavior didn’t change, it’s often over-mocked or too implementation-coupled.
Use a simple deletion test:
Also scan for duplicates: