Software Design

🧓 Uncle Bob - TDD

🤔 เขียนเทสมันจะช่วยให้การออกแบบมันดีขึ้นได้ไง ?

การทำ Clean Code ตอนที่ 4 จากวีดีโอของ 🧓 ลุงบ๊อบ หรือ Robert C. Martin หนึ่งในมหาเทพวงการคอมพิวเตอร์ ซึ่งในรอบนี้เราจะมาดูกันว่า การทำเทสมันช่วยเรื่องการออกแบบได้ไง หลายคนเคยทำเทสแล้วพบว่ามันช้าเสียเวลามาก ดังนั้นบทความนี้จะมาเฉลยกันว่าจริงๆแล้วมันดีหรือไม่ดีกันแน่นะ ?

วีดีโอตัวนี้หลักๆลุงบ๊อบแกมานั่งทำ TDD ให้ดู ซึ่งแมวน้ำเคยทำวีดีโอ กับ เขียนบทความเรื่องพวกนี้ไว้เยอะและ ดังนั้นขอข้ามตัวอย่าง เขียนแต่สรุปประเด็นให้อ่านแทนนะ ส่วนใครอยากดูที่แมวน้ำทำไว้ก็ศึกษาต่อได้จากลิงค์นี้เบย 👦 Test-Driven Development กับ 👦 Test-First Design

แนะนำให้อ่าน บทความนี้เป็นส่วนหนึ่งของบทคอร์ส 👶 Clean Code หากเพื่อนๆแมวน้ำสนใจศึกษาเรียนรู้ว่าการทำ Clean Code ว่ามันคืออะไร? มีอะไรบ้าง? บลาๆ ก็สามารถกดที่ชื่อบทความสีฟ้าๆเข้าไปอ่านได้เลยครัช

🧓 เกริ่น

โปรแกรมเมอร์ส่วนใหญ่จะ ไม่กล้าแก้โค้ดเพราะกลัวว่าที่แก้ไปมันจะไปทำให้จุดอื่นๆพัง ซึ่งหมายความว่าเราจะทำคลีนโค้ดได้ยากขึ้น เพราะแม้จะเห็นจุดที่โค้ดพันกันวุ่นวาย ก็จะไม่มีคนกล้าไปแตะโค้ดเหล่านั่นนั่นเอง แต่ในทางตรงข้าม ถ้าเราเขียนเทสไว้แล้วเราแก้โค้ดปุ๊ปแล้วมันพังปั๊ป เราก็จะรู้ได้ทันทีว่าที่แก้ไปมีปัญหานะ ดังนั้นการทำเทสนั้นจะช่วยให้เราแก้โค้ดได้เรื่อยๆโดยไม่กลัว bug นั่นเอง

🧓 เมื่อเราสามารถแก้โค้ดได้โดยไม่ต้องกลัวว่ามันจะมี bug นั่นหมายความว่า เราก็จะสามารถ ปรับ Design เปลี่ยน Algorithm ได้ตลอดเวลา และรวมถึงเมื่อมี Requirement ใหม่ๆเข้ามา เราก็จะทำมันได้โดยที่รู้ว่ามันไม่ไปทำให้สิ่งที่ทำงานได้อยู่แล้วพังนั่นเอง

เทสในวงการซอฟต์แวร์มีหลายระดับ ซึ่งระดับแรกที่ต้องรู้จักคือ (TDD) Test-Driven Development ซึ่งมันมีหัวใจหลักคือ 💖 ช่วยในการออกแบบโครงสร้าง เลยทีเดียว ดังนั้นเราจะมารู้จักเนื้อแท้มันดีกว่าว่ามีค่าพอจะเสียเวลาชีวิตเราไหม?

