ทันทีที่ผมรัน contract tests ของ Specmatic กับโปรเจกต์ Zerodha clone ที่ใช้ MERN-stack ซึ่งผมคิดว่า "เสร็จสมบูรณ์" แล้ว เครื่องมือนี้กลับตรวจพบข้อบกพร่อง (defects) จริงถึง 5 จุดที่การทดสอบด้วยมือ (manual checks) ของผมไม่เคยตรวจเจอเลย จาก test cases ทั้งหมด 178 เคสที่ถูกสร้างขึ้น ชุดทดสอบนี้ทำให้ API พังในรูปแบบที่จะส่งผลกระทบต่อผู้ใช้งานจริง ไม่ว่าจะเป็นประเภทข้อมูลที่ไม่ถูกต้อง (invalid data types), การแครชเมื่อได้รับข้อมูลล็อกอินที่ผิดรูปแบบ (malformed credentials), ข้อมูลเสียหายแบบเงียบๆ (silent data corruption), การสมัครสมาชิกที่ไม่เป็นแบบ idempotent และการเปลี่ยนแปลง contract ที่ส่งผลกระทบจนทำให้ CI gate หยุดทำงานทันที
ทำไมบั๊กเหล่านี้ถึงหลุดรอดจากการทดสอบด้วยมือ
โปรเจกต์นี้ประกอบด้วย Node.js backend, React front-end, การจัดเก็บข้อมูลด้วย MongoDB และใช้ Razorpay สำหรับการชำระเงิน ผมได้ทดสอบขั้นตอนการสมัครสมาชิกและขั้นตอนการชำระเงินด้วยตัวเองแล้ว และทุกอย่างก็ดูเหมือนจะทำงานได้ปกติ อย่างไรก็ตาม การทดสอบด้วยมือ (manual testing) มักจะทดสอบแค่ "happy path" เท่านั้น นั่นคือการยืนยันว่าโค้ดทำงานได้ถูกต้องเมื่อผู้ใช้งานทำตามขั้นตอนที่กำหนดไว้ แต่มันไม่ได้พิสูจน์ว่าบริการจะสามารถรับมือกับคำขอที่ผิดรูปแบบ (malformed requests) หรือพฤติกรรมที่ไม่คาดคิดของ client ได้
เมื่อผมใช้ Specmatic กับโค้ดที่มีอยู่ ตัว contract ซึ่งเป็นคำอธิบายที่ชัดเจนเกี่ยวกับรูปแบบ request และ response ของแต่ละ endpoint จะทำหน้าที่เป็น "source of truth" จากนั้นเครื่องมือจะสร้าง matrix ของสถานการณ์ทั้งแบบ positive และ negative จำนวนมหาศาลขึ้นมาโดยอัตโนมัติ ซึ่งหลายสถานการณ์เป็นสิ่งที่นักทดสอบที่เป็นมนุษย์อาจจะนึกไม่ถึงว่าจะต้องเขียนขึ้นมา
ข้อบกพร่อง 5 จุดที่ถูกค้นพบ
- ช่องโหว่ในการตรวจสอบข้อมูลนำเข้า (Input validation gaps) – endpoint
/newOrderยอมรับตัวเลขทศนิยมและข้อความ (strings) ในฟิลด์quantityทั้งที่ contract กำหนดให้เป็นจำนวนเต็ม (integer) การทดสอบที่ถูกสร้างขึ้นซึ่งส่งข้อมูลผิดประเภทจึงทำให้ API ทำงานผิดพลาด - ข้อผิดพลาดในการล็อกอินที่ไม่ได้จัดการไว้ (Unhandled login errors) – การส่งข้อมูลล็อกอินที่ผิดรูปแบบไปยังเส้นทาง authentication ทำให้เกิด runtime exception เนื่องจากโค้ดขาดการตรวจสอบประเภทข้อมูล (type checks)
- ข้อมูลการชำระเงินเสียหายแบบเงียบๆ (Silent payment corruption) – endpoint
/verify-paymentยอมรับค่า boolean สำหรับฟิลด์amountเมื่อค่าtrueหลุดเข้าไปในระบบ ฐานข้อมูลจึงบันทึกว่าการชำระเงินสำเร็จด้วยมูลค่าเป็นศูนย์ ซึ่งทำให้ตัวเลขรายได้สูงเกินจริงโดยไม่มีใครรู้ - ขาดคุณสมบัติ idempotency (Missing idempotency) – การรันขั้นตอนการสมัครสมาชิกซ้ำเป็นครั้งที่สองในชุดทดสอบเกิดข้อผิดพลาด เนื่องจาก endpoint พยายามสร้างผู้ใช้ซ้ำแทนที่จะจัดการกับข้อมูลที่ซ้ำกันอย่างเหมาะสม
- ตรวจพบการเปลี่ยนแปลง contract ที่ส่งผลกระทบ (Breaking contract change caught) – ผมลองจงใจเปลี่ยนประเภทข้อมูลใน contract และพบว่า CI pipeline ปฏิเสธการเปลี่ยนแปลงนั้นทันที ช่วยป้องกันไม่ให้มีการปล่อย release ที่จะทำให้ระบบพัง
ทำไม contract testing ถึงสำคัญสำหรับ CI pipelines
- การทดสอบแบบ negative ในสเกลใหญ่ – เคสส่วนใหญ่จาก 178 เคสคือข้อมูลแบบ edge-case ซึ่งการเขียนทดสอบเหล่านี้ด้วยมือจะใช้เวลานานจนไม่คุ้มค่า
- ความปลอดภัยสำหรับ client ภายนอก – Contract คือสิ่งที่กำหนดว่าบริการสัญญาว่าจะให้อะไรแก่ผู้ใช้งานภายนอก หากการเขียนโค้ด (implementation) เริ่มไม่ตรงตามสัญญา การทดสอบ contract จะล้มเหลว ซึ่งช่วยปกป้องแอปพลิเคชันที่ใช้งานต่อจากเรา (downstream apps)
- วงจรการตอบกลับที่รวดเร็ว (Fast feedback loop) – CI gate ช่วยหยุดการเปลี่ยนแปลงที่ส่งผลกระทบก่อนที่จะมีการ merge ช่วยให้ทีมไม่ต้องเผชิญกับการ rollback ที่มีค่าใช้จ่ายสูง
- คุณภาพโค้ดที่ดีขึ้น – การเพิ่ม actuator endpoint และการทำให้ขั้นตอนการสมัครสมาชิกเป็นแบบ idempotent เป็นขั้นตอนที่จำเป็นเพื่อให้สามารถทดสอบ contract ได้ ซึ่งส่งผลให้บริการมีความแข็งแกร่ง (hardened) มากขึ้น
สิ่งที่นักพัฒนาควรชั่งน้ำหนัก (Trade-off)
การทำ contract testing เพิ่มภาระในการดูแลรักษา (maintenance overhead) โดยที่ specification จะต้องสอดคล้องกับโค้ดอยู่เสมอ และกระบวนการสร้าง test อาจทำให้เวลาในการ build นานขึ้น ทีมงานจึงต้องตัดสินใจว่าความปลอดภัยที่เพิ่มขึ้นนั้นคุ้มค่ากับความพยายามที่ต้องเสียไปหรือไม่ โดยเฉพาะในโปรเจกต์ขนาดเล็กที่การทดสอบด้วยมืออาจดูเพียงพอแล้ว
สิ่งที่ควรจับตามองต่อไป
- การนำไปใช้ใน CI ที่กว้างขวางขึ้น – เมื่อมีทีมต่างๆ นำ contract suites เข้ามาใช้ใน pipeline มากขึ้น เครื่องมือต่างๆ ก็น่าจะเร็วขึ้นและปรับแต่งได้มากขึ้น
- รูปแบบ contract ที่เป็นมาตรฐาน – specification ใหม่ๆ ที่กำลังเกิดขึ้นอาจช่วยให้การแชร์ contract ระหว่างบริการและทีมต่างๆ ทำได้ง่ายขึ้น
- การอัปเดต spec แบบอัตโนมัติ – เครื่องมือที่สามารถอนุมาน (infer) contract จากการเปลี่ยนแปลงของโค้ดอาจช่วยลดภาระในการดูแลรักษาด้วยมือ
หากคุณทึกทักเอาเองว่า API ของคุณแข็งแกร่งเพียงเพราะ UI ทำงานได้อย่างราบรื่น การรัน contract test อาจเผยให้เห็นข้อผิดพลาดที่ซ่อนอยู่ซึ่งการตรวจสอบด้วยมือตรวจไม่พบ การเพิ่ม executable contract เข้าไปใน CI pipeline ของคุณจะเปลี่ยนจากคำว่า “ดูเหมือนจะดี” ให้กลายเป็น “พิสูจน์แล้วว่าปลอดภัย”
Repository: https://github.com/priya3054/zerodha-specmatic
