การ deploy เมื่อเร็วๆ นี้ทำให้ microservices สามตัวพังลง แม้ว่า unit test, integration test และการตรวจสอบด้วย mock-server ทุกอย่างจะผ่านทั้งหมด ทีมงานจึงเปลี่ยนจากการใช้ API mocks มาเป็นการทำ contract testing แทน ในเวลาหกเดือน จำนวน contract เพิ่มขึ้นจาก 3 เป็น 47 ฉบับ และอัตราการล้มเหลวของการเชื่อมต่อ (integration-failure rate) รายเดือนลดลงจากสองครั้งเหลือศูนย์
ทำไม mock ถึงปกป้องคุณไม่ได้
Mock server ทำหน้าที่เพียงแค่เลียนแบบรูปแบบ (shape) ที่ consumer คาดหวังไว้ แต่มันไม่เคยตรวจสอบว่า provider ส่งข้อมูลในรูปแบบนั้นจริงๆ หรือไม่ หาก provider เปลี่ยนชื่อฟิลด์ เช่น จาก name เป็น display_name ตัว mock จะยังคงส่ง payload แบบเดิม ทำให้ผลการทดสอบของ consumer ยังคงเป็นสีเขียว (pass) แต่ระบบจริงกลับล่ม ความล้มเหลวบน production เมื่อเวลา 14:00 น. ของวันอังคารนั้นเป็นเช่นนั้นเอง นั่นคือ mock ได้ "โกหก" เกี่ยวกับ contract ที่แท้จริง
Contract testing ช่วยเติมเต็มช่องว่างนี้
Contract testing บังคับให้ทั้งสองฝั่งของ API ต้องตกลงในนิยามที่ใช้ร่วมกันก่อนที่โค้ดจะถูกนำขึ้น production โดยมีสองแนวทางที่นิยมใช้กัน:
- Consumer-driven contracts – บริการที่เป็น consumer จะเขียนความคาดหวัง (expectations) ไว้ และบริการที่เป็น provider จะทำหน้าที่ตรวจสอบความถูกต้อง วิธีนี้ใช้ได้ดีกับ microservices ภายในองค์กรที่มีการพัฒนาไปพร้อมๆ กัน
- Provider-driven contracts – provider จะเผยแพร่ specification และ consumer จะตรวจสอบโค้ดของตนเทียบกับ specification นั้น นี่เป็นรูปแบบปกติสำหรับ public APIs
แนวทางแรกมักจะช่วยป้องกันการเชื่อมต่อที่ผิดพลาดภายในสถาปัตยกรรม microservice ได้ดี
วิธีการทำงานของ consumer-driven contract
- Consumer เขียน test ที่ระบุสิ่งที่ต้องการจาก provider อย่างชัดเจน
- การรัน test จะสร้าง pact file ซึ่งเป็นเอกสาร JSON ที่บันทึกความคาดหวังเหล่านั้นไว้
- Provider จะรัน service จริงของตนเทียบกับ pact file ใน CI pipeline
- หาก provider เปลี่ยนแปลงฟิลด์ การตรวจสอบจะล้มเหลวและ build จะถูกระงับไว้
เนื่องจากกระบวนการตรวจสอบทำงานบนโค้ดจริงของ provider การเปลี่ยนแปลงที่ส่งผลกระทบ (breaking change) จึงถูกตรวจพบตั้งแต่เนิ่นๆ ไม่ใช่หลังจาก deploy ไปแล้ว
ตำแหน่งของ contract tests ใน test pyramid ของคุณ
- Unit tests – รวดเร็ว ทดสอบ logic ที่แยกส่วนกัน
- Contract tests – ความเร็วปานกลาง ยืนยันว่าข้อตกลงของ API ยังคงถูกต้อง
- End-to-end tests – ช้า ทดสอบกระบวนการทางธุรกิจแบบครบวงจร (full business flows)
ให้มองว่า contract tests เป็นสะพานเชื่อมระหว่างการตอบสนองที่รวดเร็วของ unit tests และความครอบคลุมที่กว้างขวางของ end-to-end suites มุ่งเน้นไปที่จุดเชื่อมต่อ (integration points) ที่เสียบ่อยที่สุด และเริ่มต้นด้วย endpoint สำคัญเพียงสองหรือสามจุด
เรื่องราวการนำไปใช้งานจริง
ทีมที่เป็นที่มาของบทความนี้เริ่มต้นด้วย contract เพียง 3 ฉบับที่ครอบคลุมการเรียกใช้งานที่เปราะบางที่สุด ผ่านไป 6 เดือน พวกเขามี contract ถึง 47 ฉบับที่ครอบคลุมการรับส่งข้อมูลระหว่าง service ส่วนใหญ่ ในช่วงเวลานั้น เหตุการณ์ API พังลดลงจากเดือนละสองครั้งเหลือศูนย์
เมื่อไหร่ที่ contract testing อาจไม่คุ้มค่า
- คุณเป็นนักพัฒนาคนเดียวที่เก็บทุก service ไว้ใน repository เดียวกัน
- API มีความเสถียรสูงมากและไม่มีการเปลี่ยนแปลงมานานหลายปี
- คุณกำลังสร้าง prototype ที่สร้างขึ้นเพื่อทดสอบและจะถูกทิ้งในไม่ช้า
ในสถานการณ์เหล่านั้น ภาระในการดูแลรักษา contract อาจมีมากกว่าประโยชน์ที่ได้รับ
ข้อเสียที่อาจเกิดขึ้นและวิธีบรรเทาปัญหา
- จัดการ version ของ contract ควบคู่ไปกับโค้ดที่ระบุไว้
- ทำการตรวจสอบแบบอัตโนมัติในทุกการรัน CI เพื่อหลีกเลี่ยง contract ที่ล้าสมัย
- ตรวจสอบการเปลี่ยนแปลงของ contract ใน pull requests เพื่อตรวจจับการพังที่ไม่ได้ตั้งใจ
บทสรุป
หากคุณยังคงพึ่งพา mock ที่สร้างขึ้นเองเพื่อหลอกตัวเองว่า service ของคุณสามารถสื่อสารกันได้ คุณกำลังเดิมพันกับคำสัญญาที่ผิดพลาด Contract testing จะเปลี่ยนการเดิมพันนั้นให้กลายเป็นข้อตกลงที่ตรวจสอบได้ ช่วยตรวจพบการเปลี่ยนแปลงที่ส่งผลกระทบก่อนที่จะถึง production และดังที่ตัวเลขของทีมแสดงให้เห็น มันสามารถกำจัดความล้มเหลวในการเชื่อมต่อให้หมดไปได้เลย
