Test Scope (ขอบเขตของการทดสอบ)

ใน หนังสือของเขา Succeeding with Agile , 2 Mike Cohn อธิบายโมเดลที่เรียกว่า test pyramid เพื่อช่วยอธิบายว่าเราต้องการ automated test ประเภทไหนบ้าง pyramid นี้ช่วยให้เราคิดไม่เพียงแค่เรื่องขอบเขตของเทสต์ แต่ยังรวมถึงสัดส่วนของเทสต์ประเภทต่างๆ ที่เราควรตั้งเป้าไว้ด้วย โมเดลดั้งเดิมของ Cohn แบ่ง automated test ออกเป็น unit test, service test และ UI test ดังที่แสดงใน Figure 9-2

bms2 0902

Figure 9-2. test pyramid ของ Mike Cohn โดย Mike Cohn, Succeeding with Agile: Software Development Using Scrum , 1st ed., © 2010

สิ่งสำคัญที่ต้องจับใจความเมื่ออ่าน pyramid นี้คือ เมื่อเราขึ้นไปด้านบนของ pyramid ขอบเขตของเทสต์จะเพิ่มขึ้น เช่นเดียวกับความมั่นใจของเราว่าฟังก์ชันที่ถูกทดสอบนั้นทำงานได้ ในทางกลับกัน เวลาของ feedback cycle จะเพิ่มขึ้นเพราะเทสต์ใช้เวลารันนานขึ้น และเมื่อเทสต์ล้มเหลว ก็จะยากขึ้นที่จะระบุว่าฟังก์ชันไหนพัง เมื่อเราลงมาด้านล่างของ pyramid โดยทั่วไปเทสต์จะเร็วขึ้นมาก ทำให้เราได้ feedback cycle ที่เร็วขึ้นมาก เราพบฟังก์ชันที่พังได้เร็วขึ้น continuous integration build ของเราเร็วขึ้น และเรามีโอกาสน้อยลงที่จะย้ายไปทำงานใหม่ก่อนที่จะรู้ว่าเราทำอะไรพังไป เมื่อเทสต์ขอบเขตเล็กเหล่านั้นล้มเหลว เรามักจะรู้ด้วยว่าอะไรพัง บ่อยครั้งถึงขั้นรู้ถึงบรรทัดโค้ดที่แน่นอน เพราะแต่ละเทสต์ถูก แยก ได้ดีกว่า ทำให้เราเข้าใจและแก้ไขปัญหาได้ง่ายขึ้น ในทางกลับกัน เราจะไม่ได้รับความมั่นใจมากนักว่าระบบทั้งหมดของเราทำงานได้ ถ้าเราทดสอบแค่โค้ดบรรทัดเดียว!

ปัญหาของโมเดลนี้คือคำศัพท์เหล่านี้ทั้งหมดมีความหมายต่างกันไปสำหรับแต่ละคน คำว่า Service ถูกใช้แบบทับซ้อนความหมายมากเป็นพิเศษ และมีคำนิยามของ unit test อยู่มากมาย เทสต์หนึ่งจะเป็น unit test หรือไม่ถ้าผมทดสอบแค่โค้ดบรรทัดเดียว ผมว่า ใช่ มันยังเป็น unit test อยู่หรือเปล่าถ้าผมทดสอบหลายฟังก์ชันหรือหลายคลาส ผมว่า ไม่ใช่ แต่หลายคนคงไม่เห็นด้วย! ผมมักจะยึดติดกับชื่อ unit และ service แม้ว่ามันจะกำกวมก็ตาม แต่ผมชอบเรียก UI test ว่า end-to-end test มากกว่า ซึ่งผมจะใช้คำนี้ต่อไปนับจากนี้

แทบทุกทีมที่ผมเคยทำงานด้วยใช้ชื่อเรียกเทสต์ที่แตกต่างไปจากที่ Cohn ใช้ใน pyramid ไม่ว่าคุณจะเรียกมันว่าอะไร ประเด็นสำคัญคือคุณต้องการ functional automated test ที่มีขอบเขตแตกต่างกันสำหรับจุดประสงค์ที่แตกต่างกัน

