ควรเริ่มเขียนเทสต์จาก Unit, Mock หรือ Integration Test ก่อนดี
การเริ่มเขียนเทสต์ไม่ควรยึดตามชื่อประเภทของเทสต์เพียงอย่างเดียว แต่ควรเริ่มจากจุดที่ช่วยลดความเสี่ยงทางธุรกิจได้มากที่สุดก่อน สำหรับหลายระบบ โดยเฉพาะงาน backend การเริ่มจาก Integration Test ในเส้นทางสำคัญมักให้ผลลัพธ์ที่

การเริ่มเขียนเทสต์ในโปรเจกต์มักมาพร้อมคำถามสำคัญว่า ควรเริ่มจาก Unit Test, Mock-based Test หรือ Integration Test ก่อนดี หลายทีมพยายามหาคำตอบแบบตายตัว แต่ในความเป็นจริง สิ่งที่เหมาะสมที่สุดไม่ได้ขึ้นอยู่กับชื่อของเทสต์เสมอไป แต่อยู่ที่ว่าเทสต์แบบไหนช่วย ลดความเสี่ยง ให้ระบบและธุรกิจได้มากที่สุด
ทำไมหลายทีมถึงสับสนเรื่องประเภทของเทสต์
สาเหตุหนึ่งที่ทำให้ประเด็นนี้ถกเถียงกันไม่จบ คือแต่ละทีมตีความคำว่า Unit Test ไม่เหมือนกัน บางทีมหมายถึงการทดสอบฟังก์ชันล้วนๆ โดยไม่แตะ dependency ใดเลย บางทีม mock ทุกอย่างรอบด้านจนเหลือเพียง logic ตรงกลาง ขณะที่บางทีมเรียกการทดสอบระดับ service ที่มีฐานข้อมูลปลอมว่าเป็น unit test เช่นกัน
เมื่อคำจำกัดความไม่ตรงกัน การถกเถียงว่าอะไรควรมาก่อนจึงอาจไม่ช่วยให้ตัดสินใจได้ดีขึ้น สิ่งที่ควรพิจารณาจริงๆ คือ คุณค่าที่เทสต์นั้นมอบให้ มากกว่า
Unit Test: เร็ว ชัด และเหมาะกับ logic ที่ซับซ้อน
Unit Test แบบที่ไม่พึ่งพา dependency ภายนอก มีข้อดีชัดเจนคือ รันเร็ว เขียนง่าย และให้ feedback ไว เหมาะมากกับการล็อกพฤติกรรมของ business logic ขนาดเล็กให้แน่นหนา
ตัวอย่างงานที่เหมาะกับ Unit Test เช่น
- ฟังก์ชันคำนวณส่วนลด
- การแปลงรูปแบบข้อมูล
- การตรวจสอบ validation
- เงื่อนไขทางธุรกิจที่มีหลายกรณี
เช่น หากมีฟังก์ชันคำนวณส่วนลดจากระดับสมาชิกและยอดซื้อ การเขียน Unit Test จะช่วยจับบั๊กได้เร็วมาก และช่วยให้ทีมมั่นใจได้ว่า logic หลักยังทำงานถูกต้องเสมอ
อย่างไรก็ตาม Unit Test มีข้อจำกัดสำคัญ เพราะมันไม่สามารถบอกได้ว่า
- ระบบเชื่อมต่อฐานข้อมูลถูกต้องหรือไม่
- การ serialize หรือ deserialize ข้อมูลมีปัญหาหรือเปล่า
- การเรียก API จริงมี schema ตรงกันหรือไม่
ดังนั้น Unit Test จึงเหมาะสำหรับการป้องกันความผิดพลาดในระดับ logic แต่ไม่เพียงพอสำหรับการยืนยันว่าระบบทั้งก้อนทำงานร่วมกันได้จริง
Mock-based Test: ดีต่อการตรวจสอบพฤติกรรม แต่ต้องใช้ให้พอดี
Mock-based Test มีคุณค่าในอีกมุมหนึ่ง คือช่วยให้เราตรวจสอบ พฤติกรรมการเรียกใช้งานระหว่างองค์ประกอบต่างๆ ได้อย่างละเอียด
ตัวอย่างเช่น
- service ควรเรียก repository 1 ครั้ง
- หาก payment ล้มเหลว ต้องไม่ส่งอีเมลยืนยัน
- เมื่อเกิด error จาก dependency ภายนอก ระบบควรตอบสนองอย่างไร
ข้อดีของแนวทางนี้คือช่วยให้เข้าใจ flow ภายในระบบได้ดี และมักส่งเสริมให้ทีมออกแบบโค้ดแบบแยก dependency อย่างชัดเจนขึ้น
แต่ถ้าใช้ mock มากเกินไป เทสต์จะเริ่มผูกติดกับรายละเอียดภายในของโค้ดมากเกินจำเป็น ผลคือเพียงแค่ refactor โครงสร้างเล็กน้อย เทสต์ก็อาจพัง ทั้งที่พฤติกรรมจริงของระบบยังถูกต้องอยู่ ปัญหานี้ทำให้หลายทีมมีเทสต์จำนวนมาก แต่กลับไม่กล้าปรับปรุงโค้ด
ดังนั้น Mock-based Test ควรถูกใช้เมื่อจำเป็นจริงๆ โดยเฉพาะในกรณีที่ dependency นั้น
- ช้า
- ไม่เสถียร
- มีค่าใช้จ่าย
- หรือควบคุมไม่ได้
เช่น payment gateway, email service, SMS provider หรือ third-party API ต่างๆ
Integration Test: ช้ากว่า แต่จับปัญหาจริงได้มาก
Integration Test มักเป็นประเภทของเทสต์ที่จับปัญหาในโลกจริงได้มากที่สุด โดยเฉพาะในระบบ backend ที่พึ่งพาหลายองค์ประกอบร่วมกัน เช่น database, queue, cache หรือบริการภายนอก
ปัญหาที่ Integration Test มักช่วยจับได้ เช่น
- ORM map field ผิด
- query ดึงข้อมูลไม่ตรงตามคาด
- transaction ทำงานไม่ครบ
- config ของ environment ต่างกันจนระบบพัง
- การเชื่อมต่อระหว่าง service มีข้อผิดพลาด
แม้ Integration Test จะรันช้ากว่า Unit Test แต่หลายครั้งมันกลับป้องกันปัญหาที่กระทบผู้ใช้จริงได้ดีกว่า โดยเฉพาะในระบบที่ต่อให้ logic ภายในถูกหมด แต่ถ้าเชื่อมกับระบบจริงแล้วพัง ผู้ใช้ก็ยังใช้งานไม่ได้อยู่ดี
ถ้าจะเริ่ม ควรเริ่มจากอะไร
สำหรับหลายโปรเจกต์ คำตอบที่ใช้งานได้จริงคือ เริ่มจาก Integration Test ใน use case ที่สำคัญที่สุดก่อน ไม่จำเป็นต้องไล่จากสิ่งที่เล็กที่สุดเสมอไป
ตัวอย่างเส้นทางสำคัญที่ควรถูกป้องกันก่อน ได้แก่
- สมัครสมาชิก
- ล็อกอิน
- สร้างออเดอร์
- ตัดสต็อก
- รับชำระเงิน
เหตุผลคือ flow เหล่านี้มักเกี่ยวข้องกับรายได้ ข้อมูลสำคัญ และประสบการณ์ใช้งานโดยตรง หากเกิดปัญหาในจุดเหล่านี้ ความเสียหายทางธุรกิจมักสูงกว่าบั๊กเล็กๆ ในฟังก์ชันย่อย
แนวคิดที่ใช้ได้ดีมากคือ เขียนเทสต์ตามลำดับความเสี่ยง ไม่ใช่ตามลำดับศัพท์เทคนิค ให้เริ่มจากสิ่งที่ถ้าพังแล้วจะสร้างผลกระทบมากที่สุดก่อน เช่น ทำให้เสียรายได้ ข้อมูลผิดพลาด หรือทำให้ผู้ใช้ไปต่อไม่ได้
หลังจากนั้นค่อยเติม Unit Test ในจุดที่คุ้มค่า
เมื่อมี Integration Test ครอบคลุมเส้นทางหลักของระบบแล้ว ขั้นต่อไปคือเติม Unit Test ในจุดที่ logic ซับซ้อน โดยเฉพาะโค้ดที่มีเงื่อนไขจำนวนมากหรือแตก branch เยอะ
ไม่ใช่ Unit Test ทุกตัวจะมีคุณค่าเท่ากัน เทสต์ที่เพียงตรวจสอบ output ง่ายๆ ของ getter/setter มักไม่ได้เพิ่มความมั่นใจให้ระบบมากนัก แต่เทสต์ที่ครอบคลุมเรื่องอย่าง
- logic ส่วนลด
- ภาษี
- สิทธิ์การเข้าถึง
- การจัดลำดับ workflow
มักมีมูลค่าสูงกว่ามาก เพราะช่วยป้องกันบั๊กที่กระทบต่อธุรกิจโดยตรง
นั่นจึงเป็นเหตุผลว่าทำไมทีมไม่ควรวัดคุณภาพจาก จำนวนเทสต์ เพียงอย่างเดียว เพราะ 100 เทสต์ที่จับเรื่องไม่สำคัญ อาจมีค่าน้อยกว่า 10 เทสต์ที่ป้องกันปัญหาระดับธุรกิจได้จริง
สูตรเริ่มต้นที่ใช้ได้จริงสำหรับหลายทีม
หากทีมเพิ่งเริ่มสร้างวัฒนธรรมการเขียนเทสต์ แนวทางที่เรียบง่ายและใช้ได้ผลกับหลายระบบคือ
- มี Integration Test สำหรับ critical path
- มี Unit Test สำหรับ logic ที่ซับซ้อน
- มี Mock เท่าที่จำเป็นสำหรับ dependency ภายนอก
สัดส่วนของแต่ละแบบไม่จำเป็นต้องเท่ากันทุกระบบ เช่น
- ระบบที่มีการคำนวณเยอะ อาจเน้น Unit Test มากกว่า
- ระบบที่เชื่อมหลาย service และหลายฐานข้อมูล อาจต้องเน้น Integration Test มากกว่า
ที่สำคัญคือ อย่าเริ่มจากการพยายามเขียนเทสต์ให้ครบทุกฟังก์ชันในครั้งเดียว แต่ให้เริ่มจาก 3 ถึง 5 flow ที่พังไม่ได้ก่อน วิธีนี้จะทำให้ทีมเห็นคุณค่าได้เร็ว และได้ผลลัพธ์ที่ชัดเจนกว่ามาก
สรุป
เทสต์ที่ดีที่สุดไม่ใช่เทสต์ที่ดูสะอาดที่สุด หรือมีจำนวนมากที่สุด แต่คือเทสต์ที่ช่วยเตือนทีมได้ก่อนที่ปัญหาจะหลุดไปถึง production
หากต้องเลือกว่าเริ่มจากอะไร คำตอบสำหรับหลายทีมคือ ให้เริ่มจากสิ่งที่ ลดความเสี่ยงได้มากที่สุด ก่อน โดยมากมักเป็น Integration Test ในเส้นทางหลักของธุรกิจ จากนั้นจึงเสริม Unit Test ในจุดที่ logic ซับซ้อน และใช้ Mock เฉพาะเมื่อจำเป็นกับ dependency ภายนอก
สุดท้ายแล้ว การเขียนเทสต์ที่ดีไม่ใช่แค่เรื่องของเทคนิค แต่คือการมองให้ออกว่า อะไรคือสิ่งที่ควรถูกป้องกันก่อน และลงมือป้องกันสิ่งนั้นอย่างมีลำดับความสำคัญ