2 นาที

พรอมต์ Claude Code สำหรับสร้างเทสต์ในกรณีขอบเขต

เรียนรู้พรอมต์สำหรับสร้างเทสต์ด้วย 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 เป็นตารางกะทัดรัด เพื่อให้เห็นช่องว่างชัดเจน:

DimensionValid edgeJust outside“Weird” valueExpected behavior
length0-110,000error 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 เพื่อไม่ให้เทสต์พึ่งพาลำดับการรัน

ตัวอย่าง: แปลงฟีเจอร์หนึ่งเป็นชุดเทสต์เล็กแต่แข็งแรง

สร้างการสอดคล้องด้านพฤติกรรมกับทีม
ดึงเพื่อนร่วมงานมาในพื้นที่ทำงานเดียวกันเพื่อตกลงสัญญาและ invariants ตั้งแต่ต้น

สมมติฟีเจอร์หนึ่ง: ใช้คูปองที่เช็คเอาต์

สัญญา (เล็กและทดสอบได้): ให้ 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?

เทสต์โหมดความล้มเหลวที่ดีต้องพิสูจน์สองอย่าง:

  1. ฟังก์ชันคืน ข้อผิดพลาดที่ชัดเจนและคาดไว้ (ชนิด/ข้อความ/สถานะ)
  2. มันล้มเหลว อย่างปลอดภัย:
  • ไม่มีการเปลี่ยนสถานะบางส่วน
  • ไม่มีการเปิดเผยรายละเอียดภายใน
  • ไม่มี 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” ให้เสริมความเข้มหรือถอดทิ้ง

Related posts