TOP

จะทำระบบให้รองรับ Automated Test ได้อย่างไร (Testable)

ตอนที่หัดเขียน Automated  Test ผมเคยเจอปัญหาว่า โค้ดเก่าที่เขียนมา มันไม่รองรับการเขียน Unit Test, Integration Test เอาซะเลย แกะยากเหมือนกัน จนแล้วจนรอดก็ลองทำใหม่ แล้วฝึกกับโจทย์ไปเรื่อยๆ จนพอตกผลึกได้บ้าง ว่าถ้าจะทำระบบให้รองรับการ Test (Testable) จะต้องทำอย่างไร

วันก่อน มีน้องในทีมถามว่า “ผมจะเขียน Integration Test อย่างไรดี” ก็เลยได้ย้อนไปทบทวนประสบการณ์ หาข้อมูล และถามจากผู้รู้เพิ่มเติม จึงกลับไปเล่าให้น้องฟัง ก็คิดว่ามีประโยชน์ จึงอยากแชร์ไว้มาแลกเปลี่ยนความรู้กัน

ต้องเข้าใจโครงสร้างโปรเจ็คเสียก่อน

ทุกวันนี้เราเข้าใจโครงสร้างของโปรเจ็คเราดีแล้วหรือยัง หรือถ้าเข้าใจดีแล้ว เราได้แยกส่วนชัดเจนไหมว่าส่วนไหนเป็นโค้ดของระบบ (Production Code) และส่วนไหนเป็นโค้ดของการทดสอบ (Test Code)

ขอยกตัวอย่าง ถ้าผมจะเขียนโปรเจ็คตัวหนึ่งคือ ระบบ API (My API) โดยมีการเชื่อมต่อไปยังระบบ API ข้างนอก (External API) จากนั้นผมมีโปรเจ็ค Test แยกออกมาอีกตัวหนึ่ง รูปร่างหน้าตามันก็จะเป็นเช่นนี้

(รูปโครงสร้างโปรเจ็ค)

ลองเจาะลึกเข้าไปในการทำงานของโปรเจ็ค ในที่นี้ผมเขียนเป็น MVC ซึ่งถ้าผมเป็น User ผมจะเรียกใช้ API ตัวหนึ่ง ผ่าน Controller จากนั้นไปเรียก Services เพื่อทำการติดต่อไปยัง External API และรับค่ามาแสดงให้ User ได้เห็น

(รูปการทดงานภายในโปรเจ็ค)

แยกโปรเจ็คของชุดทดสอบ (Test) ให้ชัดเจน

จากรูปการทดงานภายในโปรเจ็ค, ผมสมมติว่า Controller ผมมี Business Logic อะไร เป็นเพียงการรับ Request และส่งผ่าน Data ไปให้ Services ทำงานต่อ คราวนี้ถ้าผมจะเขียนทดสอบ ผมควรต้องเขียนทดสอบที่ Services ซึ่งในที่นี้ผมจะทดสอบการทำงานในชั้นที่เล็กที่สุด นั่นคือ Unit Test

แต่ตามทฤษฎีแล้ว คุณสมบัติที่ดีของ Unit Test จะต้องเป็นไปตามหลักการของ FIRST คือ

  • Fast: ทำงานได้เร็ว
  • Isolated: เป็นอิสระจากกัน
  • Repeatable: ทำซ้ำได้ ไม่ขึ้นกับระบบอื่นๆ เช่น API ภายนอก, Database, File System
  • Self-Verify: แสดงให้เห็นผลการทดสอบ ผ่าน หรือไม่ผ่านได้อย่างชัดเจน
  • Timely: เขียนให้ถูกเวลา คือ ควรมี Test ก่อน เพื่อให้ได้เข้าใจปัญหา และจึงเขียนโค้ดเข้ามาแก้ปัญหานั้น

หมายความว่า ถ้าผมทำ Unit Test ผมจะไม่สามารถทำการทดสอบได้เลย ถ้า External API พัง หรือ Internet ล่ม เป็นต้น

ดังนั้น ผมต้องเขียนโค้ดทดสอบเพิ่มเข้ามาอีกชุด  ที่เรียกว่า Integration Test

(รูปการทำงานภายในโปรเจ็คที่มี Unit Test และ Integration Test)

ถ้าดูจากเส้นการทำงาน ผมจะเขียน Unit Test เพื่อทดสอบแค่กระบวนการภายใน Services ของผมเอง (เส้นสีน้ำเงิน) ส่วน Integration Test ผมจะทดสอบกระบวนการที่ Services ผมไปเรียกใช้ External API (เส้นสีเขียว)

