การ 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

  1. Consumer เขียน test ที่ระบุสิ่งที่ต้องการจาก provider อย่างชัดเจน
  2. การรัน test จะสร้าง pact file ซึ่งเป็นเอกสาร JSON ที่บันทึกความคาดหวังเหล่านั้นไว้
  3. Provider จะรัน service จริงของตนเทียบกับ pact file ใน CI pipeline
  4. หาก 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 และดังที่ตัวเลขของทีมแสดงให้เห็น มันสามารถกำจัดความล้มเหลวในการเชื่อมต่อให้หมดไปได้เลย