แนะนำให้อ่าน หากใครสนใจอยากรู้ว่าการทำ TDD หรือ Test-Driven Development จริงๆแล้วต้องทำยังไงบ้างสามารถดูตั้งแต่เริ่มต้นจนจบได้จากลิงค์นี้ 👦 Test-Driven Development ซึ่งแมวน้ำได้อธิบายเทสตั้งแต่พื้นฐานไว้อยู่แล้ว และเทียบให้ดูระหว่างทำกับไม่ทำในระยะยาวมันต่างกันขนาดไหนเรียบร้อยละ ส่วนใครขี้เกียจดูซีรี่ยาวๆก็อ่านบทความสั้นๆเพื่อให้พอเห็นภาพได้จากลิงค์นี้ครับ 👦 Test-First Design

💖 หัวใจการทำเทส

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

🧓 เทสจะช่วยให้เรา Design ได้ดีขึ้น โดยช่วยให้เราสร้างแต่ของที่จำเป็นเท่านั้นออกมา ดังนั้นเราจะสามารถ Refactor design ได้ง่าย เพราะโค้ดที่ได้มามันไม่ค่อยไปวุ่นวายกับโค้ดอื่นๆนั่นเอง ตามชื่อมัน Driven Design

🎯 กฏเหล็ก

เมื่อเราจะเริ่มเขียนโค้ดตาม requirement ที่ได้รับมานั้น ถ้าเราทำ TDD มันมีกฎง่ายๆอยู่คือตามนี้

  1. ห้ามเขียนโค้ดจนกว่าจะเขียนเทส - เดฟหลายคนพอจับคอมปุ๊ปก็จะเขียนโค้ดเลย ซึ่งข้อเสียคือ เรารู้ได้ไงว่าเราเข้าใจตัว requirement นั้นจริงๆ? หรือ เรารู้ได้ไงว่ามันควรจะต้องสร้างคลาสใหม่ หรือต้องมี method นั้นจริงๆ? ดังนั้นเราจะต้องเขียนเทสเพื่อพิสูจน์ว่าสิ่งที่เราคิดมันถูกต้องนั่นเอง

  2. ห้ามเขียนเทสต่อจนกว่าจะทำให้เทสตะกี้ผ่าน - หลังจากที่ทำข้อแรกเสร็จเราจะได้ ตัวที่เอาไว้ทดสอบโค้ดเรา 1 ตัวละ ดังนั้นเราก็จะเริ่มเขียนโค้ดให้ตัวทดสอบตัวนั้นผ่าน เพื่อเป็นการพิสูจน์ว่า การเพิ่มคลาสนี้ ใช้ตัวแปรนั้นๆ สามารถทำให้ได้ผลลัพท์แบบที่เราอยากได้นั่นเอง

  3. กลับไปทำข้อ 1 ใหม่ โดยเพิ่มเทสให้ครบทุกกรณีที่มันจะเกิดขึ้นได้ เพื่อเป็นการพิสูจน์ว่าโค้ดของเราทำงานได้กับทุกรูปแบบที่มันเป็นไปได้ โดยไม่มีข้อผิดพลาดนั่นเอง

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

🧓 โปรแกรมเมอร์ส่วนใหญ่จะมีปัญหากับแนวคิดแบบนี้ เพราะของบางอย่างที่ง่ายๆตรงไปตรงมา ทำไมไม่เขียนโค้ดไปเลย ซึ่งคำตอบถ้ามองแบบผิวเผินก็จะเป็นแบบนั้นแหละ แต่ถ้าเรามองในระยะยาว การเขียนเทสมันถูกทดสอบมาแล้วว่ามันจะทำให้เราทำงานได้เร็วขึ้นจนแบบที่ไม่เขียนเทสตามไม่ทันเลยทีเดียว (ปล.ต้องเขียนเทสให้เป็นด้วยนะ)

Specification by Example ตัวเทสที่ได้มามันคือ Document ของระบบ หากเราใช้หลักการของ (SbE) Specification by Example เข้ามาช่วย เราก็ไม่จำเป็นต้องมีคนมาเขียน document อีกเลย เพราะงานที่เดฟทำมันเป็น selfdocument ในตัวอยู่แล้ว แถมใครอ่านก็เข้าใจ ไม่ต้องมีความรู้ในเรื่องการเขียนโค้ดเลยก็ยังได้ เดี๋ยวแมวน้ำจะเขียนตัวอย่างการทำ SbE อยู่ติดตามได้จาก เพจแมวน้ำ นี้เด้อ