วิเคราะห์การทำงานของโค้ด และแยกแยะให้ถูก

มาถึงตรงนี้พอเห็นภาพโครงสร้างกว้างๆ แล้วใช่ไหมครับว่า เราจะแยกการทดสอบอย่างไร คราวนี้เราจะลองลงไปให้ลึกเข้าไปอีก ในระดับของโค้ด

ผมสมมติว่า Services ที่ผมทำขึ้นมี Method ชื่อว่า IsAdd() โดยรับหมายเลขสองตัวมาบวกกัน จากนั้นส่งไปให้ External API ตรวจสอบว่าคำนวณถูกต้องไหม ถ้าถูก ให้คืนค่า True กลับมา

(รูปของ Method IsAdd() ที่ยังไม่รู้กระบวนการภายใน)

จากคำอธิบายที่ผ่านมา มันยังเป็นเพียง Black Box ที่เราแทบไม่เห็นการทำงานของมันเลย รู้แค่ว่าส่งอะไรเข้าไป และคืนค่าอะไรกลับมา ดังนั้น เราจะต้องวิเคราะห์การทำงานของโค้ดเราให้กระจ่างเสียก่อน

(รูปของ Method IsAdd() ที่เราวิเคราะห์ว่ามีการทำงานใดเกิดขึ้นบ้าง)

หลังจากวิเคราะห์ออกมา เราพบว่ามีการทำงานสองส่วนคือ

  • Process A: เป็นการคำนวณเลขสองตัวที่รับเข้ามาว่าบวกกันได้เท่าไร
  • Process B: นำค่าที่ได้จากการคำนวณ Process A ส่งไปให้ External API และรับค่าผลลัพธ์กลับมา

แปลว่า เราต้องแบ่ง Method ตัวนี้ออกเป็นสอง Method เพื่อทำการทดสอบ Unit Test กับ Process A และ Integration Test กับ Process B

ถ้าค้นพบว่างานที่เราทำอยู่ กำลังเป็นเช่นนี้ และต้องแยกมันออกมา นั่นคือ เราเขียน Method นี้ ไม่ดีแต่แรกแล้วครับ เพราะอย่าลืมว่า 1 Method ควรจะมีเพียง 1 การทำงานเท่านั้น

(รูปของ Method IsAdd() ที่เราวิเคราะห์การทำงาน และแบ่งชุดทดสอบ)

ต้องรองรับกระบวนการ Continuous Integration ได้ด้วย

เมื่อโครงสร้างของโปรเจ็คและโครงสร้างของโค้ดเรา สามารถรองรับการทดสอบได้แล้ว สิ่งที่ต้องคิดต่อมาคือ จะนำเข้ากระบวนการ Continuous Integration ได้อย่างไร เพื่อให้เกิด Pipeline และ Feedback Loop ที่เร็ว

เพราะ Unit Test สามารถอยู่ได้ด้วยตัวเอง แต่ Integration Test จะต้องเกี่ยวข้องกับระบบอื่นๆ เช่น  Database หรือ File System ซึ่งระบบเหล่านี้ควรมีประบวนการพร้อมใช้งานก่อน ดังนั้น ถ้ามันอยู่ร่วมโปรเจ็คเดียวกัน คงไม่ดีแน่นอน และไม่รู้ได้ด้วยว่าถ้าเกิดการรันและ Fail มันเกิดที่กระบวนการของ Unit หรือ Integration กันแน่ จึงควรแยกโปรเจ็คของชุดทดสอบนี้ออกจากกัน

(รูปของ Method IsAdd() ที่เราแบ่งชุดทดสอบ ออกเป็น 2 Project)


(รูปตัวอย่างเมื่อแยกโปรเจ็ค Unit Test, Integration Test แล้ว สามารถกำหนด Pipeline ให้เหมาะสมได้)

Tip: ใช้ Postman ทดสอบ Integration Test จะได้หรือไม่

คำถามนี้ตอนแรกต้องยอมรับว่า ผมก็สับสนเหมือนกัน แต่เพิ่งได้ข้อสังเกตมาจากผู้รู้ว่า ความต่างระหว่างที่เราเขียนโค้ดทดสอบเพื่อเรียก API ตัวเอง กับใช้ Postman เพื่อทดสอบ API ตนเอง จะเรียกทั้งคู่เป็น Integration Test ได้หรือไม่

คำตอบคือ ใช้ Postman ทดสอบเรียก API จะไม่เรียกเป็น Integration Test แม้ว่าภายใน API นั้นจะเรียกไปที่ต่างๆจริง คืนค่าจริงกลับมา, แต่ถ้าให้ถูกต้องจะเรียกว่า End-to-End Testing