เมื่อพิจารณาถึงความสับสนนี้ ก็คุ้มค่าที่จะมาดูว่าแต่ละ layer เหล่านี้หมายความว่าอย่างไร

ลองมาดูตัวอย่างที่ใช้งานจริงกัน ใน Figure 9-3 เรามี helpdesk application และเว็บไซต์หลักของเรา ทั้งสองระบบต่างก็ปฏิสัมพันธ์กับ Customer microservice ของเรา เพื่อดึงข้อมูล ตรวจสอบ และแก้ไขรายละเอียดลูกค้า Customer microservice ของเราก็คุยกับ Loyalty microservice ของเรา ที่ซึ่งลูกค้าของเราสะสมแต้มจากการซื้อซีดี Justin Bieber บางที นี่เป็นเพียงเสี้ยวหนึ่งของระบบ MusicCorp โดยรวมของเราอย่างเห็นได้ชัด แต่ก็เป็นชิ้นส่วนที่ดีพอสำหรับเราที่จะเจาะลึกเข้าไปในสถานการณ์ต่างๆ ที่เราอาจต้องการทดสอบ

Parts of MusicCorp's system that will undergo testing

Figure 9-3. ส่วนหนึ่งของร้านขายเพลงของเราที่อยู่ระหว่างการทดสอบ

Unit Tests (Unit Test)

Unit test มักจะทดสอบฟังก์ชันเดียวหรือการเรียก method เดียว เทสต์ที่เกิดขึ้นจากผลพลอยได้ของ test-driven design (TDD) จะจัดอยู่ในหมวดนี้ เช่นเดียวกับเทสต์ที่เกิดจากเทคนิคอย่าง property-based testing เราไม่ได้ launch microservices ที่นี่ และจำกัดการใช้ไฟล์ภายนอกหรือการเชื่อมต่อเครือข่าย โดยทั่วไปแล้วคุณต้องการเทสต์ประเภทนี้จำนวนมาก ถ้าทำถูกวิธี มันจะเร็วมากๆ และบนฮาร์ดแวร์สมัยใหม่คุณสามารถคาดหวังที่จะรันเทสต์แบบนี้หลายพันตัวได้ในเวลาไม่ถึงหนึ่งนาที ผมรู้จักหลายคนที่ตั้งให้เทสต์เหล่านี้รันอัตโนมัติเมื่อไฟล์เปลี่ยนแปลงในเครื่อง โดยเฉพาะกับภาษาแบบ interpreted วิธีนี้ช่วยให้ได้ feedback cycle ที่รวดเร็วมาก

unit test ช่วยพวกเรานักพัฒนา ดังนั้นจึงเป็น technology facing ไม่ใช่ business facing ตามศัพท์ของ Marick มันยังเป็นจุดที่เราหวังว่าจะจับบั๊กส่วนใหญ่ของเราได้ ดังนั้นในตัวอย่างของเรา เมื่อเราคิดถึง Customer microservice unit test จะครอบคลุมส่วนเล็กๆ ของโค้ดแบบแยกส่วน ดังที่แสดงใน Figure 9-4

Scope of unit tests on our example system

Figure 9-4. ขอบเขตของ unit test บนระบบตัวอย่างของเรา

เป้าหมายหลักของเทสต์เหล่านี้คือให้ feedback ที่รวดเร็วมากเกี่ยวกับว่าฟังก์ชันของเราดีหรือไม่ unit test ยังสำคัญต่อการรองรับการ refactor โค้ด ทำให้เราสามารถปรับโครงสร้างโค้ดของเราไปเรื่อยๆ ได้อย่างมั่นใจว่าเทสต์ขอบเขตเล็กของเราจะจับความผิดพลาดของเราได้ ถ้าเราทำพลาด

Service Tests (Service Test)

Service test ถูกออกแบบมาให้ข้าม user interface และทดสอบ microservices ของเราโดยตรง ใน monolithic application เราอาจแค่ทดสอบกลุ่มของคลาสที่ให้บริการ ( service ) แก่ UI สำหรับระบบที่ประกอบด้วย microservices หลายตัว service test จะทดสอบความสามารถของ microservice แต่ละตัว