ข้อควรระวัง เดฟหลายๆคนเวลาเจอ bug ก็จะไปนั่งหา bug โดยการใช้ Debugging แล้ววาง break point ตามจุดต่างๆชิมิ ขอบอกเลยว่า พยายามเลิกใช้ Debugging tools เหล่านั้นได้แล้ว เพราะกระบวนการ debug เป็นของที่เสียเวลา และมันอาจจะแก้ปัญหาจุดนั้นๆได้ แต่ไม่มีอะไรการันตีได้เลยว่า คุณแก้ bug ได้หมดจริงๆ ซึ่งต่างจากการมี test case เพราะมันเร็วกว่าและช่วยเช็คให้เราได้ว่าของที่แก้มันไม่ทำให้อย่างอื่นพัง

หากใครเคยได้ทำงานที่มี security สูงมากๆจริงๆจะพบว่า เราไปยุ่งกับ Production ไม่ได้เลย ต่อ database ตรงไม่ได้ เข้า server ก็ไม่ได้ เพราะมันเป็น Environment ปิด (หากมีคนในทีมเข้าไปแก้อะไรมั่วๆได้อาจะทำให้เกิดการโกงได้แบบง่ายๆเลย) ดังนั้นถ้าบริษัทคุณแข็งในด้าน Sensitive data จริงๆ เราจะ debug ไม่ได้เลย และต้องพึ่งการทำ Mornitoring เท่านั้น และแนวโน้มสมัยนี้ผลักให้เราไปทำงานบนคลาว์มากขึ้น ดังนั้น Tracking system คือของที่จะมาลดบทบาทของ Debugging นั่นเอง

เกร็ดความรู้ 1 GUI จริงๆก็สามารถเขียนเทสได้นะ เช่น ทดสอบว่าหน้า Login ทำงานถูกหรือเปล่า เขาก็จะมี test case สำหรับตรวจสอบเป็นขั้นๆเลย เช่น เมื่อเข้ามาให้กรอก username กับ password เป็นแบบนี้ แล้วสั่งให้กดปุ่ม login มันจะต้องเข้าสู่ระบบได้ บลาๆ ซึ่งนี่ก็เป็น test อีกรูปแบบหนึ่งเหมือนกัน แต่ไม่ได้หมายความว่าเราจะไม่ทำ Manual Test แล้ว เพราะการเขียนเทสมันไม่สามารถตอบเรื่องความสวยงามได้ เช่น หน้า Login แบบนี้สวยหรือยัง ตำแหน่งเหมาะสมหรือเปล่า ควรจะใช้ตัวอักษรเล็กลงดีไหม บลาๆ ดังนั้นการเอาคนมาตรวจก็ยังจำเป็นอยู่นั่นเอง

เกร็ดความรู้ 2 เราสามารถนำ Pair Programming มาช่วยกันเขียน TDD ได้นะ ซึ่งปรกติเราจะสลับตากัน โดยให้คนแรกเขียนเทส fail แล้วให้คนที่สองทำให้เทสผ่าน แล้วก็สลับมาเป็นคนที่สองเขียนเทส ให้คนแรกเขียนโค้ดให้ผ่าน สลับไปสลับมาแบบนี้เรื่อยๆ มันจะช่วยให้แต่ละคนอุดจุดบอกที่อีกฝั่งตกได้ และยังช่วยเสริมความเข้าใจในตัวงานได้ดียิ่งขึ้นด้วย

💡 หลักในการออกแบบ

