Experiences with test driven development

เมื่อหลายเดือนก่อน ผมได้เขียนเกริ่นเกี่ยวกับ TDD ไป และผมก็ได้บอกกับผู้อ่านว่า ตัวผมเองนั้นเพิ่งจะเริ่มหัด TDD เลยไม่อาจจะบอกได้ว่า มันดีหรือไม่ดีอย่างไร แต่ ณ วันนี้ ผมได้เขียนโปรแกรมแบบ TDD ด้วยภาษา Swift ตั้งแต่เดือน เมษายน จนถึงปัจจุบันเดือนกันยายน เป็นเวลา 6 เดือน รวมไปถึงได้เปลี่ยนวิธีการพัฒนา software ไปเป็น agile และเห็นความเปลี่ยนแปลงต่างๆที่เกิดขึ้น จึงอยากจะนำมาแชร์ให้ฟังว่ามันมีอะไรดีและไม่ดีบ้าง

ข้อดี

  • ช่วยให้การ refactor นั้นง่ายมาก
  • การ debug โปรแกรม น้อยลงมากๆ
  • โปรแกรมมีคุณภาพมากกว่าเดิมเยอะมาก
  • Continuous Integration / Delivery และ Automate Testing

ข้อเสีย

  • ต้องมีเวลาในการเรียนรู้ พอสมควร
  • ในบางครั้งมีความซับซ้อนในการออกแบบโปรแกรม มากกว่าเดิม
  • เขียนโค้ดมากขึ้น (ก็แหงละ ต้องเขียน test นี่)
  • ต้อง maintain test suit

อธิบายเพิ่มเติมกับข้อดีก่อนละกัน คือ เมื่อเราเขียนโค้ดไปเยอะๆเนี่ย บางครั้งโค้ดที่เขียนมันก็อ่านยาก เพราะ ณ จังหวะที่เราเขียนโค้ด บางทีก็ไม่ได้คิดถึงว่า มันจะอ่านง่ายหรือเปล่า วางโครงสร้างแบบนี้ดีหรือยัง คิดแค่เพียงว่า จะให้ผลลัพธ์มันออกมายังไง สุดท้ายโค้ดที่เขียนมันก็ทำงานได้ แต่กลับอ่านยาก บางครั้งแทบจะอ่านไม่รู้เรื่อง โครงสร้างโปรแกรมวางมั่วไปหมด .. เมื่อเริ่มเกิดปัญหาเหล่านี้ สิ่งที่จะทำต่อมาก็คือ เรียบเรียงโค้ดใหม่ ให้มันทำงานเหมือนเดิมแต่อ่านง่ายขึ้น มีโครงสร้างที่ดีขึ้น หรือที่เรียกว่า Refactoring แต่ปัญหาหลักของการ refactoring คือ โค้ดที่เขียนใหม่ มักจะทำงานไม่เหมือนเดิม ที่มันไม่เหมือนเดิม ก็เพราะว่าโค้ดใหม่ของเราอาจจะเขียนไม่ครอบคลุม ลืมนั่นนี่ หรืออาจจะไม่เข้ากับ component อื่นๆ สุดท้ายก็กลายเป็น bug สร้างปัญหาให้เราซะอย่างนั้น แต่การที่เราเขียนโปรแกรมด้วย TDD มันจะบังคับให้เราเขียน Test Case ก่อน นั่นก็หมายความว่า โค้ดที่เราเริ่มเขียนใหม่ มันต้องผ่าน test เสมอ ดังนั้นแล้ว เราจะแทบไม่มีปัญหาเกี่ยวกับการ refactoring เลย และเราจะมั่นใจมากขึ้นกว่าเดิมว่าโค้ดใหม่มันจะไม่ทำให้โปรแกรมพัง

