พรอมต์ Claude Code สำหรับสร้างเทสต์ในกรณีขอบเขต
เรียนรู้พรอมต์สำหรับสร้างเทสต์ด้วย Claude Code ที่ผลิตชุดทดสอบสัญญาณสูงโดยมุ่งที่ขอบเขต อินแวเรียนต์ และโหมดความล้มเหลว แทนการทดสอบแค่เส้นทางปกติ

ทำไมการสร้างเทสต์แบบ happy-path ถึงสิ้นเปลืองเวลา
ชุดทดสอบที่สร้างโดยอัตโนมัติมักดูน่าประทับใจ: หลายสิบเทสต์ โค้ดตั้งค่ามากมาย และชื่อฟังก์ชันทุกตัวปรากฏที่ไหนสักแห่ง แต่เทสต์เหล่านั้นหลายอันเป็นเพียงการตรวจว่า “มันทำงานเมื่อทุกอย่างปกติ” เท่านั้น พวกมันผ่านได้ง่าย ไม่ค่อยจับบั๊ก และยังเสียเวลาที่ต้องอ่านและดูแลรักษา
ด้วยพรอมต์สร้างเทสต์ Claude Code ทั่วไป โมเดลมักเลียนแบบอินพุตตัวอย่างที่เห็น คุณจะได้ผลลัพธ์ที่ดูต่างกันแต่ครอบคลุมพฤติกรรมเดิม ผลคือชุดใหญ่แต่การครอบคลุมสำคัญบางจุดบางครั้งแผ่ว
เทสต์ที่มีสัญญาณสูงแตกต่าง: มันคือชุดเล็กๆ ที่น่าจะจับเหตุการณ์ที่เกิดขึ้นเมื่อเดือนที่แล้วได้ มันล้มเมื่อพฤติกรรมเปลี่ยนในทางเสี่ยง และคงเสถียรเมื่อรีแฟกเตอร์ที่ไม่เป็นอันตรายเกิดขึ้น เทสต์สัญญาณสูงหนึ่งชิ้นอาจคุ้มค่ากับการตรวจสอบ “คืนค่าตามที่คาด” ยี่สิบชิ้น
การสร้างแบบ happy-path ที่มีค่าน้อยมักมีสัญญาณชัดเจนบางอย่าง:
- เทสต์หลายอันต่างกันแค่ป้ายป้อนข้อมูล ไม่ใช่สิ่งที่จะพัง
- Assertion ผิวเผิน ("ไม่เป็น null", "สถานะเป็น 200") แทนที่จะตรวจความหมายจริง
- การตั้งค่าหนักกว่าพฤติกรรมที่ทดสอบ ผู้คนจึงหยุดอัปเดตเทสต์
- ความคุ้มครองดูสูง แต่กรณีขอบเขตไม่ได้ถูกแตะเลย
ลองนึกถึงฟังก์ชันที่ใช้รหัสส่วนลด เทสต์แบบ happy-path ยืนยันว่า "SAVE10" ลดราคาได้ แต่บั๊กจริงอาจซ่อนที่อื่น: ราคาที่เป็น 0 หรือลบ รหัสหมดอายุ ขอบการปัดเศษ หรือเพดานส่วนลด นั่นคือกรณีที่จะทำให้ยอดรวมผิด ลูกค้าโกรธ และต้องย้อนคืนตอนกลางคืน
เป้าหมายคือย้ายจาก “เทสต์มากขึ้น” เป็น “เทสต์ที่ดีกว่า” โดยมุ่งไปที่สามเป้าหมาย: ขอบเขต (boundaries), โหมดความล้มเหลว (failure modes), และ invariants
สามเป้าหมาย: ขอบเขต, โหมดความล้มเหลว, อินแวเรียนต์
ถ้าคุณต้องการเทสต์หน่วยที่มีสัญญาณสูง หยุดขอแค่ “เพิ่มเทสต์” แล้วเริ่มขอสามชนิดนี้ นี่คือแกนกลางของพรอมต์สร้างเทสต์ Claude Code ที่ให้การครอบคลุมที่มีประโยชน์ แทนที่จะเป็นกองของการตรวจ "ทำงานกับอินพุตปกติ"
1) ขอบเขต (ที่บั๊กมักแอบอยู่)
ขอบเขตคือขอบของสิ่งที่โค้ดยอมรับหรือผลิต ข้อผิดพลาดจริงหลายอย่างเป็นปัญหา off-by-one, สถานะว่าง, หรือตั้งเวลาเกินที่ไม่เคยปรากฏใน happy path
คิดเป็นค่าต่ำสุดและค่าสูงสุด (0, 1, ความยาวสูงสุด), ว่างกับมีค่า ("", [], nil), off-by-one (n-1, n, n+1), และขอบเวลา (ใกล้จุดตัด)
ตัวอย่าง: ถ้า API ยอมรับ "สูงสุด 100 รายการ" ให้ทดสอบ 100 และ 101 ไม่ใช่แค่ 3
2) โหมดความล้มเหลว (พิสูจน์ว่าล้มอย่างปลอดภัย)
โหมดความล้มเหลวคือวิธีที่ระบบอาจพัง: อินพุตไม่ถูกต้อง การขาด dependency ผลลัพธ์บางส่วน หรือข้อผิดพลาดจาก upstream เทสต์โหมดความล้มเหลวที่ดีตรวจพฤติกรรมภายใต้ความเครียด ไม่ใช่แค่ผลลัพธ์ภายใต้สภาพสมบูรณ์แบบ
ตัวอย่าง: เมื่อการเรียกฐานข้อมูลล้มเหลว ฟังก์ชันคืนข้อผิดพลาดที่ชัดเจนและหลีกเลี่ยงการเขียนข้อมูลบางส่วนหรือไม่?
3) Invariants (กฎที่ต้องเป็นจริงเสมอ)
Invariants คือความจริงที่ต้องคงอยู่ก่อนและหลังการเรียก พวกมันเปลี่ยนความถูกต้องที่คลุมเครือให้เป็นการอ้างสิทธิ์ที่ชัดเจน
ตัวอย่าง:
- “ยอดคงเหลือไม่เคยติดลบ” หลังการถอนเงินใดๆ
- “IDs ต้องไม่ซ้ำ” แม้สร้างรายการอย่างรวดเร็ว
- “เมื่อเกิดข้อผิดพลาด ไม่มีการเปลี่ยนแปลงสถานะ” (ไม่มีแถวใหม่ ไม่มี flag ถูกพลิก)
เมื่อคุณมุ่งที่สามเป้าหมายนี้ คุณจะได้เทสต์น้อยลง แต่แต่ละเทสต์มีสัญญาณมากขึ้น
เตรียมตัว: ดึงสัญญาสั้นๆ ออกมาก่อนเขียนเทสต์
ถ้าคุณขอเทสต์เร็วเกินไป มักได้กองของการตรวจ "ใช้งานได้ตามปกติ" แก้ไขง่ายคือเขียนสัญญาเล็กๆ ก่อน แล้วสร้างเทสต์จากสัญญานั้น นี่เป็นวิธีเร็วที่สุดในการแปลงพรอมต์ Claude Code ให้เป็นสิ่งที่จับบั๊กจริงได้
สัญญาที่มีประโยชน์สั้นพอที่จะอ่านได้ในหนึ่งหายใจ ตั้งเป้า 5–10 บรรทัดที่ตอบสามคำถาม: อะไรเข้ามา, อะไรออกไป, และอะไรจะเปลี่ยนแปลง
แบบฟอร์มสัญญา 5–10 บรรทัด
เขียนสัญญาเป็นภาษาธรรมดา ไม่ใช่โค้ด และรวมเฉพาะสิ่งที่ทดสอบได้
- Inputs: ชนิดข้อมูล ช่วงที่อนุญาต และอะไรถือว่าเป็น “ว่าง” หรือ “ขาดหาย”
- Output: ค่าที่คืนหรือรูปแบบข้อผิดพลาด และสิ่งที่การเรียกแบบสำเร็จรับประกัน
- Side effects: การเปลี่ยนแปลงสถานะ แถวฐานข้อมูล การเรียกเครือข่าย ไฟล์ บันทึก
- Assumptions: สิ่งที่ผู้เรียกมักทำผิด (เขตเวลา การเข้ารหัส การยืนยันตัวตน ลำดับ)
- “ห้ามเกิด”: แครช สูญหายข้อมูลโดยเงียบ เก็บเงินซ้ำ การเขียนบางส่วน
เมื่อคุณมีสัญญานั้นแล้ว สแกนหาจุดที่ความเป็นจริงอาจทำลายสมมติฐานของคุณ จุดเหล่านั้นจะกลายเป็นกรณีขอบเขต (min/max, ศูนย์, overflow, สตริงว่าง, ซ้ำ) และโหมดความล้มเหลว (timeout, permission denied, ข้อจำกัดความเป็นเอกลักษณ์, อินพุตเสียหาย)
นี่คือตัวอย่างคอนกรีตสำหรับฟีเจอร์อย่าง reserveInventory(itemId, qty):
สัญญาอาจระบุว่า qty ต้องเป็นจำนวนเต็มบวก ฟังก์ชันต้องทำงานเป็นอะตอม และจะไม่สร้างสต็อกติดลบ นั่นจะนำไปสู่เทสต์สัญญาณสูงทันที: qty = 0, qty = 1, qty มากกว่าคงเหลือ, การเรียกพร้อมกัน, และการบังคับให้ฐานข้อมูลเกิดข้อผิดพลาดกลางทาง
ถ้าคุณใช้เครื่องมือแบบ vibe-coding เช่น Koder.ai กระบวนการเดียวกันนี้นำไปใช้ได้: เขียนสัญญาในแชทก่อน แล้วสร้างเทสต์ที่โจมตีขอบเขต โหมดความล้มเหลว และรายการ “ห้ามเกิด” โดยตรง
รูปแบบพรอมต์: แผนแม่แบบเทสต์ที่มีสัญญาณสูง
ใช้พรอมต์สร้างเทสต์ Claude Code นี้เมื่อคุณต้องการเทสต์น้อยลง แต่แต่ละชิ้นต้องมีน้ำหนัก การเคลื่อนไหวสำคัญคือบังคับให้มีแผนการทดสอบก่อน แล้วจึงสร้างโค้ดทดสอบหลังจากคุณอนุมัติแผน
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.
ทริคปฏิบัติคือขอให้ boundary matrix เป็นตารางกะทัดรัด เพื่อให้เห็นช่องว่างชัดเจน:
| Dimension | Valid edge | Just outside | “Weird” value | Expected behavior |
|---|---|---|---|---|
| length | 0 | -1 | 10,000 | error vs clamp vs accept |
ถ้า Claude เสนอ 20 เทสต์ ให้บีบให้เหลือ ถามให้รวมเคสที่คล้ายกันและเก็บเฉพาะที่จับบั๊กจริงได้ (off-by-one, ประเภทข้อผิดพลาดผิด, สูญหายข้อมูลเงียบ, invariant แตก)
ขั้นตอนทีละขั้น: รันพรอมต์แล้วแปลงผลเป็นเทสต์
เริ่มด้วยสัญญาเล็กๆ ที่เป็นรูปธรรมสำหรับพฤติกรรมที่ต้องการ วาง signature ของฟังก์ชัน คำอธิบายสั้นๆ ของอินพุตและเอาต์พุต และเทสต์ที่มีอยู่ (แม้จะเป็นเพียง happy-path) วิธีนี้ช่วยให้โมเดลยึดกับสิ่งที่โค้ดทำจริง ไม่ใช่สิ่งที่มันเดา
ถัดมา ให้ขอตารางความเสี่ยงก่อนขอโค้ดทดสอบ ใส่สามคอลัมน์: ขอบเขต (edges of valid input), โหมดความล้มเหลว (bad input, missing data, timeouts), และ invariants (กฎที่ต้องเป็นจริง) เติมหนึ่งประโยคต่อแถวว่า “ทำไมสิ่งนี้ถึงพังได้” ตารางเรียบง่ายเผยช่องว่างได้เร็วกว่ากองไฟล์เทสต์
จากนั้นเลือกชุดเทสต์ที่เล็กที่สุดที่แต่ละอันมีจุดประสงค์จับบั๊กแตกต่างกัน ถ้าสองเทสต์ล้มเพราะสาเหตุเดียวกัน ให้เก็บอันที่แข็งแรงกว่า
กฎการเลือกปฏิบัติ:
- เก็บเทสต์ที่โดนขอบเขตต่างกัน (min, max, empty, off-by-one)
- เก็บเทสต์ที่พิสูจน์การทำงานอย่างปลอดภัยเมื่อเกิดความล้มเหลว (error ชัดเจน, ไม่มีการเขียนบางส่วน, ไม่มีแครช)
- เก็บเทสต์ที่ยืนยัน invariant (ลำดับ, ยอดรวม, idempotency, ไม่มีการซ้ำ)
- ตัดเทสต์ที่แค่ซ้ำว่า “ทำงานกับอินพุตปกติ”
สุดท้าย ให้คำอธิบายสั้นๆ ต่อเทสต์ว่า: ถ้าเทสต์นี้ล้ม มันจะจับบั๊กรูปแบบไหน ถ้าประโยคนั้นคลุมเครือ ("ยืนยันพฤติกรรม") เทสต์นั้นมักเป็น low-signal
วิธีเข้ารหัส invariant ลงใน assertion
Invariant คือกฎที่ต้องเป็นจริงไม่ว่าอินพุตที่ถูกต้องใดจะถูกส่งเข้าไป สำหรับการทดสอบเชิง invariant ให้เขียนกฎเป็นภาษาธรรมดา แล้วแปลงเป็น assertion ที่ล้มดังและชัดเจนได้
เลือก 1–2 invariants ที่จริงๆ แล้วปกป้องคุณจากบั๊กจริง Invariant ที่ดีมักเกี่ยวกับความปลอดภัย (ไม่สูญหายข้อมูล), ความสอดคล้อง (ผลเดียวกันกับอินพุตเดียวกัน), หรือขีดจำกัด (ไม่เกินเพดาน)
แปลง invariant ให้เป็นเช็คที่พิสูจน์ได้
เขียน invariant เป็นประโยคสั้นๆ แล้วตัดสินใจว่าหลักฐานที่จะสังเกตได้คืออะไร: ค่าที่คืน ข้อมูลที่เก็บ เหตุการณ์ที่ปล่อย หรือการเรียกไปยัง dependency การอ้างสิทธิ์ที่แข็งแรงเช็คทั้งผลลัพธ์และ side effects เพราะบั๊กหลายตัวซ่อนอยู่ที่ "คืน OK แต่เขียนผิด"
ตัวอย่าง เช่น ฟังก์ชันที่ใช้คูปองกับคำสั่งซื้อ:
- Invariant: ยอดท้ายสุดไม่เป็นลบ
- Invariant: การใช้คูปองเดียวกันสองครั้งไม่ลดสองครั้ง
ตอนนี้เข้ารหัสพวกนั้นเป็น assertion ที่วัดได้:
expect(result.total).toBeGreaterThanOrEqual(0)
expect(db.getOrder(orderId).discountCents).toBe(originalDiscountCents)
หลีกเลี่ยง assert คลุมเครืออย่าง “คืนค่าตามที่คาด” ให้ assert กฎเฉพาะ (ไม่ติดลบ) และ side effect เฉพาะ (ส่วนลดเก็บครั้งเดียว)
เพิ่มบันทึกตัวอย่างที่ทำให้เกิดความผิดพลาดเพื่อให้เทสต์ยังคมอยู่
สำหรับแต่ละ invariant ให้เพิ่มบันทึกสั้นๆ ในเทสต์ว่า ข้อมูลแบบไหนที่จะผิดเงื่อนไขนี้ วิธีนี้ช่วยให้เทสต์ไม่กลายเป็นการตรวจ happy-path ในภายหลัง
รูปแบบง่ายๆ ที่ทนทานเมื่อเวลาผ่านไป:
- ใส่ invariant ในชื่อเทสต์
- ตรวจ invariant บนผลลัพธ์
- ตรวจ side effect สำคัญ (หรือการไม่มี side effect)
- เพิ่มคอมเมนต์หนึ่งบรรทัดเกี่ยวกับกรณีที่จะละเมิด (เช่น คูปองมูลค่าสูงมากหรือการใช้ซ้ำ)
โหมดความล้มเหลว: เขียนเทสต์ที่พิสูจน์การล้มอย่างปลอดภัย
เทสต์สัญญาณสูงมักเป็นเทสต์ที่ยืนยันว่าโค้ดล้มอย่างปลอดภัย หากโมเดลเขียนแค่เทสต์แบบ happy-path คุณจะไม่รู้อะไรเลยเกี่ยวกับพฤติกรรมเมื่้ออินพุตหรือ dependency สกปรก
เริ่มด้วยการตัดสินใจว่า “ปลอดภัย” หมายความว่าอย่างไรสำหรับฟีเจอร์นี้ คืนข้อผิดพลาดเป็นชนิดที่พิมพ์ได้หรือ fallback ไปค่าเริ่มต้น หรือ retry หนึ่งครั้งแล้วหยุด? เขียนพฤติกรรมที่คาดไว้ในหนึ่งประโยคแล้วให้เทสต์พิสูจน์มัน
เมื่อขอ Claude Code สำหรับเทสต์โหมดความล้มเหลว ให้เข้มงวด: ครอบคลุมวิธีที่ระบบจะพัง และ assert ตอบสนองที่คุณต้องการ บรรทัดที่มีประโยชน์คือ: “ชอบเทสต์น้อยชิ้นที่มี assertion แข็งแรง มากกว่าเทสต์หลายอันที่ตื้น”
หมวดความล้มเหลวที่มักให้เทสต์ดีที่สุด:
- อินพุตไม่ถูกต้อง: ฟอร์แมตไม่ถูกต้อง ฟิลด์จำเป็นหายไป ค่านั้นอยู่นอกช่วง
- ความล้มเหลวของ dependency: timeout, 500, ตอบกลับว่าง, payload เสียหาย
- ปัญหาลำดับ: เหตุการณ์มาถึงไม่เรียง ซ้ำ การเขียนบางส่วน
- ความขนาน: อัปเดตแข่งกัน, การตรวจ idempotent
- พฤติกรรมกู้คืน: คืน error หรือ fallback หรือ retry
ตัวอย่าง: มี endpoint สร้างผู้ใช้และเรียกบริการอีเมลส่งข้อความต้อนรับ เทสต์แบบ low-value ตรวจแค่ว่า “คืน 201.” แต่เทสต์โหมดความล้มเหลวที่มีสัญญาณจะตรวจว่า หากบริการอีเมล timeout คุณจะ (a) ยังคงสร้างผู้ใช้และคืน 201 พร้อม flag “email_pending” หรือ (b) คืน 503 ชัดเจนและไม่สร้างผู้ใช้ เลือกพฤติกรรมอย่างใดอย่างหนึ่ง แล้ว assert ทั้ง response และ side effects
ยังต้องทดสอบว่าคุณไม่รั่วไหลอะไร ถ้าการตรวจสอบไม่ผ่าน ให้แน่ใจว่าไม่มีอะไรถูกเขียนลง DB ถ้า dependency คืน payload เสียหาย ให้แน่ใจว่าไม่โยน exception ไม่ระบาย stack trace ดิบๆ
กับดักทั่วไปที่สร้างเทสต์ไร้ค่า
ชุดเทสต์ที่มีค่าน้อยมักเกิดเมื่อโมเดลถูกรางวัลด้วยปริมาณ ถ้าพรอมต์ Claude Code ของคุณขอ “20 unit tests” มักได้การเปลี่ยนแปลงเล็กๆ น้อยๆ ที่ดูเหมือนครอบคลุมแต่จับอะไรไม่ได้
กับดักทั่วไป:
- เทสต์หน้าตาเหมือนกัน: เทสต์อินพุตที่ถูกต้องซ้ำๆ กับสตริงหรือตัวเลขต่างกัน
- เทสต์ที่สะท้อนโค้ด: อ้างถึงขั้นตอนภายในหรือ helper แทนพฤติกรรมที่สังเกตได้
- ม็อกทุกอย่าง: แทน DB, นาฬิกา, เครือข่าย, config ทั้งหมด
- Assertion อ่อน: แค่เช็ค “ไม่มีข้อผิดพลาด”, “ไม่เป็น null”, หรือ “สถานะ 200”
- สถานะที่สกปรกร่วมกัน: ทิ้งข้อมูล seeded, global เปลี่ยนแปลง, หรือ cache
ตัวอย่าง: ฟังก์ชัน “create user” สิบเทสต์แบบ happy-path อาจเปลี่ยนอีเมลเป็นหลายรูปแบบแต่พลาดสิ่งสำคัญ: ปฏิเสธอีเมลซ้ำ, รหัสผ่านว่าง, และยืนยันว่า ID ที่คืนไม่มีซ้ำและคงที่
เส้นทางช่วยในการตรวจสอบ:
- ขอบเขต: ให้แต่ละเทสต์ชื่อความเสี่ยงที่มันครอบคลุม (boundary, failure mode, หรือ invariant)
- หลีกเลี่ยงการตรวจเฉพาะการทำงานภายใน ยกเว้นมันเปลี่ยนพฤติกรรมที่สังเกตได้
- ม็อกให้น้อยที่สุด และอนุญาตให้มีเทสต์บางชิ้นที่ทดสอบ integration จริงเมื่อเป็นไปได้
- บังคับ assertion แข็งแรง: ผลลัพธ์ที่แน่นอน, การเปลี่ยนสถานะ, และประเภท/ข้อความของข้อผิดพลาด
- เพิ่มกฎ cleanup เพื่อไม่ให้เทสต์พึ่งพาลำดับการรัน
ตัวอย่าง: แปลงฟีเจอร์หนึ่งเป็นชุดเทสต์เล็กแต่แข็งแรง
สมมติฟีเจอร์หนึ่ง: ใช้คูปองที่เช็คเอาต์
สัญญา (เล็กและทดสอบได้): ให้ subtotal ของตะกร้าเป็นเซ็นต์และคูปองเป็นออปชัน คืนยอดสุดท้ายเป็นเซ็นต์ กฎ: คูปองเปอร์เซ็นต์ปัดเศษลงเป็นเซ็นต์ที่ใกล้เคียงที่สุด คูปองคงที่หักเป็นจำนวนคงที่ และยอดไม่ควรติดลบ คูปองอาจไม่ถูกต้อง หมดอายุ หรือถูกใช้แล้ว
อย่าขอแค่ว่า “เทสต์ applyCoupon()” แต่ขอการทดสอบกรณีขอบเขต โหมดความล้มเหลว และ invariants ผูกกับสัญญานี้
ขอบเขตที่บังคับพฤติกรรมขอบ
เลือกอินพุตที่มักทำให้การคำนวณหรือการตรวจสอบพัง: รหัสคูปองว่าง, subtotal = 0, subtotal ใกล้ต่ำสุดและสูงสุด, ส่วนลดคงที่มากกว่ายอดสั่งซื้อ, และเปอร์เซ็นต์เช่น 33% ที่ทำให้เกิดการปัดเศษ
โหมดความล้มเหลวเพื่อพิสูจน์การล้มอย่างปลอดภัย
สมมติว่าการค้นหาคูปองอาจพังและสถานะอาจผิด: บริการคูปองล่ม คูปองหมดอายุ หรือคูปองถูกใช้แล้วโดยผู้ใช้คนนี้ เทสต์ควรพิสูจน์ผลลัพธ์ถัดไป (ปฏิเสธคูปองด้วยข้อผิดพลาดชัดเจน ยอดไม่เปลี่ยน)
ชุดเทสต์สัญญาณสูงขั้นต่ำ (5 เทสต์) และสิ่งที่แต่ละอันจับได้:
- ปฏิเสธโค้ดว่างหรือช่องว่าง: จับบั๊กที่รับช่องว่างเป็นค่าถูกต้องและการ trim ผิดพลาด
- ปัดเศษคูปองเปอร์เซ็นต์ (subtotal 101, 33%): จับข้อผิดพลาดการปัดเศษและ off-by-one เซนต์
- ส่วนลดคงที่มากกว่ายอด (subtotal 500, discount 1000): พิสูจน์ invariant ว่ายอดไม่ควรติดลบ
- ขอบขั้นต่ำในการซื้อ (subtotal 999 vs 1000): จับตรรกะการเปรียบเทียบผิด (< กับ <=)
- การค้นหาคูปองล้มหรือ timeout: พิสูจน์ fallback ที่ปลอดภัย (ไม่ใช้ส่วนลด) และการจัดการข้อผิดพลาดที่เสถียร
ถ้าเทสต์พวกนี้ผ่าน คุณได้ครอบคลุมจุดแตกหักทั่วไปโดยไม่เติมชุดด้วยเทสต์ happy-path ซ้ำๆ
เช็คลิสต์ด่วนสำหรับเทสต์ที่ AI สร้างแล้วมีสัญญาณสูง
ก่อนยอมรับผลที่โมเดลสร้าง ให้ตรวจสอบอย่างรวดเร็ว เป้าหมายคือเทสต์แต่ละอันต้องปกป้องคุณจากบั๊กที่เฉพาะและน่าจะเกิดจริง
ใช้เช็คลิสต์นี้เป็นเกต:
- ขอบเขตต่ออินพุต: สำหรับแต่ละฟิลด์อินพุต (สตริง, ID, timestamp, flag) ให้รวมอย่างน้อยหนึ่งกรณีขอบ (ว่าง vs ช่องว่าง, ความยาวสูงสุด, ศูนย์ vs ลบ, ฟิลด์ออปชันหายไป, หนึ่งเกินขีดจำกัด)
- ความล้มเหลวของ dependency: รวมอย่างน้อยหนึ่งเทสต์ที่ dependency ทำงานผิดพลาด (timeout ฐานข้อมูล, API ภายนอก 500, โทเค็นหมดอายุ) พิสูจน์พฤติกรรมที่ปลอดภัย (error ชัดเจน, ไม่มีการเขียนบางส่วน)
- Invariants พร้อม assertion แข็งแรง: เลือก 1–3 กฎที่ต้องเป็นจริงและ assert โดยตรง หลีกเลี่ยง assert คลุมเครืออย่าง “response ok”
- หนึ่งบั๊กเฉพาะต่อเทสต์: อ่านชื่อเทสต์แต่ละอันแล้วถามว่า “เทสต์นี้จะจับบั๊กอะไร?” ถ้าสองเทสต์ตอบคำถามเดียวกัน ให้รวมเข้าด้วยกัน
- เทสต์ลบ: ลองลบเทสต์ ถ้าไม่มีอะไรสำคัญหายไป (ไม่มี boundary, ไม่มี failure mode, ไม่มี invariant) เทสต์นั้นไม่ได้สมควรอยู่
ทริคปฏิบัติหลังการสร้าง: เปลี่ยนชื่อเทสต์เป็น “should <พฤติกรรม> when <เงื่อนไขขอบ>” และ “should not <ผลร้าย> when <ความล้มเหลว>” ถ้าชื่อไม่เปลี่ยนง่าย เทสต์นั้นมักไม่เฉพาะเจาะจงพอ
ถ้าคุณสร้างแอปผ่านแชท ให้รันวงจรนี้ใน Koder.ai (koder.ai) เพื่อให้สัญญา แผน และเทสต์ที่สร้างอยู่ในที่เดียว เมื่อรีแฟกเตอร์เปลี่ยนพฤติกรรมโดยไม่คาดคิด สแนปช็อตและการย้อนคืนจะช่วยให้เปรียบเทียบและปรับจนชุดสัญญาณสูงคงที่
คำถามที่พบบ่อย
How many unit tests should I generate per function?
ค่าเริ่มต้น: ตั้งเป้าเป็นชุดเล็กที่สามารถจับบั๊กจริงได้
เกณฑ์ที่ใช้งานได้ดีคือ 6–10 เทสต์ต่อหน่วย (ฟังก์ชัน/โมดูล). ถ้าต้องการมากกว่านี้ มักหมายความว่าหน่วยนั้นทำงานมากเกินไปหรือสัญญา (contract) ยังไม่ชัดเจน
What’s wrong with generating lots of happy-path tests?
เทสต์แบบ happy-path ส่วนใหญ่แค่ยืนยันว่า ตัวอย่างยังทำงานได้ พวกมันมักพลาดสิ่งที่จะพังในสภาพแวดล้อมจริง
เทสต์ที่มีสัญญาณสูงจะมุ่งเป้าไปที่:
- ขอบเขต (Boundaries) (0/1/max, ว่าง/ null, off-by-one)
- โหมดความล้มเหลว (Failure modes) (timeout, input ไม่ถูกต้อง, dependency error)
- Invariants (กฎที่ต้องเป็นจริงเสมอ เช่น “ไม่เขียนข้อมูลบางส่วนเมื่อเกิดข้อผิดพลาด”)
What should I write down before asking an AI to generate tests?
เริ่มจาก สัญญาเล็กๆ ที่อ่านได้ในครั้งเดียว:
- Inputs: ชนิดข้อมูล ช่วงที่อนุญาต และอะไรถือว่าเป็นค่าว่าง/ขาดหาย
- Outputs: รูปร่างของผลสำเร็จและข้อผิดพลาด
- Side effects: สิ่งที่จะถูกเขียน/เปลี่ยน (DB, ไฟล์, เครือข่าย)
- “ห้ามเกิด”: การแครช สูญหายข้อมูลโดยเงียบ เก็บเงินซ้ำ การเขียนแบบไม่สมบูรณ์
แล้วจึงสร้างเทสต์จากสัญญานี้ ไม่ใช่จากตัวอย่างอย่างเดียว
Which boundary cases are usually worth testing?
ทดสอบกรณีเหล่านี้ก่อน:
- ค่าสูงสุด/ต่ำสุด (0, 1, max, มากกว่า max)
- ว่าง vs มีค่า ("", [], null)
- off-by-one (n-1, n, n+1)
- ขอบด้านฟอร์แมต (สตริงมีแค่ช่องว่าง, เลขนำ, ฯลฯ)
- ขอบเวลา (ก่อน/หลังวันหมดอายุ)
เลือก 1–2 กรณีต่อมิติของอินพุต เพื่อให้แต่ละเทสต์ครอบคลุมความเสี่ยงที่ต่างกัน
How do I write a good “failure mode” test instead of a shallow one?
เทสต์โหมดความล้มเหลวที่ดีต้องพิสูจน์สองอย่าง:
- ฟังก์ชันคืน ข้อผิดพลาดที่ชัดเจนและคาดไว้ (ชนิด/ข้อความ/สถานะ)
- มันล้มเหลว อย่างปลอดภัย:
- ไม่มีการเปลี่ยนสถานะบางส่วน
- ไม่มีการเปิดเผยรายละเอียดภายใน
- ไม่มี retry หรือ side effect ที่ไม่ตั้งใจ
ถ้ามีการเขียนฐานข้อมูล ให้เสมอตรวจก่อนและหลังเหตุการณ์ผิดพลาดว่ามีอะไรเกิดขึ้นใน storage
How do I turn an invariant into a test assertion?
แนวทางปกติ: เปลี่ยน invariant เป็นการอ้างสิทธิ์บนผลสังเกตได้ (observable outcome).
ตัวอย่าง:
- “ยอดรวมท้ายที่สุดต้องไม่ติดลบ” →
expect(total).toBeGreaterThanOrEqual(0) - “เมื่อเกิดข้อผิดพลาด ห้ามเปลี่ยนสถานะ” → ตรวจสอบ ไม่มีแถวใหม่ / ไม่มี flag ถูกพลิก
- “Idempotent” → เรียกสองครั้งแล้วยืนยันว่าการเรียกครั้งที่สองไม่เปลี่ยนสถานะ
ควรตรวจทั้ง ค่าที่ส่งกลับ และ side effects เพราะบั๊กหลายตัวซ่อนอยู่ที่ “คืนค่า OK แต่เขียนผิด”
When is a happy-path test still worth writing?
คุ้มค่าที่จะเก็บเทสต์แบบ happy-path เมื่อมันปกป้อง invariant หรือการรวมที่สำคัญ
เหตุผลที่ดีในการเก็บ:
- ยืนยัน invariant สำคัญสำหรับอินพุตปกติ (เช่น กฎการปัดเศษ)
- ล็อกสัญญาของ API ที่ผู้เรียกพึ่งพา
- ป้องกันการ regressions จากเหตุการณ์ในอดีต
ถ้าไม่ใช่ ให้แลกเป็นเทสต์ขอบเขต/ความล้มเหลวที่จับบั๊กได้มากกว่า
What should I ask the model to output before generating test code?
บังคับเอา PHASE 1: แผนเท่านั้น มาก่อน
ให้โมเดลส่ง:
- ข้อเสนอ 6–10 เทสต์สูงสุด
- สำหรับแต่ละอัน: เจตนา, การเตรียม, อินพุต, ผลที่คาด, ทำไมถึงมีสัญญาณสูง
- ตาราง boundary ขนาดเล็ก
- รายการ failure-mode
- 3–5 invariants และวิธีตรวจสอบ
ก็ต่อเมื่อคุณอนุมัติแผนแล้วจึงสั่งให้สร้างโค้ด (PHASE 2). วิธีนี้ป้องกันการได้ผลเป็น 20 เทสต์ลอกกันไปมา
How do I avoid tests that are brittle because they mock too much?
แนวทางเริ่มต้น: mock เฉพาะขอบเขตที่คุณไม่ควบคุม (DB/เครือข่าย/นาฬิกา) และเก็บที่เหลือให้เป็นของจริง
เพื่อลดการม็อกเกินไป:
- อย่า mock helper ภายในเพียงเพราะต้องการสะท้อน implementation
- ใช้ in-memory หรือ fake เล็กๆ ที่มีพฤติกรรมชัดเจนเมื่อทำได้
- mock นาฬิกา/randomness เมื่อมันส่งผลต่อ assertion เท่านั้น
ถ้าเทสต์พังเพราะรีแฟกเตอร์แต่พฤติกรรมไม่เปลี่ยน มักแปลว่า mock เยอะหรือผูกกับ implementation มากเกินไป
How can I quickly tell if an AI-generated test is low-value?
ใช้การลบทดสอบแบบง่าย:
- ถ้าคุณลบเทสต์แล้ว ไม่เสียขอบเขต, ไม่เสีย failure mode, และ ไม่เสีย invariant เทสต์นั้นไม่ได้สมควรอยู่
สแกนหาเทสต์ซ้ำ:
- ถ้าสองเทสต์จะแพ้เพราะบั๊กเดียวกัน ให้เก็บอันที่มี assertion แข็งแรงกว่า
- ถ้า assertion แค่ “not null” หรือ “status 200” ให้เสริมความเข้มหรือถอดทิ้ง