Test Driven Development
เขียน test ก่อน แล้วให้ test ขับเคลื่อนการเขียน code
คืออะไร
หัวข้อที่มีชื่อว่า “คืออะไร”Test Driven Development (TDD) หรือที่รู้จักในชื่อ Test Driven Design เป็นวินัยการเขียน code ที่ใช้ test เป็นตัวนิยามและยืนยันพฤติกรรมของซอฟต์แวร์ ก่อนที่จะมี code จริงเกิดขึ้นเลยด้วยซ้ำ มันมีลักษณะเด่นเป็นวงจรสั้น ๆ ที่เรียกว่า Red — Green — Refactor ซึ่งวนซ้ำเพื่อค่อย ๆ สร้างซอฟต์แวร์ที่ทำงานได้ขึ้นมาทีละนิด
Kent Beck เป็นผู้ทำให้ TDD เป็นที่รู้จักในช่วงปลายทศวรรษ 1990 ในฐานะส่วนหนึ่งของ Extreme Programming (XP) เขาเล่าว่าไอเดียนี้ไม่ใช่ของใหม่ทั้งหมด แต่ “ค้นพบใหม่” จากตำราเขียนโปรแกรมเก่าที่แนะนำให้เขียนกรณีทดสอบไว้ก่อนเขียน code แล้วนำมาปรับใช้กับ framework xUnit ที่เขาสร้างขึ้น
ประเด็นสำคัญที่มักถูกมองข้ามคือ TDD ไม่ใช่แค่ “เทคนิคการ test” แต่เป็น เทคนิคการออกแบบ ด้วย Martin Fowler ชี้ว่าการบังคับให้เขียน test ก่อนเขียน code จริง ทำให้ผู้พัฒนาต้องคิดถึง วิธีใช้งาน code ก่อนคิดถึง วิธีสร้าง มัน ผลลัพธ์คือการแยก interface ออกจาก implementation ได้ดีขึ้นโดยธรรมชาติ — สิ่งที่นักพัฒนาจำนวนมากทำได้ยากหากออกแบบ code ก่อนแล้วค่อยเขียน test ทีหลัง
ควรเข้าใจด้วยว่า TDD เน้นที่ระดับ unit เป็นหลัก จึงไม่ใช่กลยุทธ์การ test ที่ครบถ้วนในตัวเอง ทีมงานยังต้องมี integration test, system test และ acceptance test ควบคู่กันไปเพื่อครอบคลุมพฤติกรรมระดับที่กว้างกว่าหน่วย code เดี่ยว ๆ — แนวคิดนี้ต่อยอดไปสู่ Behavior-Driven Development ซึ่งขยับ test ให้ใกล้ภาษาของ domain และผู้ใช้งานมากขึ้น
ทำอย่างไร
หัวข้อที่มีชื่อว่า “ทำอย่างไร”ก่อนเริ่มวงจร Red-Green-Refactor แรก Fowler แนะนำให้ทำ test list เสียก่อน คือร่างรายการกรณีทดสอบที่คาดว่าจะต้องมีทั้งหมดคร่าว ๆ แล้วเลือกมาทำทีละข้อ การจัดลำดับว่าจะทดสอบอะไรก่อนหลังเป็นทักษะสำคัญ เพราะ test ที่เลือกมาก่อนควรพาไปสู่จุดออกแบบที่สำคัญที่สุดได้เร็วที่สุด
ขั้นที่ 1 — เขียน test ที่ล้มเหลว Red
หัวข้อที่มีชื่อว่า “ขั้นที่ 1 — เขียน test ที่ล้มเหลว Red”เขียน test ที่ ล้มเหลว สำหรับ feature หรือพฤติกรรมใหม่หนึ่งอย่าง แล้วรันมันเพื่อยืนยันว่ามันล้มเหลวจริง (ไม่ใช่ล้มเหลวเพราะ test เขียนผิด) ตัวรัน test (test runner) ควรแสดงสัญญาณสี แดง (RED) test ที่ดีมักเขียนตามรูปแบบ Arrange-Act-Assert (AAA): จัดเตรียมข้อมูล/สถานะ (Arrange) เรียกพฤติกรรมที่ต้องการทดสอบ (Act) แล้วตรวจผลลัพธ์ (Assert) — รูปแบบนี้ทำให้ test อ่านง่าย สื่อความหมายในตัวเอง และตรวจสอบเพียงเรื่องเดียวต่อ1 test
ขั้นที่ 2 — ทำให้ test ผ่าน Green
หัวข้อที่มีชื่อว่า “ขั้นที่ 2 — ทำให้ test ผ่าน Green”แก้ไข system under test ให้ น้อยที่สุด เพื่อให้ test ผ่าน โดยเฉพาะเมื่อยังใหม่กับ TDD ขั้นนี้ควรเป็น “การเปลี่ยนแปลงที่ง่ายที่สุดที่ทำให้ test ผ่าน” อย่างแท้จริง Kent Beck อธิบายเทคนิคย่อยไว้สามแบบที่ใช้สลับกันได้:
- Fake It — hard-code ค่าที่คาดหวังลงไปตรง ๆ เพื่อไปให้ถึง GREEN เร็วที่สุดก่อน
- Obvious Implementation — ถ้ามั่นใจว่ารู้คำตอบที่ถูกต้องอยู่แล้ว เขียนมันลงไปได้เลยโดยไม่ต้อง fake
- Triangulation — เมื่อ Fake It เริ่มดูขัดหูขัดตา ให้เพิ่ม test ที่สองซึ่งมี input ต่างออกไป บังคับให้ code ต้องถูก generalize จากค่า hard-code เป็นตรรกะจริง
เมื่อ test ผ่านแล้ว ตัวรัน test ควรขึ้นสัญญาณ เขียว (GREEN)
จากแดงสู่เขียว: ทำให้ผ่านอย่างง่ายที่สุดก่อน (แม้จะ hard-code)
// RED — test นี้ล้มเหลว เพราะ FullName ยังไม่มีpublic void ReturnsFullNameFromParts(){ var person = new Person("Ada", "Lovelace"); Assert.Equal("Ada Lovelace", person.FullName);}
// GREEN — Fake It: hard-code ให้ผ่านไปก่อน แล้วค่อยไปทำให้ถูกต้องจริงใน test ถัดไปpublic string FullName => "Ada Lovelace";ขั้นที่ 3 — refactor Refactor
หัวข้อที่มีชื่อว่า “ขั้นที่ 3 — refactor Refactor”ขั้นที่สองมุ่งไปที่การไปให้ถึง GREEN อย่างเร็วและง่ายที่สุด บางครั้งจึงเหลือความซ้ำซ้อนหรือ code ที่ไม่งดงามซึ่งต้องเก็บกวาด มองหา code smell หรือ technical debt ที่เห็นได้ชัด โดยเฉพาะความซ้ำซ้อน แล้วrefactorในขั้นนี้ ต้องรันชุด test ต่อไปเรื่อย ๆ เพื่อให้แน่ใจว่ามันยัง GREEN อยู่ — การ refactor ไม่ควรเปลี่ยน สิ่งที่ code ทำ แต่เปลี่ยนเพียง วิธีที่ มันทำ และ test คือสิ่งที่คอยรับประกันข้อนี้
Fowler เตือนว่าข้อผิดพลาดที่พบบ่อยที่สุดของ TDD คือการ ข้ามขั้น refactor ไปเลย เพราะ test ผ่านแล้วก็รู้สึกว่า “เสร็จ” ทั้งที่ code ยังไม่ถูกจัดระเบียบ ผลคือได้ code ที่มี test คุ้มครองก็จริง แต่คุณภาพการออกแบบยังแย่อยู่ดี
เมื่อพอใจกับ code แล้ว ก็ถึงเวลาไปยัง test ถัดไปใน list ก่อนหน้านั้น โดยเฉพาะถ้าใช้ระบบควบคุม version แบบเบาหรือทำงานในเบรนช์ของตัวเอง ลองพิจารณา commit หลังแต่ละขั้น คุณอาจคิดถึง Red-Green-Refactor-Commit ว่าเป็น Red-Green-Refactor แบบใหม่ของ TDD ก็ได้ ที่จริงคุณอาจอยาก commit ก่อน refactor ด้วยซ้ำ โดยเฉพาะเมื่อกำลังจะเริ่มการ refactor ครั้งใหญ่
flowchart TD
Start[เริ่มหยิบ test ถัดไปจาก test list]
WriteTest[เขียน test ใหม่ที่ยังไม่ผ่าน]
RunRed[รัน test]
CheckRed{test ล้มเหลวจริงไหม}
WriteCode[เขียน code ขั้นต่ำสุดให้ผ่าน]
RunGreen[รัน test อีกครั้ง]
CheckGreen{test ผ่านทั้งชุดไหม}
Refactor[refactor code และ test]
Commit[Commit การเปลี่ยนแปลง]
Start --> WriteTest --> RunRed --> CheckRed
CheckRed -- ไม่ล้มเหลว แก้ test --> WriteTest
CheckRed -- ล้มเหลวแบบ Red --> WriteCode
WriteCode --> RunGreen --> CheckGreen
CheckGreen -- ยังไม่ผ่าน --> WriteCode
CheckGreen -- ผ่านแบบ Green --> Refactor
Refactor --> Commit
Commit --> Start
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่างต่อไปนี้แสดงการใช้ Triangulation เพื่อบังคับให้ code ถูก generalize จากค่า hard-code ไปเป็นตรรกะจริง — แนวทางเดียวกับที่ Microsoft Learn สาธิตไว้ในเอกสาร TDD ของ Visual Studio โดยเริ่มจาก stub ว่างแล้วค่อย ๆ ขยายจนกว่า test ทั้งหมดจะบังคับให้เกิดตรรกะที่ถูกต้อง
// test ที่ 1 — RED แล้ว GREEN ด้วยการ Fake It[TestMethod]public void Add_OnePlusOne_ReturnsTwo(){ // Arrange var calculator = new Calculator();
// Act var result = calculator.Add(1, 1);
// Assert Assert.AreEqual(2, result);}
// GREEN แบบ Fake It — ยังไม่ใช่ตรรกะจริงpublic int Add(int a, int b) => 2;
// test ที่ 2 — Triangulation บังคับให้ Fake It อยู่ไม่ได้อีกต่อไป[TestMethod]public void Add_TwoPlusThree_ReturnsFive(){ var calculator = new Calculator(); var result = calculator.Add(2, 3); Assert.AreEqual(5, result);}
// GREEN ที่แท้จริง — ตอนนี้ต้อง generalize แล้วpublic int Add(int a, int b) => a + b;เมื่อครบ2 test นี้ ค่า hard-code เดิม 2 ไม่สามารถทำให้ test ทั้งสองผ่านพร้อมกันได้อีก ทำให้การ generalize เป็น a + b เป็นทางเลือกที่ง่ายที่สุดที่ทำให้ทั้งชุดกลับมาเป็น GREEN — นี่คือแก่นของ Triangulation: ให้ test เป็นตัวบีบบังคับการออกแบบ ไม่ใช่ให้ผู้เขียน code คาดเดาความทั่วไปของ solution ไปเองล่วงหน้า
ประโยชน์และข้อควรระวัง
หัวข้อที่มีชื่อว่า “ประโยชน์และข้อควรระวัง”ประโยชน์
- ฟีดแบ็กเร็ว — รู้ทันทีว่าพฤติกรรมใดพังจากการเปลี่ยนแปลงล่าสุด แทนที่จะไปเจอตอน QA หรือ production
- ตาข่ายนิรภัยสำหรับการ refactor — เมื่อมีชุด test ที่ครอบคลุม การปรับโครงสร้าง code ทำได้อย่างมั่นใจมากขึ้น
- ผลักดันการออกแบบที่ดีขึ้น — code ที่ test ได้ง่ายมักมี unit เล็กลง coupling หลวมขึ้น และมี interface ที่ชัดเจนขึ้นโดยธรรมชาติ
- เอกสารที่รันได้จริง — ชุด test ทำหน้าที่เป็นตัวอย่างการใช้งานที่เป็นปัจจุบันเสมอ ต่างจากเอกสารข้อความที่ล้าสมัยได้ง่าย
ข้อควรระวัง
- งานวิจัยเชิง meta-analysis (เช่นงานปี 2013 ที่ Wikipedia อ้างอิง) พบว่า TDD มีผลบวกเล็กน้อยต่อคุณภาพภายนอกของ code แต่แทบไม่มีผลต่อ productivity โดยรวม — TDD ไม่ใช่ยาวิเศษที่รับประกันผลลัพธ์เสมอไป
- การข้ามขั้น refactor คือกับดักที่พบบ่อยที่สุด ทำให้ได้ code ที่มี test คุ้มครองแต่โครงสร้างยังไม่ดี
- การพึ่งพา mock/stub มากเกินไปอาจทำให้ test ผูกติดกับรายละเอียด implementation จนกลายเป็น test เปราะ (fragile) ที่พังทุกครั้งที่ refactor แม้พฤติกรรมภายนอกจะไม่เปลี่ยน
- TDD เน้นระดับ unit เป็นหลัก ยังต้องมี integration/system/acceptance test เสริมเพื่อครอบคลุมพฤติกรรมข้าม component, ฐานข้อมูล หรือระบบกระจาย
- การลำดับ test (test list) และเลือกกรณีทดสอบที่ “พาไปสู่จุดออกแบบสำคัญ” เป็นทักษะที่ต้องฝึก มือใหม่มักรู้สึกว่า TDD ทำให้ช้าลงในช่วงแรก ก่อนที่จะคุ้นเคย
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Red, Green, Refactor
- Refactor Regularly
- Continuous Integration
- Behavior-Driven Development
- Dependency Injection
- YAGNI
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/practices/test-driven-development
- Test Driven Development — Martin Fowler’s Bliki
- Test-driven development — Wikipedia
- Test-driven development — Visual Studio, Microsoft Learn
- Test-Driven Development: By Example — Kent Beck (หนังสือที่บัญญัติเทคนิค Fake It, Obvious Implementation และ Triangulation)