การ debug โปรแกรมก็ลดลง ผมเชื่อว่าหลายคนเสียเวลาหลาย ชม. เพื่อดีบักโปรแกรม ที่มันเสียเวลาดีบักนาน ก็เพราะว่าส่วนมากแล้วเราไม่รู้ว่าปัญหามันเกิดขึ้นที่ตรงไหน มีอะไรหลุดจากที่เราเขียน แต่ปัญหาเหล่านี้จะลดน้อยลง เพราะเรามี test case รองรับไว้แล้ว ถึงแม้ว่า test case มันจะไม่ครอบคลุมในทุกๆส่วน แต่มันช่วย safe ตัวเราจากความสะเพร่าในการเขียนไปหลายอย่างทีเดียว ยกตัวอย่างที่เป็นประสบการตรงก็คือ ในช่วงแรกๆที่เริ่มเขียน TDD ผมคิดว่าการเขียน test case บางอย่างมันดูง่อย และรู้สึกว่ามันไม่มีประโยชน์ คือผมเขียนเคสที่ใช้ทดสอบการแปลงตัวเลขไปสตริง โค้ดที่ใช้สำหรับแปลงตัวเลขไปเป็นสตริงมันง่ายมาก คิดว่าคงไม่มี bug หรอก .. ใช่มันง่ายมาก และ test case นี้ก็ดูง่ายและแทบไม่มีประโยชน์เลย แต่ test case ง่ายๆแบบนี้กลับช่วยตัวผมไว้ เพราะใครจะไปคิดว่าโปรแกรมมันก็ทำงานได้ดี ใน Simulator และ iPhone 6 แต่พอไปทำงานใน iPhone 5 กลับทำโปรแกรมพัง ซะอย่างนั้น .. ปัญหาที่เกิดก็คือ iPhone 5 เป็น 32 bit ส่วน Simulator และ iPhone 6 เป็น 64 bit .. ค่าตัวแปรมันเลยมีขนาดไม่เท่ากัน เมื่อแปลงค่าเป็นสตริงผลลัพธ์เลยเลยผิด ใครจะไปคิดว่า แค่การแปลงตัวเลขเป็นอักษรง่ายๆแค่นี้ มันจะมีปัญา ถ้าไม่มี test ผมอาจจะต้องใช้เวลาหลาย ชม เพื่อหาว่า ทำไมโปรแกรมมันถึงพัง และอาจจะแก้ปัญหาผิดจุด

เมื่อโปรแกรมเรามี bug น้อยลง โปรแกรมมันก็มีคุณภาพมากกว่าเดิม คงไม่มีใครชอบใช้โปรแกรมที่มี bug เยอะๆหรอก จริงไหม ? ต่อให้มันมีฟีเจอร์ดีแค่ไหน ถ้ามันเกิด error บ่อยๆ ก็คงไม่แฮปปี้ที่ใช้โปรแกรมของเรา การที่เขียนโปรแกรมด้วย TDD มันทำให้โปรแกรมมี bug น้อยลงเนื่องจากเรามี test นั่นแหละ

และสุดท้ายคือ การทำ Continuous Integration (CI) มันจะง่ายมาก หลายคนอาจจะเพิ่งเคยได้ยิน CI ผมก็จะขออธิบายคอนเซ็ปคร่าวๆให้ฟังก็คือว่า  CI เนี่ยมันคือการรวมโค้ดกันบ่อยๆ คือเมื่อเราเขียนโปรแกรมใหญ่ๆ มันต้องมีคนเขียนหลายๆคน แต่ละคนก็รับผิดชอบคนละส่วน ทีนี้ปัญหามันก็จะเกิดตอนเอาโค้ดของแต่ละคนมารวมกันนี่แหละ .. เพราะสมมติว่า ต้องเขียน feature A แต่จะเขียนโค้ดที่ master branch ก็ไม่ได้เพราะว่า อาจจะทำโค้ดหลักพัง ดังนั้นนาย ก. จึงแยกโค้ดจาก master branch ไปเขียน branch feature แต่กว่าที่นาย ก จะเขียนเสร็จก็ใช้เวลา 3 วัน และการที่จะเอาโค้ดของนาย ก. มารวมกับโค้ดของคนอื่น .. มันก็จะเกิดปัญหาที่ว่า โค้ดหลัก master branch ได้เปลี่ยนไปแล้ว โค้ดของนาย ก. ก็ต้องแก้ไขเพื่อให้มันทำงานร่วมกับ master ได้ .. แต่ถ้า นาย ก. เอาโค้ดที่เขียนไปในแต่ละวันมารวมเข้ากับ master branch บ่อยๆละ .. ก็แน่นอนว่า นาย ก. คงไม่เกิดปัญหาแบบที่ได้กล่าวไป นั่นก็คือการทำ Continuous Integration .. แต่การที่จะทำให้โค้ดของ นาย ก. มารวมกับ master ได้บ่อยๆนั้น ไม่ใช่เรื่องง่าย เพราะว่า โค้ดที่นาย ก. เขียนไป อาจจะทำ master พังก็ได้ ดังนั้นแล้ว .. ถ้าหากนาย ก. ได้เขียน test ไว้ ก็จะเพิ่มความมั่นใจได้ว่า โค้ดที่นาย ก. เพิ่มเข้ามาจะไม่ทำให้ของเก่าพังนั่นเอง นอกจากนี้ เมื่อมี CI เราก็จะสามารถทำ automate test ได้ง่ายขึ้นกว่าเดิม

พูดข้อดีมาเยอะแล้ว ก็ใช่ว่ามันจะไม่มีข้อเสีย