ด้วยการรันเทสต์กับ microservice เดียวแบบนี้ เราจะได้รับความมั่นใจเพิ่มขึ้นว่า service จะทำงานตามที่เราคาดหวัง แต่เรายังคงรักษาขอบเขตของเทสต์ให้ค่อนข้างแยกส่วนอยู่ สาเหตุของความล้มเหลวของเทสต์ควรจำกัดอยู่แค่ microservice ที่กำลังถูกทดสอบเท่านั้น เพื่อให้ได้การแยกส่วนนี้ เราต้อง stub ผู้ร่วมงานภายนอกทั้งหมดออกไป เพื่อให้เฉพาะ microservice เองเท่านั้นที่อยู่ในขอบเขต ดังที่ Figure 9-5 แสดง

เทสต์เหล่านี้บางส่วนอาจเร็วพอๆ กับ unit test ขอบเขตเล็กของเรา แต่ถ้าคุณตัดสินใจทดสอบกับฐานข้อมูลจริง หรือส่งผ่านเครือข่ายไปยัง stubbed downstream collaborator เวลาในการทดสอบก็อาจเพิ่มขึ้นได้ เทสต์เหล่านี้ยังครอบคลุมขอบเขตมากกว่า unit test ธรรมดา ดังนั้นเมื่อมันล้มเหลว ก็จะตรวจจับได้ยากกว่าว่าอะไรพัง เมื่อเทียบกับ unit test อย่างไรก็ตาม มันมีชิ้นส่วนที่เคลื่อนไหวน้อยกว่ามาก จึงเปราะบางน้อยกว่าเทสต์ที่มีขอบเขตใหญ่กว่า

Scope of service tests on our example system

Figure 9-5. ขอบเขตของ service test บนระบบตัวอย่างของเรา

End-to-End Tests (End-to-End Test)

End-to-end test คือเทสต์ที่รันกับระบบทั้งหมดของคุณ บ่อยครั้งมันจะขับเคลื่อน GUI ผ่าน browser แต่ก็สามารถเลียนแบบการปฏิสัมพันธ์ของผู้ใช้แบบอื่นๆ ได้ง่ายๆ เช่น การอัปโหลดไฟล์

เทสต์เหล่านี้ครอบคลุมโค้ด production จำนวนมาก ดังที่เราเห็นใน Figure 9-6 ดังนั้นเมื่อมันผ่าน คุณจะรู้สึกดี คุณมีความมั่นใจในระดับสูงว่าโค้ดที่ถูกทดสอบจะทำงานได้ใน production แต่ขอบเขตที่เพิ่มขึ้นนี้ก็มาพร้อมกับข้อเสีย และอย่างที่เราจะเห็นในไม่ช้า end-to-end test อาจทำได้ยากมากในบริบทของ microservices

Scope of end-to-end tests on our example system

Figure 9-6. ขอบเขตของ end-to-end test บนระบบตัวอย่างของเรา
What About Integration Tests? (แล้ว Integration Test ล่ะ)

คุณ อาจสังเกตเห็นว่าผมยังไม่ได้อธิบาย integration test อย่างชัดเจน นี่เป็นความตั้งใจ ผมพบว่าคำนี้มักถูกใช้โดยคนต่างกันเพื่ออธิบายเทสต์ประเภทต่างกัน สำหรับบางคน integration test อาจแค่ดูปฏิสัมพันธ์ระหว่าง service สองตัว หรืออาจเป็นการเชื่อมต่อระหว่างโค้ดกับฐานข้อมูล สำหรับคนอื่น integration test กลับกลายเป็นสิ่งเดียวกันกับ end-to-end test แบบเต็มรูปแบบ ผมพยายามใช้คำที่ชัดเจนกว่าในบทนี้ ผมหวังว่ามันจะทำให้คุณจับคู่สิ่งที่คุณเรียกว่า "integration test" เข้ากับคำศัพท์ที่ผมใช้ที่นี่ได้ง่าย