ในการเขียนเทสนั้นมีของ 2 อย่างที่มันเกี่ยวข้องกันนั่นคือ โค้ดสำหรับเทส กับ โค้ดที่ทำงานจริง ซึ่งของพวกนี้มันมีวิธีการคิดที่ต่างกันคนละมุมคือ

  • โค้ดสำหรับเทส - จะต้องมองในมุมของการเทส นั่นหมายความว่า ทุกครั้งที่เพิ่มเคสใหม่ๆเข้าไป มันจะ ระบุเจาะจงมากขึ้นไปเรื่อยๆ และเริ่มแตกเป็นหลายๆกรณีลึกเข้าไปเรื่อยๆจนถึงเคสขอบ (เคสที่มีโอกาสเกิดน้อยมาก หรืออาจจะไม่เกิดเลย แต่เราต้องทำเพราะถ้ามันเกิดขึ้น อย่างน้อยโค้ดของเราก็จะไม่พัง)

  • โค้ดที่ทำงานจริง - จะต้องมองในมุมของระบบที่จะทำงานด้วยกัน ดังนั้นทุกๆครั้งที่เพิ่มโค้ดที่จะเอาไปใช้งานจริงๆ เราจะต้องทำให้มันทำงานได้แบบกว้างๆ (Generalize) มากขึ้น ห้ามทำให้มันไปยึดติดกับของบางสิ่งบางอย่าง เพราะไม่อย่างนั้นเวลาที่ต้องแก้ไขจะทำได้ยากนั่นเอง

🧓 เดฟหลายคนจะมีปัญหาในการเอา TDD ไปใช้ เพราะทั้ง 2 เรื่องนี้มันคิดสวนทางกันเลย ยิ่งเพิ่มยิ่งเจาะจง กับ ยิ่งเพิ่มยิ่งไม่เจาะจง ซึ่งจริงๆก็ไม่เกี่ยวกับ TDD โดยตรง แต่มันเป็นหลักในการออกแบบขั้นพื้นฐานนั่นเอง

แนะนำให้อ่าน ทำไมโค้ดที่ทำงานจริงถึงห้ามเขียนเฉพาะเจาะจง แมวน้ำเคยอธิบายไว้ในบทความ 🤰 Creational Patterns แล้ว ซึ่งมันเป็นหัวใจหลักของการทำ Architecture Design เลย

🙌 ส่งท้ายบท

TDD เป็นแค่ส่วนหนึ่งของการทำเทส ถ้าเราศึกษาต่อจะพบว่าการเขียนเทสนั้นมีหลายมิติมาก และแต่ละเรื่องมันก็จะเทสเฉพาะส่วนของมันเท่านั้น และวิธีทำก็เป็นคนละเรื่องกันนั่นเอง หากใครสนใจก็ลองไปศึกษาต่อได้กับ Software Testing

🧓 สมัยก่อนลุงบ๊อบก็คิดว่าแนวคิดแบบ TDD มันงี่เง่ามาก ไอ้บ้าที่ไหนจะเขียนเทสก่อนเขียนโค้ดจริง จนกระทั่งแกได้ไปนั่งทำ Pair Programming กับ Kent Beck (หนึ่งในมหาเทพอีกองค์) ถึงได้เข้าใจว่ามันเป็นเทคนิคที่ทรงพลังจริงๆ

🧓 หากใครเอา TDD ไปใช้แล้วคิดว่ามันช้า ลุงบ๊อบบอกเลยว่าเพราะ คุณ(เอ็ง)ยังกากอยู่ ยังฝึกไม่ชำนาญพอ เพราะถ้าทำจริงๆสุดท้ายมันจะเร็วจนไม่มีโปรเจคไหนที่เราจะไม่ทำเทสเลย แต่ห้ามไปฝึก TDD ในโปรเจคที่จะใช้จริงนะไม่งั้นบรรลัยจะบังเกิด ให้หาเวลาว่างไปฝึกกับปัญหาอะไรก็ได้เรื่อยๆจนกว่าจะเข้าใจถึงแก่นมันจริงๆจะดีกว่า

🎥 วีดีโอลุงบ๊อบ