ข้อเสียอย่างแรกก็คือ มันต้องใช้เวลาในการเรียนรู้ คือ TDD คอนเซปมันง่ายนะ แต่เอาจริงๆแล้ว ยังมีอีกหลายอย่างที่ต้องทำความเข้าใจ เพราะเมื่อเริ่มเขียน TDD สิ่งที่จะตามมาก็คือ ต้องเข้าใจ Stub / Mock เพราะเป็นส่วนที่ช่วยให้เราเขียน test ได้ง่ายขึ้น .. เมื่อเขียน stub/mock เป็นแล้ว ต่อมาก็ต้องเข้าใจเรื่อง dependency injection และ Inversion of Control คือสิ่งพวกนี้มันใช้เวลาในการเรียนรู้ค่อนข้างมาก ที่ผมบอกว่าค่อนข้างมาก ก็เพราะว่าในช่วงแรกนั้น เราอาจจะไม่เข้าใจว่าทำไมจะต้องเขียน mock / stub หรือใช้พวก dependency injection พวกนี้ กว่าที่จะเข้าใจและเห็นปัญหา มันใช้เวลาเรียนรู้สักพักใหญ่ๆ คือมันไม่ใช่แค่ว่าอ่านแล้วเข้าใจนะ แต่มันต้องเขียนไปสักพักถึงจะเข้าใจว่า อ้อออออ มันมีประโยชน์แบบนี้เอง

ข้อเสียอย่างที่สองก็คือ โปรแกรมมันอาจจะซับซ้อนมากกว่าเดิม อ้าวทำไมเป็นอย่างนั้นละ .. เพราะเวลาที่เราเขียนโปรแกรมด้วย TDD สิ่งที่ต้องคิดเป็นอย่างแรกก็คือ เราจะออกแบบให้โปรแกรมเรามัน testable ได้อย่างไร .. คือพอคิดแบบนี้แล้ว บางทีเราต้องเขียนส่วนต่างๆเพิ่มมากขึ้นกว่าแต่ก่อน อย่างเช่น stub / mock คือมันก็ทำให้โปรแกรมมันซับซ้อนขึ้น

TDD ทำให้เขียนโค้ดมากขึ้น แต่โค้ดพวกนั้นไม่ได้เป็นส่วนเกี่ยวข้องกับโปรแกรม เพราะมันเป็นโค้ดในส่วนของ test และโดยเฉลี่ย เราจะต้องเขียนโค้ดมากกว่าเดิม 20% ( เสียเวลาเขียนโค้ดแต่มันก็ลดเวลาในการ debug นะ) และอีกอย่างคือ ถ้าหากผลลัพธ์ที่เราต้องการเปลี่ยนแปลง เราก็จะต้องคอยปรับ test suit ของเราด้วย

สรุป

TDD เป็นสิ่งที่ดีมาก และควรจะหัดเรียนรู้ เพราะมันช่วยให้ชีวิตดีขึ้นหลายๆด้าน โดยเฉพาะ Quality ของ Code และ Product ของเรา แต่ก็ต้องยอมรับว่า สำหรับผู้ที่จะเริ่ม TDD นั้น ไม่ใช่เรื่องที่จะเริ่มได้ง่ายเลย เพราะมันมีหลายอย่างที่ต้องทำความเข้าใจ ( ถ้ามีคนสอนจะเป็นไวกว่ามากครับ ) นอกจากนี้ยังต้องใช้เวลาในการที่จะเรียนรู้ว่า การเขียน test ให้ดีนั้นต้องเขียนแบบไหน ทำยังไง

โดยส่วนตัวผมคิดว่า แม้ว่า TDD จะเป็นวิธีการที่ดีมากในการเขียนโปรแกรม เพราะมันเพิ่มคุณภาพและลด bug ของโปรแกรม แต่มันก็ไม่ได้เหมาะกับงานทุกอย่าง ที่เห็นได้ชัดๆ ก็คือ การเขียนเกมส์  คือไม่ใช่ว่า TDD มันไม่ดีนะ แต่ว่ามันไม่เหมาะกับงานแบบนี้ ลองจิตนาการว่า เราจะเขียน test สำหรับ particle system หรือจะเขียน test ทดสอบว่า เกมส์มัน lag แบบนี้ วัตถุชนกันในแบบ 3D หรือมุมกล้องแบบนี้ผู้เล่นมองไม่เห็น เกมส์มัน lag อะไรแบบนี้ .. คิดออกไหมว่าจะเขียนโค้ดของการ test มันจะออกมายังไง ? และเกมส์โปรแกรมเมอร์ ก็มีวิธีการอื่นที่ให้ผลเหมือนอย่าง TDD เช่น Assertions

แล้วผมจะเขียน TDD ต่อไหม ? .. เขียนครับ และผมก็แนะนำให้พวกคุณเขียนด้วย เพราะมันช่วยให้เราพัฒนาตัวเองได้เยอะครับ แต่ถ้าต้องเขียน เกมส์ ก็คงไม่ได้ใช้ TDD ผมคิดว่าวิธีการต่างนั้นไม่ว่าจะเป็น TDD, automate test หรือ CI อะไรนั้นดีหมด แต่ต้องเลือกสิ่งที่เหมาะสมกับการใช้งานครับ

Leave a Reply