Test do AI viết ra rất dễ pass nhưng cực kỳ dễ vô dụng — thứ bạn cần không phải là test pass, mà là test biết la làng khi code hỏng.
Từ test rỗng đến test thật sự có giá trị
Có một loại test mà mình gọi là "test rỗng" (hollow test) — nhìn coverage report thì đẹp, chạy CI thì xanh, nhưng thực chất chẳng bảo vệ được gì. Dạng phổ biến nhất: expect(true).toBe(true), hoặc mock toàn bộ dependency rồi assert đúng cái mock trả về — tức là bạn đang test lại chính cái bạn vừa giả lập, không hề chạm vào logic thật.
AI coding agent (Copilot, Cursor, Claude Code...) có xu hướng tự nhiên trôi về phía test rỗng, và lý do khá dễ hiểu nếu bạn nhìn từ góc độ optimization: model được tối ưu để sinh ra test "pass ngay", không phải test "bắt được bug". Khi AI đọc function của bạn, cách nhanh nhất và an toàn nhất để viết ra một test pass là... đọc code đang làm gì, rồi assert đúng y hệt hành vi hiện tại. Kết quả là bạn có test tautological (đồng nghĩa lặp lại) — nó xác nhận code làm điều nó đang làm, chứ không xác nhận code làm điều nó nên làm. Nếu code có bug từ đầu, test kiểu này sẽ "bảo vệ" luôn cái bug đó.
Ví dụ cụ thể, hàm tính giảm giá phần trăm:
function applyPercentageDiscount(orderTotal: number, percent: number): number {
// Bug: quên clamp percent, và làm tròn sai
return orderTotal - orderTotal * (percent / 100);
}
Test rỗng mà AI hay sinh ra khi chỉ được giao mỗi function này, không có ngữ cảnh business rule:
// Test rỗng — chỉ echo lại implementation
test('applies percentage discount', () => {
const result = applyPercentageDiscount(100, 20);
expect(result).toBe(applyPercentageDiscount(100, 20)); // vô nghĩa
});
Hoặc tinh vi hơn một chút nhưng vẫn rỗng:
test('applies percentage discount', () => {
const result = applyPercentageDiscount(100, 20);
expect(result).toBeDefined();
expect(typeof result).toBe('number');
});
Test này pass, coverage tăng, nhưng nếu percent = 150 (nhập sai) hàm trả về số âm mà không ai biết. Test "thật sự có giá trị" (genuine test) phải khẳng định con số chính xác dựa trên business rule, và phải có case kiểm tra hành vi khi input vượt ngưỡng hợp lệ:
test('20% discount on 100,000 VND order returns 80,000 VND', () => {
expect(applyPercentageDiscount(100000, 20)).toBe(80000);
});
test('percent above 100 must be rejected, not silently applied', () => {
expect(() => applyPercentageDiscount(100000, 150)).toThrow('Invalid discount percent');
});
Ranh giới giữa hai loại test này chính là chủ đề xuyên suốt cả module: làm sao ép AI viết loại thứ hai, thay vì loại thứ nhất — mặc định dễ dãi hơn nhiều.
Mẹo: Mỗi khi review test do AI sinh, tự hỏi "nếu tôi xoá logic thật và trả về giá trị bừa, test này còn fail không?" Nếu câu trả lời là "không chắc", đó là test rỗng — yêu cầu AI viết lại với assertion cụ thể hơn.
Prompt AI để có unit test chất lượng cao
Chìa khoá để có unit test (kiểm thử đơn vị) chất lượng không nằm ở việc chọn đúng framework, mà nằm ở lượng ngữ cảnh (context) bạn đưa cho AI trước khi nó viết dòng test đầu tiên. Prompt hời hợt kiểu "viết test cho function này" gần như chắc chắn ra test rỗng. Muốn genuine test, prompt phải có tối thiểu bốn thành phần:
- Function signature đầy đủ (kèm type) — để AI không đoán mò kiểu dữ liệu.
- Business rule bằng lời, không chỉ code — vì code có thể đang có bug, rule mới là nguồn sự thật.
- Danh sách edge case rõ ràng — đừng để AI tự nghĩ ra, bạn liệt kê thì tỉ lệ bao phủ tốt hơn hẳn.
- Chỉ thị rõ: không mock chính unit đang test — chỉ mock external dependency (DB, HTTP call...), còn logic thuần thì để chạy thật.
Ví dụ prompt đầy đủ cho hàm tính coupon giảm giá cố định có ràng buộc đơn hàng tối thiểu:
Viết unit test bằng Jest + TypeScript cho function sau:
function applyFixedDiscount(orderTotal: number, discountAmount: number, minOrderValue: number): number
Business rules:
- Nếu orderTotal < minOrderValue, coupon không được áp dụng, hàm phải throw Error('Order does not meet minimum value')
- discountAmount không được vượt quá orderTotal (không cho phép đơn hàng âm tiền)
- Nếu discountAmount == orderTotal, kết quả phải là 0, không phải số âm
- Tất cả số tiền tính bằng VND, là số nguyên, không có phần thập phân
Yêu cầu:
- Viết cả happy path lẫn failure path, không chỉ happy path
- Áp dụng boundary value analysis (phân tích giá trị biên): test đúng tại minOrderValue,
ngay dưới minOrderValue (minOrderValue - 1), và ngay trên
- Test case discountAmount == orderTotal và discountAmount > orderTotal riêng biệt
- Không mock function applyFixedDiscount hay bất kỳ logic nội bộ nào của nó — đây là pure
function, chạy thật toàn bộ
- Mỗi test chỉ assert một hành vi, đặt tên test theo pattern
"should <expected behavior> when <condition>"
- Không viết test kiểu expect(result).toBeDefined() hoặc toBeTruthy() — luôn assert giá trị
số cụ thể hoặc loại Error cụ thể
Với prompt này, output thường trông như sau — chú ý cách đặt tên test và cách mỗi case gắn với đúng một rule:
describe('applyFixedDiscount', () => {
it('should throw when order total is below minimum order value', () => {
expect(() => applyFixedDiscount(99000, 10000, 100000))
.toThrow('Order does not meet minimum value');
});
it('should apply discount when order total equals minimum order value exactly', () => {
expect(applyFixedDiscount(100000, 10000, 100000)).toBe(90000);
});
it('should return zero when discount amount equals order total', () => {
expect(applyFixedDiscount(50000, 50000, 0)).toBe(0);
});
it('should throw or clamp when discount amount exceeds order total', () => {
expect(() => applyFixedDiscount(50000, 60000, 0)).toThrow('Discount cannot exceed order total');
});
});
Nếu bạn dùng pytest cho backend Python, cấu trúc prompt tương tự, chỉ đổi cú pháp assertion (pytest.raises thay cho toThrow). Điều quan trọng là bộ quy tắc — boundary value, failure path, không mock unit — áp dụng cho mọi ngôn ngữ.
Mẹo: Dán hẳn đoạn business rule từ ticket/PRD vào prompt thay vì diễn giải lại bằng trí nhớ — AI bám sát rule gốc tốt hơn nhiều so với rule bị bạn tóm tắt lại qua nhiều lớp.
Sinh integration test kiểm thử hành vi thật
Integration test (kiểm thử tích hợp) khác unit test ở chỗ nó chấp nhận, thậm chí đòi hỏi, việc chạy qua nhiều lớp thật: real database, real HTTP server, real message queue. Vấn đề phổ biến nhất khi để AI viết integration test là nó lại mock hết mọi thứ — kết quả là bạn có một bài test tự xưng "integration" nhưng thực chất chỉ là unit test đội lốt, mock repository, mock service, mock luôn cả DB connection. Dạng này không bắt được các lỗi thật sự nguy hiểm: câu SQL sai cú pháp, migration thiếu cột, transaction không rollback đúng, race condition khi hai request chạm cùng một row.
Cách khắc phục thực tế nhất hiện nay: dùng Testcontainers để spin lên một Postgres/MySQL thật trong Docker chỉ cho vòng đời của test suite, không đụng tới DB thật hay DB CI chia sẻ. Với Node.js có testcontainers npm package, Python có testcontainers-python, Java có Testcontainers gốc từ lâu.
Ví dụ prompt cho integration test luồng redeem coupon (áp dụng mã giảm giá vào đơn hàng), đòi hỏi DB thật:
Viết integration test bằng Jest cho luồng redeem coupon trong CouponService.redeem(orderId, couponCode).
Yêu cầu môi trường:
- Dùng @testcontainers/postgresql để khởi tạo Postgres 15 thật trong Docker cho mỗi test run
- Chạy migration thật (dùng script migrate.ts hiện có) trước khi test, không mock schema
- Kết nối tới DB test bằng cùng Prisma client mà production code dùng — không mock Prisma
Client hay repository layer
Kịch bản cần test:
1. Redeem coupon hợp lệ, còn hạn, đơn hàng đủ điều kiện -> order.discountAmount được cập nhật
đúng trong DB, coupon.usageCount tăng thêm 1
2. Redeem cùng một coupon 2 lần liên tiếp cho cùng user -> lần 2 phải bị từ chối nếu coupon
có giới hạn usage per user = 1, kiểm tra bằng cách query lại DB sau khi cả 2 request chạy
xong, không chỉ dựa vào return value
3. Redeem 2 request đồng thời (Promise.all) cùng một coupon có usageLimit = 1 -> chỉ đúng 1
request thành công, request còn lại nhận lỗi CouponExhaustedError (test race condition
thật, không giả lập)
4. Sau khi test xong, cleanup toàn bộ container, không để lại state
Không mock bất kỳ layer nào giữa CouponService và database. Chỉ được mock các dịch vụ bên thứ
ba thật sự nằm ngoài hệ thống (ví dụ payment gateway, email service).
Case số 3 ở trên là ví dụ kinh điển cho việc tại sao phải test với DB thật: race condition chỉ lộ ra khi có transaction thật, row locking thật. Mock hoàn toàn không thể mô phỏng được tình huống này.
Với hệ thống có HTTP layer, áp dụng tương tự: dùng test server thật (supertest cho Express, TestClient của FastAPI) thay vì mock request/response object. Chỉ mock những gì thực sự ở ngoài biên hệ thống của bạn — third-party API, payment gateway — bằng MSW hoặc nock ở tầng network, không mock ở tầng logic nội bộ.
Mẹo: Nếu CI của bạn chưa hỗ trợ Docker-in-Docker cho Testcontainers, thử Testcontainers Cloud hoặc ít nhất dùng SQLite in-memory cho case đơn giản — nhưng phải cảnh báo rõ trong code review rằng SQLite không bắt được lỗi cú pháp SQL đặc thù của Postgres.
Contract testing với Pact và API schema
Khi hai service độc lập giao tiếp qua API — ví dụ Order Service gọi Payment Service — unit test và integration test trong nội bộ mỗi service không đủ để đảm bảo hai bên "nói cùng ngôn ngữ". Đây là lúc cần contract testing (kiểm thử hợp đồng): xác nhận rằng request/response giữa consumer (bên gọi) và provider (bên được gọi) khớp với một "hợp đồng" đã thống nhất, mà không cần deploy cả hai service cùng lúc để test end-to-end.
Mô hình phổ biến nhất là consumer-driven contract với Pact. Quy trình gồm 3 bước:
- Consumer test — bên gọi (ví dụ Order Service) viết test mô tả nó mong đợi request/response như thế nào khi gọi Payment Service. Pact chạy test này với một mock provider, đồng thời sinh ra một pact file (JSON) ghi lại contract.
- Publish pact file lên Pact Broker — một service trung tâm lưu version của mọi contract giữa các service.
- Provider verification — bên cung cấp (Payment Service) tải pact file về, chạy provider test thật để xác nhận nó đáp ứng đúng contract đó. Nếu Payment Service đổi response shape mà không cập nhật, verification fail ngay trong CI trước khi deploy — tránh được lỗi "hai team không nói chuyện với nhau" kinh điển.
Ví dụ prompt để AI soạn consumer test cho việc Order Service gọi Payment Service để charge thẻ:
Viết Pact consumer test bằng @pact-foundation/pact cho PaymentClient.chargeCard().
Consumer: order-service
Provider: payment-service
Request thực tế mà PaymentClient gửi:
POST /v1/charges
Body: { "orderId": string, "amountVnd": number, "cardToken": string }
Response mong đợi khi thành công (HTTP 200):
{ "chargeId": string, "status": "succeeded", "capturedAmountVnd": number }
Response mong đợi khi thẻ bị từ chối (HTTP 402):
{ "error": "card_declined", "message": string }
Yêu cầu:
- Dùng matcher (like, string, integer từ @pact-foundation/pact) thay vì giá trị cứng, để
contract không quá chặt với dữ liệu cụ thể nhưng vẫn chặt về shape và type
- Viết 2 interaction: charge thành công, và charge bị decline
- Sau khi test pass, in ra đường dẫn pact file để publish lên Pact Broker
- Không gọi payment-service thật qua network — Pact tự dựng mock provider dựa trên
interaction đã định nghĩa
Với hệ thống nhỏ hơn, chưa cần đầu tư Pact Broker, một lựa chọn nhẹ hơn là validate theo OpenAPI/Swagger schema: dùng thư viện như jest-openapi hoặc dredd để assert response thực tế của API khớp với schema đã publish, hoặc dùng Postman/Newman chạy collection kèm schema validation trong CI. Cách này không có khái niệm "consumer-driven" hai chiều như Pact, nhưng rẻ hơn, dễ setup hơn, phù hợp khi bạn chỉ cần đảm bảo API không "âm thầm" đổi shape.
Riêng với mock ở tầng network cho test loại này — WireMock (Java) hoặc MSW (JavaScript/TypeScript) cho phép định nghĩa stub server thật sự lắng nghe HTTP, khác với việc mock module ở tầng code, nên vẫn giữ được độ trung thực cao hơn nhiều so với jest.mock thuần.
Mẹo: Đừng để Pact Broker là "nice to have" — gắn provider verification vào pipeline CI của provider service như một required check, nếu không sớm muộn contract cũng bị lãng quên và trôi (drift) khỏi thực tế.
Viết prompt để có assertion thực sự ý nghĩa
Assertion yếu là triệu chứng rõ nhất của test rỗng, và cũng là chỗ dễ sửa nhất nếu bạn biết prompt đúng cách. Ba dạng assertion yếu hay gặp nhất khi để AI tự do:
expect(result).toBeDefined()/toBeTruthy()— chỉ xác nhận có giá trị, không xác nhận giá trị đúng.expect(result).toMatchObject({})với object rỗng hoặc chỉ vài field không liên quan tới logic đang test.expect(spy).toHaveBeenCalled()mà không kiểm tra được gọi với argument gì, bao nhiêu lần.
Assertion có giá trị phải trả lời được câu hỏi: "Nếu logic sai một chút, test này có fail không?" Muốn ép AI đi theo hướng này, prompt cần chỉ định rõ loại assertion mong muốn, không chỉ nói "viết test đầy đủ".
Viết test cho function calculateStackedDiscount(orderTotal, coupons[]) — hàm tính tổng giảm
giá khi áp dụng nhiều coupon xếp chồng (stacking).
Yêu cầu về assertion, bắt buộc tuân theo:
- KHÔNG chấp nhận expect(result).toBeDefined(), toBeTruthy(), toBeInstanceOf(Object) cho
giá trị số hoặc object kết quả
- Với số tiền: assert đúng con số VND cụ thể, ví dụ
expect(result.finalTotal).toBe(72000), không phải expect(result.finalTotal).toBeGreaterThan(0)
- Với thay đổi state (side effect): sau khi gọi function, query lại object/DB record và assert
từng field đã đổi đúng giá trị mong đợi, không chỉ assert function "đã chạy xong không lỗi"
- Với lỗi: assert đúng loại Error/exception (theo class hoặc error code), và đúng message,
không chỉ assert expect(fn).toThrow() không kèm tham số
- Với function có side effect gọi ra ngoài (ví dụ ghi log, gọi API khác qua mock): assert
spy được gọi với đúng argument cụ thể bằng toHaveBeenCalledWith(...), không chỉ
toHaveBeenCalled()
Trả lời bằng cách liệt kê từng test case, ghi rõ input, expected output CHÍNH XÁC (số cụ thể)
trước khi viết code test.
Kỹ thuật đáng chú ý ở cuối prompt: yêu cầu AI liệt kê input/expected-output cụ thể trước khi viết code. Bước này buộc model phải tự tính toán expected value một cách tường minh thay vì "đoán" theo code hiện có — nếu AI tính sai, bạn phát hiện ngay ở bước liệt kê, rẻ hơn nhiều so với phát hiện sau khi đã có cả file test.
Mẹo: Sau khi AI sinh test, thử "temporarily" sửa một con số trong business logic (ví dụ đổi 10% thành 12%) rồi chạy lại test suite — nếu không có test nào đỏ, coverage của bạn có lỗ hổng thật, bất kể % coverage report nói gì.
Sinh dữ liệu test (Test Data Generation)
Chất lượng test phụ thuộc rất nhiều vào chất lượng dữ liệu đưa vào, và đây là mảng AI hỗ trợ cực tốt nếu bạn hướng dẫn đúng — vì sinh nhiều biến thể dữ liệu là việc AI làm nhanh hơn con người rất nhiều lần.
Ba tầng dữ liệu test nên có:
- Factory/builder pattern cho object hợp lệ mặc định, dễ override từng field khi cần. Ví dụ với TypeScript:
function buildCoupon(overrides: Partial<Coupon> = {}): Coupon {
return {
code: 'SAVE10',
type: 'percentage',
value: 10,
minOrderValue: 100000,
expiresAt: new Date('2026-12-31'),
usageLimit: 100,
usageCount: 0,
...overrides,
};
}
// dùng: buildCoupon({ expiresAt: new Date('2020-01-01') }) để test coupon hết hạn
-
Faker library (Faker.js, Python
faker) cho dữ liệu "thực tế" số lượng lớn — tên khách hàng, địa chỉ, số điện thoại — hữu ích khi test performance hoặc test tính duy nhất (uniqueness) trên số lượng lớn record, nhưng đừng lạm dụng cho unit test logic nghiệp vụ vì tính ngẫu nhiên làm test khó tái lập (non-deterministic) nếu không seed cố định. -
Bộ dữ liệu adversarial/edge-case — đây là phần AI giúp nhiều nhất vì con người hay bỏ sót:
- Giá trị biên: 0, số âm,
Number.MAX_SAFE_INTEGER, đúng bằng minOrderValue. - Chuỗi Unicode: tên khách hàng có emoji, tiếng Việt có dấu, ký tự right-to-left (tiếng Ả Rập), chuỗi rỗng, chuỗi rất dài.
- Ngày giờ biên timezone: coupon hết hạn đúng lúc nửa đêm UTC nhưng chưa hết hạn theo giờ Việt Nam (UTC+7), leap year 29/2, DST transition ở các nước áp dụng giờ mùa hè.
- Số tiền có phần thập phân do lỗi làm tròn floating point (0.1 + 0.2 trong JavaScript).
Prompt ví dụ để AI sinh bộ dữ liệu adversarial cho coupon service:
Sinh bộ test data (dưới dạng mảng object TypeScript) cho Coupon, tập trung vào edge case và
adversarial input, dùng để test hàm validateCoupon(coupon, now).
Bao gồm ít nhất các case sau, mỗi case ghi rõ comment giải thích nó test gì:
- Coupon code chứa khoảng trắng đầu/cuối (" SAVE10 ")
- Coupon code toàn chữ thường khi hệ thống lưu chữ hoa (case sensitivity)
- expiresAt đúng bằng thời điểm `now` (boundary: hết hạn ngay tại thời điểm kiểm tra)
- expiresAt là 23:59:59 ngày 31/12 theo giờ UTC+7 nhưng mới 16:59:59 UTC cùng ngày
(kiểm tra lỗi so sánh timezone)
- usageCount == usageLimit - 1 (còn đúng 1 lượt) và usageCount == usageLimit (hết lượt)
- minOrderValue = 0 (coupon không giới hạn đơn tối thiểu)
- value âm hoặc value = 0 cho loại percentage (dữ liệu hỏng, phải bị từ chối ở tầng validate)
- code chứa ký tự đặc biệt SQL injection thử nghiệm ("SAVE10'; DROP TABLE coupons;--")
để chắc chắn tầng validate không crash và không pass query thẳng xuống DB
Không sinh dữ liệu ngẫu nhiên không kiểm soát được — mỗi entry phải deterministic và có
comment giải thích rationale.
Mẹo: Lưu bộ adversarial data này thành fixture dùng chung (
coupon.fixtures.ts) thay vì lặp lại trong từng file test — khi phát hiện edge case mới ngoài production, thêm một entry vào đây và mọi test suite liên quan tự động được bảo vệ thêm.
Thực hành: Sinh bộ test đầy đủ cho một Coupon Service
Giờ ghép mọi kỹ thuật ở trên vào một ví dụ thực tế: Coupon Service cho một hệ thống thương mại điện tử, với 4 nghiệp vụ chính:
apply(orderTotal, coupon): áp dụng một coupon (percentage hoặc fixed amount) vào đơn hàng, có kiểm traminOrderValue.validate(coupon, now): kiểm tra coupon còn hạn (expiresAt), còn lượt dùng (usageCount < usageLimit).stack(orderTotal, coupons[]): áp dụng nhiều coupon cùng lúc theo rule "percentage tính trước, fixed trừ sau", có giới hạn tối đa 2 coupon stack cùng lúc.expire(couponId): đánh dấu coupon hết hiệu lực thủ công, ghi nhận vào DB, gửi eventcoupon.expiredra message queue.
Prompt tổng để sinh trọn bộ test — unit, integration, contract:
Bạn là senior QA engineer. Sinh bộ test đầy đủ cho CouponService gồm 4 method: apply, validate,
stack, expire. Tách theo 3 tầng, mỗi tầng một file riêng:
1. UNIT TEST (coupon.unit.test.ts) — Jest, không mock logic nội bộ:
- Test apply(): percentage discount, fixed discount, đơn hàng dưới minOrderValue (phải throw)
- Test validate(): coupon hết hạn, coupon hết lượt dùng, coupon hợp lệ, boundary tại đúng
thời điểm expiresAt
- Test stack(): đúng 2 coupon (percentage trước, fixed sau), thử stack 3 coupon (phải throw
StackLimitExceededError), thử stack coupon không hợp lệ (đã hết hạn) trong danh sách
- Assertion: luôn dùng số VND cụ thể, không dùng toBeDefined/toBeTruthy cho số tiền
- Boundary value analysis đầy đủ cho minOrderValue và usageLimit
2. INTEGRATION TEST (coupon.integration.test.ts) — Testcontainers Postgres thật, Prisma client
thật, không mock DB:
- expire() phải cập nhật đúng field isActive=false trong DB và publish event
'coupon.expired' (mock message queue publisher vì đây là external boundary)
- Race condition: 2 request redeem đồng thời cùng coupon usageLimit=1, chỉ 1 thành công
- Rollback: nếu publish event thất bại, transaction DB phải rollback, coupon không bị
đánh dấu expired
3. CONTRACT TEST (coupon.pact.test.ts) — Pact, consumer là coupon-service, provider là
notification-service (nơi tiêu thụ event coupon.expired qua HTTP webhook thay vì queue
trong ví dụ giả định này):
- Định nghĩa interaction POST /webhooks/coupon-expired với payload
{ couponId: string, expiredAt: string(ISO 8601) }
- Dùng matcher, không hardcode giá trị cụ thể
Với mỗi file, liệt kê danh sách test case (tên + input + expected output) trước, chờ tôi duyệt,
rồi mới viết code đầy đủ.
Một đoạn trích từ output thực tế (phần unit test cho stack), minh hoạ đúng tinh thần "assert giá trị cụ thể, test cả failure path":
describe('CouponService.stack', () => {
it('should apply percentage coupon before fixed coupon when stacking 2 coupons', () => {
const result = couponService.stack(500000, [
buildCoupon({ type: 'percentage', value: 10 }),
buildCoupon({ type: 'fixed', value: 20000 }),
]);
// 500,000 - 10% = 450,000; sau đó - 20,000 = 430,000
expect(result.finalTotal).toBe(430000);
expect(result.appliedOrder).toEqual(['percentage', 'fixed']);
});
it('should throw StackLimitExceededError when stacking more than 2 coupons', () => {
const coupons = [buildCoupon(), buildCoupon(), buildCoupon()];
expect(() => couponService.stack(500000, coupons)).toThrow(StackLimitExceededError);
});
});
Checklist review cho reviewer khi nhận test do AI sinh (dùng trước khi merge):
- [ ] Không còn assertion kiểu
toBeDefined()/toBeTruthy()cho giá trị số hoặc state quan trọng. - [ ] Mỗi business rule trong spec có ít nhất một test case tương ứng, map 1-1 rõ ràng.
- [ ] Có test cho cả boundary trên và boundary dưới của mọi ngưỡng số (min/max).
- [ ] Integration test chạy với DB/service thật, không mock layer đang được test.
- [ ] Test failure path (throw, reject, error code) không chỉ test happy path.
- [ ] Thử "cố tình" sửa sai 1 chỗ trong logic — ít nhất 1 test phải đỏ (mutation nhanh bằng tay).
- [ ] Contract test dùng matcher hợp lý, không quá lỏng (match mọi thứ) hoặc quá chặt (hardcode giá trị không cần thiết).
- [ ] Test data adversarial (Unicode, timezone, giá trị âm) đã được đại diện, không chỉ có "happy" fixture.
Mẹo: Chạy thử toàn bộ suite này với một mutation testing tool (Stryker cho JS/TS, mutmut cho Python) trước khi merge — nếu mutation score thấp dù coverage cao, đó là bằng chứng rõ ràng nhất cho thấy AI đã sinh test rỗng ở đâu đó.
Những điểm chính
- Test AI sinh mặc định thiên về "hollow" — pass nhanh nhưng không bắt bug — vì model tối ưu cho việc test pass, không tối ưu cho việc test catch được lỗi.
- Prompt cho unit test cần đủ 4 thứ: function signature, business rule bằng lời, danh sách edge case cụ thể, chỉ thị không mock unit đang test.
- Integration test phải chạy với dependency thật (Testcontainers, real HTTP server) — mock hết mọi thứ biến integration test thành unit test đội lốt.
- Contract testing (Pact hoặc OpenAPI schema validation) giải quyết bài toán "hai service không nói cùng ngôn ngữ" mà không cần deploy song song để test end-to-end.
- Ép AI liệt kê input/expected-output cụ thể trước khi viết code test giúp lộ sai sót tính toán sớm, và tự động đẩy assertion về hướng cụ thể thay vì mơ hồ.
- Dữ liệu test nên có 3 tầng: factory mặc định hợp lệ, faker cho dữ liệu số lượng lớn, và bộ adversarial/edge-case do AI hỗ trợ sinh nhưng con người review kỹ.
- Luôn có bước xác minh độc lập với coverage report — sửa sai 1 chỗ trong logic bằng tay hoặc chạy mutation testing để chắc chắn test thật sự "cắn" được khi code hỏng.
- Checklist review trước khi merge test do AI viết không phải bước thừa — đó là nơi phát hiện phần lớn test rỗng trước khi nó vào production.