Should You Avoid End-to-End Tests? (คุณควรหลีกเลี่ยง End-to-End Test หรือไม่)
แม้จะมีข้อเสียที่ระบุไว้ ข้างต้น สำหรับผู้ใช้หลายคน end-to-end test ก็ยังคงจัดการได้กับ microservices จำนวนน้อย และในสถานการณ์เหล่านั้นมันก็ยังคงสมเหตุสมผลมาก แต่จะเกิดอะไรขึ้นกับ 3, 4, 10 หรือ 20 service ล่ะ test suite เหล่านี้จะบวมขึ้นอย่างรวดเร็วมาก และในกรณีที่แย่ที่สุดอาจส่งผลให้เกิดการระเบิดของ scenario ที่ต้องทดสอบแบบ Cartesian
อันที่จริง แม้กระทั่งกับ microservices จำนวนน้อย เทสต์เหล่านี้ก็จะยากขึ้นเมื่อคุณมีหลายทีมแชร์ end-to-end test ร่วมกัน เมื่อมี end-to-end test suite ที่แชร์กัน คุณก็บั่นทอนเป้าหมายของ independent deployability ความสามารถของทีมคุณในการ deploy microservice ตอนนี้ต้องพึ่งพาให้ test suite ที่แชร์กันโดยหลายทีมนั้นผ่าน
หนึ่งในปัญหาสำคัญที่เรากำลังพยายามแก้เมื่อเราใช้ end-to-end test ตามที่ระบุไว้ก่อนหน้านี้คืออะไร เรากำลังพยายามมั่นใจว่าเมื่อเรา deploy service ใหม่เข้าสู่ production การเปลี่ยนแปลงของเราจะไม่ทำให้ consumer พัง ทีนี้ อย่างที่เราครอบคลุมอย่างละเอียดใน “Structural Versus Semantic Contract Breakages” การมี schema ที่ชัดเจนสำหรับ interface ของ microservice ของเราสามารถช่วยจับ structural breakage ได้ และแน่นอนว่านั่นสามารถลดความจำเป็นของ end-to-end test ที่ซับซ้อนกว่าได้
อย่างไรก็ตาม schema ไม่สามารถจับ semantic breakage ได้ นั่นคือการเปลี่ยนแปลงใน พฤติกรรม ที่ทำให้เกิดการพังเพราะความไม่เข้ากันแบบย้อนหลัง end-to-end test สามารถช่วยจับ semantic breakage เหล่านี้ได้อย่างแน่นอน แต่ก็ต้องแลกมาด้วยต้นทุนที่สูงมาก โดยหลักการแล้ว เราต้องการเทสต์บางประเภทที่สามารถจับ semantic breaking change ได้และรันในขอบเขตที่ลดลง ปรับปรุงการแยกส่วนของเทสต์ (และดังนั้นความเร็วของ feedback) นี่คือจุดที่ contract test และ consumer-driven contract เข้ามามีบทบาท
Contract Tests and Consumer-Driven Contracts (CDCs) (Contract Test และ Consumer-Driven Contract)
ด้วย contract test ทีมที่ microservice ของตัวเองใช้บริการจาก external service จะเขียนเทสต์ที่อธิบายว่าพวกเขาคาดหวังให้ external service นั้นทำงานอย่างไร นี่ไม่ใช่การทดสอบ microservice ของตัวเองเป็นหลัก แต่เป็นการระบุว่าคุณคาดหวังให้ external service ทำงานอย่างไรมากกว่า หนึ่งในเหตุผลหลักที่ contract test เหล่านี้มีประโยชน์คือมันสามารถถูกรันกับ stub หรือ mock ใดๆ ที่คุณใช้แทน external service ได้ contract test ของคุณควรผ่านเมื่อคุณรันกับ stub ของคุณเอง เช่นเดียวกับที่มันควรผ่านกับ external service ตัวจริง
Contract test จะมีประโยชน์มากเมื่อถูกใช้เป็นส่วนหนึ่งของ consumer-driven contract (CDC) contract test นี้ในความเป็นจริงคือการแทนค่าอย่างชัดเจนและเชิงโปรแกรมของวิธีที่ consumer (upstream) microservice คาดหวังให้ producer (downstream) microservice ทำงาน ด้วย CDC ทีม consumer จะมั่นใจว่า contract test เหล่านี้ถูกแชร์กับทีม producer เพื่อให้ทีม producer มั่นใจได้ว่า microservice ของพวกเขาตรงตามความคาดหวังเหล่านี้ โดยทั่วไปแล้ว สิ่งนี้ทำได้โดยให้ทีม producer downstream รัน consumer contract สำหรับแต่ละ microservice ที่เป็น consumer เป็นส่วนหนึ่งของ test suite ของตัวเองที่จะถูกรันในทุก build สิ่งสำคัญมากจากมุมมองของ test feedback คือ เทสต์เหล่านี้ต้องถูกรันกับ producer ตัวเดียวแบบแยกส่วนเท่านั้น ดังนั้นมันจึงเร็วกว่าและน่าเชื่อถือกว่า end-to-end test ที่มันอาจแทนที่ได้
ตัวอย่างเช่น ลองกลับมาดูสถานการณ์ก่อนหน้านี้ของเราอีกครั้ง Customer microservice มี consumer แยกกันสองตัว คือ helpdesk และ web shop application ที่เป็น consumer ทั้งสองนี้ต่างก็มีความคาดหวังว่า Customer microservice จะทำงานอย่างไร ในตัวอย่างนี้ คุณสร้างชุดเทสต์สำหรับ consumer แต่ละตัว ชุดหนึ่งแทนความคาดหวังของ helpdesk ที่มีต่อ Customer microservice และอีกชุดแทนความคาดหวังที่ web shop มี
เพราะ CDC เหล่านี้คือความคาดหวังว่า Customer microservice ควรทำงานอย่างไร เราต้องรันแค่ Customer microservice เองเท่านั้น หมายความว่าเรามีขอบเขตของเทสต์ที่มีประสิทธิผลเทียบเท่ากับ service test ของเรา มันจะมีลักษณะประสิทธิภาพคล้ายกัน และต้องการให้เรารันแค่ Customer microservice เองเท่านั้น โดย stub external dependency ใดๆ ออกไป
แนวปฏิบัติที่ดีในที่นี้คือให้คนจากทีม producer และ consumer ร่วมมือกันสร้างเทสต์ ดังนั้นอาจเป็นการที่คนจากทีม web shop และ helpdesk มาจับคู่ทำงาน (pair) กับคนจากทีม customer service จะว่าไปแล้ว consumer-driven contract เป็นเรื่องของการส่งเสริมเส้นทางการสื่อสารและความร่วมมือที่ชัดเจนพอๆ กัน ระหว่าง microservices กับทีมที่ใช้งานมัน ในที่ที่จำเป็น อันที่จริงอาจกล่าวได้ว่า การ implement CDC เป็นเพียงการทำให้การสื่อสารระหว่างทีมที่ต้องมีอยู่แล้วชัดเจนยิ่งขึ้นเท่านั้น ในความร่วมมือข้ามทีม CDC เป็นการเตือนใจอย่างชัดเจนถึงกฎของ Conway
CDC อยู่ในระดับเดียวกันใน test pyramid กับ service test แม้ว่าจะมีจุดเน้นที่แตกต่างกันมาก ดังที่แสดงใน Figure 9-10 เทสต์เหล่านี้เน้นไปที่วิธีที่ consumer จะใช้ service และตัว trigger เมื่อพวกมันพังนั้นแตกต่างอย่างมากเมื่อเทียบกับ service test ถ้า CDC ตัวใดตัวหนึ่งพังระหว่าง build ของ Customer service มันจะชัดเจนทันทีว่า consumer ตัวไหนจะได้รับผลกระทบ ณ จุดนี้ คุณสามารถแก้ไขปัญหา หรือเริ่มการพูดคุยเกี่ยวกับการนำ breaking change เข้ามาในแบบที่เราคุยกันไปใน “Handling Change Between Microservices” ดังนั้นด้วย CDC เราสามารถระบุ breaking change ได้ก่อนที่ซอฟต์แวร์ของเราจะเข้าสู่ production โดยไม่ต้องใช้ end-to-end test ที่อาจมีต้นทุนสูง
Figure 9-10. การผสาน consumer-driven test เข้ากับ test pyramid
Pact
Pact คือ เครื่องมือทดสอบแบบ consumer-driven ที่แต่เดิมพัฒนาขึ้นใช้ภายในของ realestate.com.au แต่ตอนนี้เป็น open source แล้ว แต่เดิมมีแค่สำหรับ Ruby และเน้นเฉพาะ HTTP protocol เท่านั้น ตอนนี้ Pact รองรับหลายภาษาและแพลตฟอร์ม เช่น JVM, JavaScript, Python และ .NET และยังสามารถใช้กับการปฏิสัมพันธ์แบบ messaging ได้ด้วย
ด้วย Pact คุณเริ่มต้นด้วยการกำหนดความคาดหวังของ producer โดยใช้ DSL ในภาษาใดภาษาหนึ่งที่รองรับ จากนั้นคุณ launch Pact server ในเครื่อง และรันความคาดหวังนี้กับมันเพื่อสร้างไฟล์ Pact specification ไฟล์ Pact เป็นแค่ JSON specification ที่เป็นทางการ คุณสามารถเขียนมันด้วยมือได้อย่างแน่นอน แต่การใช้ SDK เฉพาะภาษาจะง่ายกว่ามาก
คุณสมบัติที่ดีมากอย่างหนึ่งของโมเดลนี้คือ mock server ที่รันในเครื่องที่ใช้สร้างไฟล์ Pact ยังทำหน้าที่เป็น local stub สำหรับ downstream microservices ได้ด้วย ด้วยการกำหนดความคาดหวังของคุณในเครื่อง คุณก็กำลังกำหนดว่า local stub service นี้ควรตอบสนองอย่างไร สิ่งนี้สามารถแทนที่ความจำเป็นในการใช้เครื่องมืออย่าง mountebank ได้ (หรือโซลูชัน stubbing หรือ mocking ที่คุณสร้างขึ้นเอง)
ในฝั่ง producer คุณจะตรวจสอบว่า consumer specification นี้ถูกทำตามหรือไม่ โดยใช้ JSON Pact specification เพื่อขับเคลื่อนการเรียกเข้า microservice ของคุณ และตรวจสอบ response เพื่อให้สิ่งนี้ทำงานได้ producer ต้องเข้าถึงไฟล์ Pact ได้ อย่างที่เราคุยกันไปก่อนหน้านี้ใน “Mapping Source Code and Builds to Microservices” เราคาดว่า consumer และ producer จะอยู่ใน build ที่แตกต่างกัน นั่นหมายความว่าเราต้องการวิธีบางอย่างให้ไฟล์ JSON นี้ ซึ่งจะถูกสร้างขึ้นใน consumer build นั้น ถูกทำให้ producer เข้าถึงได้
คุณสามารถเก็บไฟล์ Pact ไว้ใน artifact repository ของ CI/CD tool ของคุณได้ หรือไม่ก็ใช้ Pact Broker ซึ่งช่วยให้คุณเก็บ Pact specification ได้หลายเวอร์ชัน สิ่งนี้จะช่วยให้คุณรัน consumer-driven contract test กับ consumer เวอร์ชันที่แตกต่างกันหลายเวอร์ชันได้ ถ้าคุณต้องการทดสอบกับ เช่น เวอร์ชันของ consumer ใน production และเวอร์ชันของ consumer ที่เพิ่ง build ล่าสุด
จริงๆ แล้ว Pact Broker มีความสามารถที่มีประโยชน์มากมาย นอกเหนือจากการเป็นสถานที่เก็บ contract คุณยังสามารถดูได้ว่า contract เหล่านั้นถูกตรวจสอบเมื่อไร นอกจากนี้ เพราะ Pact Broker รู้เกี่ยวกับความสัมพันธ์ระหว่าง consumer และ producer มันจึงสามารถแสดงให้คุณเห็นได้ว่า microservice ตัวไหนพึ่งพา microservice ตัวไหนบ้าง
Other options (ตัวเลือกอื่นๆ)
Pact ไม่ใช่ตัวเลือกเดียวสำหรับ tooling รอบๆ consumer-driven contract Spring Cloud Contract ก็เป็นตัวอย่างหนึ่งเช่นกัน อย่างไรก็ตาม ควรสังเกตว่า ต่างจาก Pact ที่ถูกออกแบบมาตั้งแต่ต้นให้รองรับ technology stack ที่แตกต่างกัน Spring Cloud Contract จะมีประโยชน์จริงๆ ก็แค่ใน ระบบนิเวศ JVM ล้วนๆ เท่านั้น
It’s about conversations (มันคือเรื่องของบทสนทนา)
ใน Agile story มักถูกเรียกว่าเป็นตัวแทนสำหรับบทสนทนา CDC ก็เป็นแบบนั้นเหมือนกัน มันกลายเป็นการเขียนข้อสรุปของชุดการพูดคุยเกี่ยวกับว่า API ของ service ควรมีหน้าตาอย่างไร และเมื่อมันพัง มันก็กลายเป็นจุดเริ่มต้นให้เกิดบทสนทนาเกี่ยวกับว่า API นั้นควรพัฒนาต่อไปอย่างไร
สิ่งสำคัญที่ต้องเข้าใจคือ CDC ต้องการการสื่อสารที่ดีและความไว้วางใจระหว่าง consumer และ producing service ถ้าทั้งสองฝ่ายอยู่ในทีมเดียวกัน (หรือเป็นคนคนเดียวกัน!) เรื่องนี้ก็ไม่น่าจะยาก อย่างไรก็ตาม ถ้าคุณกำลังใช้ service ที่จัดหาโดย third party คุณอาจไม่มีความถี่ของการสื่อสาร หรือความไว้วางใจมากพอที่จะทำให้ CDC ทำงานได้ ในสถานการณ์แบบนี้ คุณอาจต้องพึ่งพา integration test ขอบเขตใหญ่ที่จำกัดอยู่แค่รอบๆ component ที่ ไม่น่าไว้วางใจ เท่านั้น ในทางกลับกัน ถ้าคุณกำลังสร้าง API สำหรับ consumer ที่มีศักยภาพนับพันราย เช่น web service API ที่เปิดให้สาธารณะใช้ คุณอาจต้องสวมบทบาทเป็น consumer ด้วยตัวเอง (หรืออาจทำงานร่วมกับ consumer ส่วนหนึ่งของคุณ) ในการกำหนดเทสต์เหล่านี้ การทำให้ consumer ภายนอกจำนวนมหาศาลพังนั้นเป็นความคิดที่แย่มาก ดังนั้นความสำคัญของ CDC จึงยิ่งเพิ่มขึ้นไปอีก!
The Final Word (คำสุดท้าย)
อย่างที่ระบุไว้อย่างละเอียดก่อนหน้านี้ในบทนี้ end-to-end test มีข้อเสียจำนวนมากที่เพิ่มขึ้นอย่างมีนัยสำคัญเมื่อคุณเพิ่มชิ้นส่วนที่เคลื่อนไหวเข้าไปในสิ่งที่ถูกทดสอบมากขึ้น จากการพูดคุยกับคนที่ implement microservices ในสเกลใหญ่มาระยะหนึ่งแล้ว ผมได้เรียนรู้ว่าส่วนใหญ่แล้วเมื่อเวลาผ่านไปพวกเขาจะกำจัดความจำเป็นของ end-to-end test ออกไปทั้งหมด และหันไปใช้กลไกอื่นเพื่อตรวจสอบคุณภาพของซอฟต์แวร์ของพวกเขาแทน ตัวอย่างเช่น การใช้ schema ที่ชัดเจนและ CDC, การทดสอบใน production หรืออาจเป็นเทคนิค progressive delivery บางอย่างที่เราคุยกันไปแล้ว เช่น canary release
คุณสามารถมองการรัน end-to-end test ก่อนการ deploy เข้าสู่ production ว่าเป็นเหมือนล้อฝึกหัด (training wheels) ในขณะที่คุณกำลังเรียนรู้ว่า CDC ทำงานอย่างไร และปรับปรุงเทคนิคการ monitoring และการ deploy ใน production ของคุณ end-to-end test เหล่านี้อาจเป็น safety net ที่มีประโยชน์ ซึ่งคุณกำลังแลก cycle time กับความเสี่ยงที่ลดลง แต่เมื่อคุณปรับปรุงพื้นที่อื่นๆ เหล่านั้น และเมื่อต้นทุนสัมพัทธ์ของการสร้าง end-to-end test เพิ่มขึ้น คุณก็สามารถเริ่มลดการพึ่งพา end-to-end test ลงได้ จนถึงจุดที่ไม่จำเป็นต้องใช้มันอีกต่อไป การทิ้ง end-to-end test โดยไม่เข้าใจอย่างถ่องแท้ว่าคุณสูญเสียอะไรไปนั้นอาจเป็นความคิดที่แย่
แน่นอนว่าคุณจะเข้าใจ risk profile ขององค์กรของคุณเองได้ดีกว่าผม แต่ผมขอท้าทายให้คุณคิดให้หนักและนานเกี่ยวกับว่าคุณต้องการทำ end-to-end testing มากแค่ไหนกันแน่