Trade-Offs (การแลกเปลี่ยน)

สิ่งที่เรามุ่งหวังกับเทสต์ประเภทต่างๆ ที่ pyramid ครอบคลุมคือความสมดุลที่สมเหตุสมผล เราต้องการ feedback ที่รวดเร็ว และเราต้องการความมั่นใจว่าระบบของเราทำงานได้

unit test มีขอบเขตเล็ก ดังนั้นเมื่อมันล้มเหลวเราจะหาปัญหาได้เร็ว มันยังเขียนได้เร็วและ รัน ได้เร็วมากด้วย เมื่อเทสต์ของเรามีขอบเขตใหญ่ขึ้น เราจะได้ความมั่นใจในระบบของเรามากขึ้น แต่ feedback ของเราจะเริ่มแย่ลงเพราะเทสต์ใช้เวลารันนานขึ้น มันยังมีต้นทุนในการเขียนและดูแลรักษาสูงขึ้นด้วย

คุณจะต้องหาสมดุลบ่อยๆ ว่าต้องการเทสต์แต่ละประเภทเท่าไรเพื่อหาจุดที่ลงตัว พบว่า test suite ของคุณใช้เวลารันนานเกินไปหรือเปล่า เมื่อเทสต์ขอบเขตกว้างอย่าง service หรือ end-to-end test ล้มเหลว ให้เขียน unit test ขอบเขตเล็กเพื่อจับความผิดพลาดนั้นได้เร็วขึ้น มองหาโอกาสแทนที่เทสต์ขอบเขตใหญ่ (และช้ากว่า) บางส่วนด้วย unit test ขอบเขตเล็กที่เร็วกว่า ในทางกลับกัน เมื่อบั๊กหลุดรอดไปถึง production นั่นอาจเป็นสัญญาณว่าคุณขาดเทสต์บางตัวไป

ดังนั้นถ้าเทสต์เหล่านี้ทั้งหมดมีการแลกเปลี่ยน คุณต้องการเทสต์แต่ละประเภทเท่าไรกันแน่ กฎง่ายๆ ที่ดีคือคุณน่าจะต้องการเทสต์เพิ่มขึ้นเป็นสิบเท่าเมื่อคุณลงมาด้านล่างของ pyramid สิ่งสำคัญคือต้องรู้ว่าคุณมี automated test หลายประเภทจริงๆ และสำคัญที่จะเข้าใจว่าสมดุลปัจจุบันของคุณสร้างปัญหาให้คุณหรือไม่!

ตัวอย่างเช่น ผมเคยทำงานกับระบบ monolithic ระบบหนึ่งที่เรามี unit test 4,000 ตัว service test 1,000 ตัว และ end-to-end test 60 ตัว เราตัดสินใจว่าจากมุมมองของ feedback แล้ว เรามี service test และ end-to-end test มากเกินไป (โดยเฉพาะอย่างหลังที่ส่งผลกระทบต่อ feedback loop มากที่สุด) ดังนั้นเราจึงทำงานหนักเพื่อแทนที่ coverage ของเทสต์ด้วยเทสต์ขอบเขตเล็กกว่า

antipattern ที่พบได้ทั่วไปคือสิ่งที่มักถูกเรียกว่า test snow cone หรือ pyramid กลับหัว ในกรณีนี้ แทบไม่มีเทสต์ขอบเขตเล็กเลย โดย coverage ทั้งหมดอยู่ในเทสต์ขอบเขตใหญ่ โปรเจกต์แบบนี้มักจะรันเทสต์ช้าเป็นน้ำแข็งไหลและมี feedback cycle ที่ยาวนานมาก ถ้าเทสต์เหล่านี้ถูกรันเป็นส่วนหนึ่งของ continuous integration คุณจะไม่ได้ build บ่อยนัก และธรรมชาติของเวลา build ก็หมายความว่า build อาจพังอยู่นานเป็นระยะเวลานานเมื่อมีอะไรพังขึ้นมาจริงๆ