Skip to content
Bùi Thúc Đồng Notebook · since 2009
§ 03 · TUTORIAL INTERMEDIATE ~20 MIN UPDATED 2026-09-23

Đồng Use Claude — Phần 6: TEST, nghiệm thu như QA thật

Nghiệm thu không phải mở app lên thấy chạy. Là chạy đúng kịch bản đã viết trong plan, bắt AI đưa output thật — và biết phân biệt claim nào giữ, claim nào không. Kèm TEST-01.md và output verify thật của app sổ khách.

“Nghiệm thu” với người không biết code thường nghĩa là: mở app lên, bấm lung tung một lúc, thấy không crash → ký nhận. Đó là cách cổng thanh toán treo cứng ngay ngày mở bán. Nghiệm thu đúng là chạy kịch bản đã viết từ bước PLAN, đối chiếu output thật với kỳ vọng, và ghi kết quả vào file.

Bài 6 trong series. App sổ khách đã build xong theo PLAN-01 — giờ nghiệm thu. Mọi lệnh và output trong bài này đã chạy thật trên bản app mẫu kèm series (Cloudflare Workers + Hono + D1).

File TEST-01.md — kịch bản nghiệm thu

Mỗi [PRF-n] trong plan thành một kịch bản: lệnh chạy → output kỳ vọng → output thật → pass/fail.

# Test: sổ khách v1
Ref: plan/PLAN-01.md §4. Môi trường: wrangler dev (local D1)

- [ ] TC-1 [PRF-1] Tạo khách mới → 201, merged:false
- [ ] TC-2 [PRF-2] Cùng phone viết kiểu +84 → merged:true,
  trả về id cũ, ghi chú nối vào
- [ ] TC-3 [PRF-3] Import CSV có 2 dòng trùng →
  added:1, merged:1
- [ ] TC-4 [PRF-4] 10 POST đồng thời cùng phone →
  đúng 1 row trong DB
- [ ] TC-5 [PRF-5] Mở / trên điện thoại → form dùng được

Lệnh setup (chạy thật, copy được):

cd mini-crm
npm install
cp .dev.vars.example .dev.vars   # passkey local: dong-2026
npx wrangler d1 execute DB --local --file=schema.sql
npx wrangler dev --port 8790    # cổng 8787 bận thì đổi cổng

App có passkey gate (chi tiết ở Phần 7) — mọi lệnh curl bên dưới đều kèm header -H 'x-passkey: dong-2026'. Không có header → API trả 401.

TC-1, TC-2: tạo và gộp trùng

curl -s -w '\nHTTP %{http_code}\n' -X POST http://localhost:8790/api/customers \
  -H 'content-type: application/json' -H 'x-passkey: dong-2026' \
  -d '{"name":"An Nguyen","phone":"0901 234 567","email":"[email protected]","note":"Khách mới"}'

Output thật:

{"merged":false,"customer":{"id":1,"phone_norm":"0901234567","email":"[email protected]","note":"Khách mới"}}
HTTP 201

Phone "0901 234 567" đã được chuẩn hoá thành 0901234567 — đúng [BR-1]. Giờ nhập lại cùng người đó, kiểu phone khác:

curl -s -X POST http://localhost:8790/api/customers \
  -H 'content-type: application/json' -H 'x-passkey: dong-2026' \
  -d '{"name":"An Nguyen","phone":"+84 901-234-567","note":"Gọi lại sau 5h"}'
{"merged":true,"customer":{"id":1,"note":"Khách mới\nGọi lại sau 5h"}}

+84 901-234-567 → cùng 0901234567 → gộp vào id:1, ghi chú mới nối sau ghi chú cũ — đúng [BR-3]. Đây là lúc nghiệm thu đúng nghĩa: bạn không đọc code, bạn đọc output và đối chiếu với luật đã viết trong spec.

TC-4: kịch bản đối kháng — 10 request cùng lúc

Kịch bản phân biệt app thật và demo: mười nhân viên cùng nhập một khách trong cùng giây.

for i in $(seq 1 10); do
  curl -s -o "race-$i.json" -w "%{http_code} " \
    -X POST http://localhost:8790/api/customers \
    -H 'content-type: application/json' -H 'x-passkey: dong-2026' \
    -d "{\"name\":\"Race $i\",\"phone\":\"0988 111 222\",\"note\":\"req $i\"}" &
done; wait

Output thật:

201 200 200 200 200 200 200 200 200 200

Đúng một request thắng (201, tạo mới), chín request còn lại rơi vào nhánh merge (200) — và cả 10 đều trả về cùng một id. Kiểm chứng ở tầng DB luôn:

npx wrangler d1 execute DB --local --command \
  "SELECT phone_norm, COUNT(*) AS n FROM customers GROUP BY phone_norm HAVING n > 1"
# → "results": []  — không số phone nào xuất hiện hai lần

Bài học từ output thật: claim nào giữ, claim nào không

Đây là phần quan trọng nhất của bài. Khi chạy kịch bản đồng thời ở trên, output thật cho thấy: đúng 1 row được tạo — nhưng trong 10 ghi chú gửi lên, chỉ 2 cái sống sót trong row cuối. Lý do: UNIQUE constraint giữ lời hứa “không trùng phone”, còn bước gộp ghi chú là đọc-rồi-ghi — mười request đua nhau ghi đè.

Nghiệm thu thật là vậy: claim [BR-2] “không bao giờ hai bản ghi cùng phone” giữ — có bằng chứng SQL. Nhưng một claim ai cũng ngầm cho là đúng — “gộp thì không mất thông tin” — không giữ dưới tải đồng thời. Phát hiện này không có nghĩa app hỏng; nó có nghĩa spec thiếu một luật. Việc của bạn (Risk Owner) là quyết: fix ngay (INSERT ... ON CONFLICT DO UPDATE — merge nguyên tử), hay ghi vào known-limitation vì nghiệp vụ hiếm khi có 10 người nhập cùng một khách trong một giây.

Đó là quyết định nghiệp vụ — AI không quyết thay bạn được. Nó chỉ đưa bằng chứng; bạn chọn chấp nhận hay sửa.

TC-5: test bằng tay trên thiết bị thật

Kịch bản cuối không chạy bằng curl: mở http://localhost:8790 trên điện thoại (cùng wifi, dùng IP máy), nhập vài khách bằng ngón tay. Form một cột, nút full-width — nếu phải zoom hay bấm hụt, đó là bug UI cần ghi vào file, không phải “tạm chấp nhận”.

Quy ước ký nhận

Khi tất cả TC pass, thêm vào cuối TEST-01.md:

## Kết quả — 2026-09-23
TC-1..5 PASS trên local. Known limitation: ghi chú có
thể mất khi >2 request cùng merge một phone đồng thời
→ quyết định: chấp nhận cho v1, ghi vào backlog.

Ký nhận: [tên bạn]

Từ đây app được “bàn giao” — và mọi lần sửa sau này đều có mốc để hỏi: nó còn pass TEST-01 không?

Bài tập

  1. Viết test/TEST-01.md từ các PRF trong plan của bạn — một PRF = một TC.
  2. Chạy TC-4 tương đương cho app của bạn: tìm hành vi “hai người cùng làm một lúc” và bắt nó thành kịch bản đối kháng.
  3. Với mỗi claim trong spec, tự hỏi: output nào chứng minh? Claim nào không có output chứng minh được là claim viết cho vui.

Nguồn tham khảo

Bài tiếp theo: Phần 7 — DEPLOY.