Red, Green, Refactor
แดง เขียว refactor — จังหวะสามก้าวของ TDD
คืออะไร
หัวข้อที่มีชื่อว่า “คืออะไร”Red, Green, Refactor คือแนวปฏิบัติ (practice) ที่เป็นหัวใจของ Test-Driven Development มันคือวงจรสามจังหวะที่ทำซ้ำหลายสิบรอบต่อชั่วโมงระหว่างการเขียน code: เขียน test ที่ล้มเหลวก่อน (Red) ทำให้มันผ่านด้วย code ที่น้อยที่สุด (Green) แล้วจึงพิจารณาว่าจะปรับปรุง design ของ code ที่มีอยู่ให้ดีขึ้นหรือไม่ (Refactor) ก่อนวนกลับไปเริ่มรอบใหม่
ชื่อ “Red” และ “Green” มาจากสีที่ตัวรัน test ส่วนใหญ่ใช้แสดงผล — test ที่ล้มเหลวติดสีแดง test ที่ผ่านติดสีเขียว วงจรนี้ถูกเผยแพร่กว้างขวางผ่านหนังสือ Test-Driven Development: By Example ของ Kent Beck และถูกอธิบายซ้ำในหลายแหล่ง เช่นบทความ “Workflows of Refactoring” ของ Martin Fowler ที่สรุปสามขั้นไว้ว่า Add a Test, Make it Work, Make it Clean
หลักการสำคัญที่มักถูกมองข้ามคือ แต่ละขั้นต้องการโหมดความคิดที่ต่างกัน — ตอนเขียน code ให้ test ผ่าน (Green) ให้สนใจแค่ “ทำงานถูกต้อง” อย่าห่วงเรื่อง design ส่วนตอน Refactor ให้สนใจแค่ “design สะอาด” โดยห้ามเปลี่ยนพฤติกรรม การแยกสองโหมดนี้ออกจากกันอย่างเคร่งครัด (นักเขียนบางคนเรียกว่า “wear one hat at a time”) คือสิ่งที่ทำให้วงจรนี้ปลอดภัยและเร็ว เพราะไม่ต้อง debug พร้อมกับปรับ design ในเวลาเดียวกัน
ทำอย่างไร
หัวข้อที่มีชื่อว่า “ทำอย่างไร”flowchart LR
Think[คิดว่าจะ test อะไรต่อ] --> Red[Red: เขียน test ที่ล้มเหลว]
Red --> Green[Green: เขียน code น้อยที่สุดให้ผ่าน]
Green --> Decide{ควร Refactor ไหม}
Decide -->|ใช่| Refactor[Refactor: ปรับ design test ยังเขียว]
Decide -->|ยังไม่ใช่| Think
Refactor --> Think
Red — เขียน test ที่ล้มเหลว (Write a failing test)
หัวข้อที่มีชื่อว่า “Red — เขียน test ที่ล้มเหลว (Write a failing test)”ใน TDD ทุกอย่างเริ่มต้นด้วย test ที่ล้มเหลว คุณเขียน test ก่อน เพื่อจะได้รู้ว่าเมื่อไรควร หยุด เขียน code สำหรับก้าวเล็ก ๆ ก้าวนี้ของกระบวนการทั้งหมด — นี่คือความหมายของคำว่า driven (ขับเคลื่อน) ใน Test-Driven Development test เป็นตัวขับเคลื่อนการเขียน code โปรดักชัน ด้วยการมอบเป้าหมายเฉพาะหน้าให้คุณ
test แต่ละตัวตั้ง สมมติฐาน ว่าระบบควรหรืออาจทำงานอย่างไร ต่างจากวิทยาศาสตร์ที่มีโลกเพียงใบเดียวให้ทดลองภายใต้กฎชุดเดียว test รันกับระบบซอฟต์แวร์ที่เปลี่ยนแปลงได้ตลอดเวลา ดังนั้นสมมติฐานที่เป็นเท็จในซอฟต์แวร์ version หนึ่ง อาจเป็นจริงใน version อนาคต (และอาจกลับเป็นเท็จอีกครั้งหากเกิด regression!)
ตัวรัน test ส่วนใหญ่แสดง test ที่ล้มเหลวด้วยสีแดง จึงใช้สีนี้แทนสถานะที่มี test ล้มเหลว และสำคัญมากที่ test แต่ละตัวต้องถูกรัน — และเห็นว่ามันล้มเหลว — ก่อน ที่ code ซึ่งมันทดสอบจะถูกเขียนขึ้น เพราะมันง่ายเกินไปที่จะเผลอเขียน test ที่ไม่มีทางล้มเหลว หรือที่ไม่ได้ทดสอบสิ่งที่คุณคิดว่ามันทดสอบ James Shore เรียกขั้นก่อนหน้านี้ (ก่อนลงมือเขียน test) ว่า “Think” — เลือกว่าจะ test อะไรต่อไป ซึ่งเขามองว่าเป็นขั้นที่ยากที่สุดสำหรับมือใหม่ เพราะต้องออกแบบ interface ไปพร้อมกับตัดสินใจว่าพฤติกรรมถัดไปที่ควรมีคืออะไร
Green — ทำให้ test ผ่าน (Make the test pass)
หัวข้อที่มีชื่อว่า “Green — ทำให้ test ผ่าน (Make the test pass)”ทำให้ test ผ่านอย่างง่ายที่สุดเท่าที่จะทำได้ แล้ว หยุด ต้านทานสิ่งล่อใจที่จะเขียน code เพิ่ม เพื่อคาดเดาความต้องการในอนาคต (ดู YAGNI) หรือเพื่อทำให้ทั่วไปหรือ optimize เกินจำเป็น Fowler อธิบายขั้นนี้ว่าทำให้ทำงาน “simply but crudely” ได้ — บางครั้งถึงขั้น hardcode คำตอบไปก่อนก็ได้ ตราบใดที่ test ผ่านจริง เพราะความสวยงามของ design เป็นหน้าที่ของขั้นถัดไป
ประโยชน์สำคัญข้อหนึ่งของ TDD คือมันลดปริมาณ code ที่ต้องเขียนเพื่อให้ระบบทำงานตามข้อกำหนด code ที่เขียนน้อยลงหมายถึงเวลาเขียน code น้อยลง รวมถึงเวลาในการดูแลและ debug code ส่วนเกินนั้นน้อยลงด้วย ในแง่นี้ การใช้ TDD จึงเป็นวิธีนำหลักการแบบ lean มาใช้กับซอฟต์แวร์ เมื่อเขียน test ที่ล้มเหลวแล้วทำให้ผ่านได้ ก็เป็นจังหวะที่ดีที่จะ commit อย่างน้อยก็ในเครื่องของตัวเอง
Refactor — เมื่อสมควร (If warranted)
หัวข้อที่มีชื่อว่า “Refactor — เมื่อสมควร (If warranted)”Refactoring คือการเปลี่ยน design ของ code โดยไม่เปลี่ยนพฤติกรรม (ภายนอก) ของมัน ดูรายละเอียดเพิ่มเติมได้ที่ Refactor Mercilessly คุณควร refactor เฉพาะเมื่อ test ทั้งหมดเป็นสีเขียว เท่านั้น — วิธีนี้จะทำให้มั่นใจได้ว่า test ใดที่พังหลังการ refactor เป็นเพราะคุณทำพลาด ไม่ใช่เพราะระบบพังอยู่ก่อนแล้ว นี่คือกฎที่ Fowler ย้ำว่า “you only refactor with green tests, and any test failing indicates a mistake”
เป็นความคิดที่ดีที่จะ commit หลังการ refactor แต่ละก้าว โดยเฉพาะถ้ายังไม่ได้ commit ก่อนหน้านั้น แม้รอบ TDD จะทำงานเป็นก้าวเล็ก ๆ ราว 5–10 นาทีต่อ1 test (Shore ระบุว่านักพัฒนาที่คล่องมักวนรอบได้ 20–40 ครั้งต่อชั่วโมง) ก็ยังคุ้มที่จะ commit ในเครื่องต่อ1 test เผื่อวันหนึ่งพบว่าเดินมาผิดทาง จะได้ไม่เสียความคืบหน้ามากเกินไปเวลาย้อนกลับไปยัง commit ก่อนหน้า จำสุภาษิตตุรกีที่ว่า “ไม่ว่าคุณจะเดินมาผิดทางไกลแค่ไหน จงหันหลังกลับเดี๋ยวนี้”
เคล็ดลับ: ระหว่าง commit ให้ refactor code test หรือ code system under test อย่างใดอย่างหนึ่ง แต่อย่าทำทั้งสองพร้อมกัน วิธีนี้ทำให้ commit เล็กลง และทำให้ test คอยตรวจสอบการแก้ SUT ของคุณ ขณะที่ SUT ก็คอยตรวจสอบ code test ของคุณเช่นกัน
สุดท้าย พึงจำไว้ว่า refactoring เป็นขั้น ที่เลือกได้ หากกำลังไปได้สวยและการ refactor มีแต่จะทำให้ช้าลง มักเป็นความคิดที่ดีที่จะเดินหน้าต่ออีกสักหนึ่งหรือ2 test แล้วค่อยหยุดพินิจสิ่งที่ทำออกมาจริง ๆ ว่าจะทำให้เรียบง่ายขึ้นหรือดีขึ้นได้อีกหรือไม่
“ไม่ว่าคุณจะเดินมาผิดทางไกลแค่ไหน จงหันหลังกลับเดี๋ยวนี้”
— สุภาษิตตุรกี
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”สมมติต้องเขียน class ตะกร้าสินค้าอย่างง่าย ทำ TDD หนึ่งรอบเต็มด้วย xUnit ในภาษา C#
Red — เขียน test ก่อนที่ ShoppingCart จะมีอยู่จริง test นี้ compile ไม่ผ่านหรือรันแล้วล้มเหลวแน่นอน
public class ShoppingCartTests{ [Fact] public void Total_ควรรวมราคาสินค้าทั้งหมดในตะกร้า() { var cart = new ShoppingCart(); cart.AddItem("Book", 250m); cart.AddItem("Pen", 15m);
Assert.Equal(265m, cart.Total()); }}Green — เขียน code น้อยที่สุดเท่าที่จำเป็นให้ test ผ่าน ยังไม่ต้องสวย ไม่ต้องกัน edge case ที่ยังไม่มี test คลุม
public class ShoppingCart{ private readonly List<decimal> _prices = new();
public void AddItem(string name, decimal price) => _prices.Add(price);
// เขียนแบบตรงไปตรงมาที่สุดให้ test ผ่านก่อน ค่อย refactor ทีหลัง public decimal Total() { decimal sum = 0m; foreach (var p in _prices) { sum += p; } return sum; }}Refactor — test เขียวแล้ว จึงปรับ Total() ให้กระชับขึ้นด้วย LINQ โดยพฤติกรรมภายนอกยังเหมือนเดิมทุกประการ รัน test ซ้ำเพื่อยืนยันว่ายังผ่าน
public decimal Total() => _prices.Sum();จากนั้นวนกลับไปที่ Red อีกครั้งด้วย test ถัดไป เช่น กรณีตะกร้าว่างควรคืน 0m หรือกรณีใส่จำนวน (quantity) ของสินค้าแต่ละชิ้น
ประโยชน์และข้อควรระวัง
หัวข้อที่มีชื่อว่า “ประโยชน์และข้อควรระวัง”ประโยชน์
- ฟีดแบ็กเร็วและแม่นยำ — เพราะแต่ละก้าวเล็กมาก bug ที่เกิดขึ้นจะอยู่ใน code ไม่กี่บรรทัดล่าสุดเสมอ ทำให้หาสาเหตุง่ายกว่ามาก
- safety net สำหรับ refactor — มีชุด test เขียวคอยยืนยันว่า code ยังทำงานถูกต้องหลังปรับ design ลดความกลัวในการแตะ code เก่า
- ลด code ส่วนเกิน — เมื่อเขียนแค่พอให้ test ผ่าน ธรรมชาติของวงจรจะผลักดันให้ยึด YAGNI และ Keep It Simple โดยอัตโนมัติ
- เอกสารที่รันได้ — test ที่ผ่านคือข้อกำหนด (specification) ของระบบ ณ ขณะนั้น อ่านแล้วรู้ว่าระบบควรทำงานอย่างไรจริง ๆ
ข้อควรระวัง
- การข้ามขั้น Refactor คือความผิดพลาดที่พบบ่อยที่สุด Fowler เตือนว่าถ้าละเลยขั้นนี้ ผลลัพธ์จะกลายเป็นกอง code ปะติดปะต่อที่ทำงานได้แต่ดูแลยาก
- อย่า refactor ขณะ test แดง การปนสองโหมด (เพิ่ม feature + ปรับ design) พร้อมกันทำให้ debug ยากขึ้นมาก เพราะไม่รู้ว่าความล้มเหลวมาจากตรรกะใหม่หรือจากการ refactor
- test ที่ไม่เคยเห็นล้มเหลวเป็น test ที่เชื่อถือไม่ได้ ถ้าข้ามขั้น Red ไปเลยเขียน production code ก่อน อาจได้ test ที่ผ่านเสมอไม่ว่า code จะถูกหรือผิด ดู Poor Tests เป็นกลิ่นที่เกี่ยวข้อง
- ก้าวที่ใหญ่เกินไปทำลายจังหวะ ถ้า1 test ต้องใช้เวลานานกว่าจะเขียน production code ให้ผ่าน มักเป็นสัญญาณว่า test นั้นครอบคลุมมากเกินไปในคราวเดียว ควรตัดให้เล็กลง
- TDD ไม่ใช่ตัวแทนของการออกแบบสถาปัตยกรรมระดับสูง วงจร Red-Green-Refactor ทำงานได้ดีในระดับหน่วยย่อย (unit) แต่การตัดสินใจเชิงสถาปัตยกรรมยังต้องอาศัยการคิดแยกต่างหาก