เพราะเราเอาระบบภายนอก (Postman) เรียกไปที่ API ว่าทำงานได้หรือไม่ แต่เราไม่ได้ทดสอบว่าระบบทั้งสองทำงานถึงกันได้หรือไม่ (เส้นสีชมพู) นั่นจึงเป็นที่มาว่า ใช้ Postman ก็อาจไม่ชัวร์ว่า ระบบเราจะทำงานได้จริง

หรือให้ลองนึกอีกที เคยไหมครับ ที่ใช้ Postman ทดสอบ API ได้ แต่พอรันโค้ด กลับทำงานไม่ได้ เพราะมี คอมพิวเตอร์เรามี Environment เรียกใช้ต่างกันกับเครื่อง Server เป็นต้น

Tip: ถ้าอยากทดสอบทุกกระบวนการ แต่ไม่ต่อไปถึง External API จะทำอย่างไร

เป็นคำถามที่ถูกถามบ่อยเหมือนกัน และโปรแกรมเมอร์ก็มักเจอบ่อยเช่นกันว่า

  • ถ้าจะทดสอบ Services ตัวเอง โดยยังไม่อยากไปเชื่อมต่อระบบภายนอก
  • อยากทดสอบ Services ตัวเอง แต่ระบบภายนอกยังทำไม่เสร็จ
  • อยากทดสอบ Services ตัวเอง แต่ระบบภายนอกทำงานช้า ไม่ทันใจ

จากปัญหาข้างต้น เราสามารถนำวิธีการชื่อว่า Stub จาก Test Double มาใช้ได้ คือทำ Stub ตัว External API มันซะเลย เพื่อให้ได้ค่าที่เราต้องการเสมอ และไม่ต้องออกไปรับค่าจริงจาก External API

ถามว่าแล้วจะทำไปทำไม ก็ต้องตอบว่า เมื่อเราทดสอบการทำงานย่อยๆแล้ว (Unit Test) และทดสอบการทำงานที่สองระบบเชื่อมถึงกันแล้ว (Integration Test) เราอยากจะสร้างความมั่นใจเพิ่มไหมล่ะ ว่า ถ้าทั้งสองการทดสอบทำงานร่วมกัน มันยังถูกต้องอยู่

เราจะเรียกการทดสอบแบบนี้ว่า Component Test ซึ่งในที่นี้เราต้องการทดสอบการทำงานทั้งหมดของ Service เรา แต่เราไม่ได้สนใจว่า External API จะอยู่หรือจะตาย ทำงานได้ถูกต้องหรือไม่ ดังนั้น เราจึงต้องจำลอง External API มันขึ้นมา โดยใช้ Stub นั่นเอง (เขียนเองก็ได้ หรือหา Library ฟรีมาใช้ก็ได้ เช่น  .Net Core: Stuberry, Java: Stubby4j)

สรุป

การทำระบบให้รองรับ Automated Test เป็นอะไรที่สนุกเหมือนกัน ต้องวางแผนตั้งแต่แรกเลยว่าจะเป็นอย่างไร แล้วส่วนมากเราก็มักไม่ทำกันซะด้วย เพิ่งมาตามเก็บเอาตอนหลัง เหมือนที่ผมเกริ่นไปตอนต้น ว่าถ้ามีโปรเจ็คเก่าแล้วเอามาทำ เป็นอะไรที่ต้องแก้เยอะพอสมควร แต่ถ้าเป็นโปรเจ็คใหม่ ก็สามารถออกแบบให้ดีตั้งแต่ต้นได้เลย หากใครผ่านมาอ่าน มีคำแนะนำเพิ่มเติม หรือคำถาม สามารถแลกเปลี่ยนความรู้กันได้เลยนะครับ

สำหรับตัวอย่างการแยก Test ลองดูที่ GitHub ของผมได้ครับ มีแต่ .NET Core นะ กำลังอิน ฮ่าๆ (BOT UnitTest, BOT IntegrationTest, BOT Service)


อ่านภาคการนำไปประยุกต์ใช้ต่อได้ที่ ภาคปฎิบัติ: จะทำระบบให้รองรับ Automated Test ได้อย่างไร (Testable)

ผู้ชายธรรมดาคนหนึ่ง ชื่นชอบหลายเรื่องที่ไม่น่าจะไปกันได้ ทำงานไอที แต่ชอบท่องโลกกว้าง รักประวัติศาสตร์ แต่ก็สนใจเทคโนโลยี ชอบสร้างแรงบันดาลใจให้ตัวเอง และไปป้ายยาคนอื่นต่อ