TDD คืออะไร
เกริ่นนำหลังจากหายไปเกือบเดือน
สวัสดีครับทุกคน วันนี้ผมจะมาเล่าเรื่องเกี่ยวกับ Test-Driven Development (TDD) กันครับ ก็หายไปนานเลย(เกือบๆ 1 เดือน) แต่ก็ยังไม่ลืมเขียนบล็อกนะครับ
พอดีว่านั่งหางานแบบไฟลุกท่วม ก็เลยไม่ได้เว้นเวลามาเขียนบล็อกเลย ก็เลยมาอัพเดทบล็อกกันบ้าง วันนี้เราจะมาเริ่มต้นด้วย Test-Driven Development (TDD) กันครับ
พอดีที่ทำงานนั้นให้ความสำคัญกับ Test First (เดี๋ยวผมกับมาเขียนทีหลังฮ่าๆ) และ TDD อยู่ และผมที่เคยได้ยินเรื่องนี้เป็นครั้งแรกเพราะเคยเขียนโค้ด แบบเขียนโค้ดแล้วค่อยเขียน test มาตลอดเวลา 5555 ก็ถือว่าเป็นการเรียนรู้อะไรใหม่ๆน่ะครับ
โอเคครับ เกริ่นมาสักพักแล้วเรามาเริ่มเรื่องกันเลยครับ
Test-Driven Development (TDD) คืออะไร
เป็นแนวทางการพัฒนาที่เน้นการเขียน test ก่อนเขียนโค้ดจริง โดยมีขั้นตอนหลัก 3 ขั้นตอนที่เรียกว่า Red-Green-Refactor ดังนี้

- Red: เขียน test ก่อน แล้วรันให้ล้มเหลว (เพราะยังไม่มีโค้ดจริงรองรับ)
- Green: เขียนโค้ดให้ผ่าน test
- Refactor: ปรับปรุงโค้ดให้ดีขึ้นโดยยังคงผ่าน test
การเริ่มต้นแบบ TDD นั้นก็เหมือนกับการทำอะไรสักอย่างในชีวิตประจำวัน เราต้องกำหนดเป้าหมายก่อน (Red) พยายามทำให้ผ่านเป้าหมายนั้น (Green) และปรับปรุงให้ดีขึ้นเรื่อยๆ (Refactor)
โอเคครับ เราอาจจะเข้าใจ Concept คร่าวๆแล้ว แต่ว่าเรามาดูตัวอย่างกันดีกว่า
ตัวอย่างการเขียนโค้ดด้วย TDD
ก่อนอื่นเลยเดี๋ยวผมจะแนะนำเครื่องมือที่ผมจะใช้กันก่อนเลยครับ
- Bun ใช้เป็น Runtime ของ TypeScript ที่เราจะใช้ในการรันโค้ดกันครับ (เหมือนกับ Node.js)
- Bun Test ใช้เป็น Test Runner ของ TypeScript ที่เราจะใช้ในการรัน Test กันครับ (เหมือนกับ Jest)
โดยปกติแล้วเนี่ยเราอาจจะใช้ Jest ในการเขียน Test แต่ว่าเนื่องจากผมไม่อยากใช้เวลา Setup นานก็เลยใช้ Bun Test แทน
โอเคครับ นี้คือโจทย์ของเราวันนี้ครับ ระบบคำนวณค่าอาหาร + ค่าทิป
- รับค่าอาหาร (billAmount) และเปอร์เซ็นต์ทิป (tipRate)
- คำนวณ ยอดรวม ตามสูตร:
billAmount + (billAmount × tipRate ÷ 100)
- ถ้าไม่ได้เลือกเปอร์เซ็นต์ทิป ให้ใช้ค่าเริ่มต้น 10%
เรามาเริ่มที่เขียน Test กันก่อนเลยครับ
import { calculateTotal } from "./calculateTotal";
describe("Calculate Tip Success Case", () => {
it("should return total amount including 20% tip", () => {
const bill = 100;
const tipPercentage = 20;
const result = 120;
const expected = calculateTip(bill, tipPercentage);
expect(expected).toBe(result);
});
});เราเริ่มจากการเขียน Test ก่อนเสมอ โดยเราจะเขียน Test ที่เราคาดหวังได้ก่อน แล้วจึงเขียนโค้ดให้ผ่าน Test นั้น
export function calculateTotal(billAmount: number, tipRate: number): number {
return billAmount + (billAmount * tipRate) / 100;
}ลองรัน Test ดูกันครับ โดยใช้คำสั่ง bun test
ถ้า Test ผ่านเราจะได้ผลลัพธ์ดังนี้
bun test v1.1.43 (76800b04)
src/calculatetip/tip.test.ts:
✓ Calculate Tip Success Case > should return total amount including 20% tip
1 pass
0 fail
1 expect() calls
Ran 1 tests across 1 files. [9.00ms]เราจะเขียนโค้ดให้ผ่าน Test ที่เราเขียนไว้ก่อนหน้านี้ แล้วจึง Refactor โค้ดให้ดีขึ้น
export function calculateTotal(billAmount: number, tipRate: number): number {
const tipAmount = (billAmount * tipRate) / 100;
const total = billAmount + tipAmount;
return total;
}เราจะ Refactor โค้ดให้ดีขึ้นโดยยังคงผ่าน Test ที่เราเขียนไว้ก่อนหน้านี้
ต่อไปเราจะมาลองเพิ่ม Test ใหม่เพื่อเช็คว่าเมื่อเราไม่ใส่เปอร์เซ็นต์ทิปเข้าไปและถ้าค่าอาหารเป็น 0 จะคำนวณถูกต้องหรือไม่
describe("Calculate Tip Default Case", () => {
it("should return total amount including 10% tip by default", () => {
const bill = 100;
const result = 110;
const expected = calculateTip(bill);
expect(expected).toBe(result);
});
it("should return 0 if bill is 0", () => {
const bill = 0;
const result = 0;
const expected = calculateTip(bill);
expect(expected).toBe(result);
});
});เราจะเขียน Test ใหม่เพื่อเช็คว่าเมื่อเราไม่ใส่เปอร์เซ็นต์ทิปเข้าไป จะคำนวณถูกต้องหรือไม่ ลองรัน Test ดูกันครับ
bun test v1.1.43 (76800b04)
src/calculatetip/tip.test.ts:
✓ Calculate Tip Success Case > should return total amount including 20% tip
25 | const bill = 100;
26 | const result = 110;
27 |
28 | const expected = calculateTip(bill);
29 |
30 | expect(expected).toBe(result);
^
error: expect(received).toBe(expected)
Expected: 110
Received: NaN
at <anonymous>
✗ Calculate Tip Success Case > should use default tip percentage of 10% if not provided
✓ Calculate Tip Success Case > should return 0 if bill is 0
2 pass
1 fail
3 expect() calls
Ran 3 tests across 1 files. [9.00ms]
จากผลลัพธ์ที่ได้เราจะเห็นว่า Test ที่เราเขียนไว้ก่อนหน้านี้มี Test ที่ผ่านและ Test ที่ไม่ผ่าน เราจะ Refactor โค้ดให้ผ่าน Test ที่เราเขียนไว้ก่อนหน้านี้
export function calculateTotal(
billAmount: number,
tipRate: number = 10
): number {
const tipAmount = (billAmount * tipRate) / 100;
const total = billAmount + tipAmount;
return total;
}ลองรัน Test อีกครั้งดูกันครับ
bun test v1.1.43 (76800b04)
src/calculatetip/tip.test.ts:
✓ Calculate Tip Success Case > should return total amount including 20% tip
✓ Calculate Tip Success Case > should use default tip percentage of 10% if not provided
✓ Calculate Tip Success Case > should return 0 if bill is 0
3 pass
0 fail
3 expect() calls
Ran 3 tests across 1 files. [9.00ms]เราจะเห็นว่า Test ทั้งหมดผ่าน และโค้ดของเราผ่าน Test ทั้งหมด
ต่อไปเราจะมาลองสร้าง Alternative Test ใหม่เพื่อเช็คว่าเมื่อเราใส่ค่าที่ผิดเข้าไป จะคำนวณถูกต้องหรือไม่
describe("Calculate Tip Failure Case", () => {
it("should throw an error if bill is negative", () => {
const bill = -100;
const tipPercentage = 20;
expect(() => calculateTip(bill, tipPercentage)).toThrow();
});
it("should throw an error if tip percentage is negative", () => {
const bill = 100;
const tipPercentage = -20;
expect(() => calculateTip(bill, tipPercentage)).toThrow();
});
});เราจะเขียน Test ใหม่เพื่อเช็คว่าเมื่อเราใส่ค่าที่ผิดเข้าไป จะคำนวณถูกต้องหรือไม่ ลองรัน Test ดูกันครับ
bun test v1.1.43 (76800b04)
src/calculatetip/tip.test.ts:
✓ Calculate Tip Success Case > should return total amount including 20% tip
✓ Calculate Tip Success Case > should use default tip percentage of 10% if not provided
✓ Calculate Tip Success Case > should return 0 if bill is 0
44 | describe("Calculate Tip Failure Case", () => {
45 | it("should throw an error if bill is negative", () => {
46 | const bill = -100;
47 | const tipPercentage = 20;
48 |
49 | expect(() => calculateTip(bill, tipPercentage)).toThrow();
^
error: expect(received).toThrow()
Received function did not throw
Received value: -120
at <anonymous>
✗ Calculate Tip Failure Case > should throw an error if bill is negative
51 |
52 | it("should throw an error if tip percentage is negative", () => {
53 | const bill = 100;
54 | const tipPercentage = -20;
55 |
56 | expect(() => calculateTip(bill, tipPercentage)).toThrow();
^
error: expect(received).toThrow()
Received function did not throw
Received value: 80
at <anonymous>
✗ Calculate Tip Failure Case > should throw an error if tip percentage is negative
3 pass
2 fail
5 expect() calls
Ran 5 tests across 1 files. [10.00ms]
จากผลลัพธ์ที่ได้เราจะเห็นว่า Test ที่เราเขียนเพิ่มเข้ามาไม่ผ่าน และเราจะ Refactor โค้ดให้ผ่าน Test ที่เราเขียนไว้ก่อนหน้านี้
export function calculateTotal(
billAmount: number,
tipRate: number = 10
): number {
if (billAmount < 0 || tipRate < 0) {
throw new Error("Bill amount and tip rate must be positive");
}
const tipAmount = (billAmount * tipRate) / 100;
const total = billAmount + tipAmount;
return total;
}ลองรัน Test อีกครั้งดูกันครับ
bun test v1.1.43 (76800b04)
src/calculatetip/tip.test.ts:
✓ Calculate Tip Success Case > should return total amount including 20% tip
✓ Calculate Tip Success Case > should use default tip percentage of 10% if not provided
✓ Calculate Tip Success Case > should return 0 if bill is 0
✓ Calculate Tip Failure Case > should throw an error if bill is negative
✓ Calculate Tip Failure Case > should throw an error if tip percentage is negative
5 pass
0 fail
5 expect() calls
Ran 5 tests across 1 files. [10.00ms]เราจะเห็นว่า Test ทั้งหมดผ่าน และโค้ดของเราผ่าน Test ทั้งหมด เย้ๆ
นี้คือตาราง Test ทั้งหมดที่เราเขียนไว้ครับ
| Test Case | Input | Expected Output |
|---|---|---|
| ✅ คำนวณยอดรวมถูกต้อง | 100, 20 | 120 |
| ✅ คำนวณยอดรวมถูกต้อง | 100 | 110 |
| ✅ คำนวณยอดรวมถูกต้อง | 0 | 0 |
| ✅ คำนวณผิดพลาดเมื่อใส่ค่าผิด | -100, 20 | Error |
| ✅ คำนวณผิดพลาดเมื่อใส่ค่าผิด | 100, -20 | Error |
ผมกับ TDD และสรุป
เราจะเห็นว่าการเขียน Test ก่อนเขียนโค้ดจริงๆนั้นจะช่วยให้เราเขียนโค้ดได้ดีขึ้น และเราสามารถ Refactor โค้ดได้ง่ายขึ้น และเราสามารถเช็คว่าโค้ดของเราทำงานถูกต้องหรือไม่
สำหรับตัวผมแล้วผมไม่ค่อยได้เขียน Test นัก คงปรับ mindset ให้เหมาะกับ TDD ด้วยวิธีนี้
- เขียน test ก่อนเขียนโค้ดจริง
- ทำให้ test เข้าใจง่าย (ไม่ซับซ้อนเกินไป)
- ทดสอบเฉพาะ logic สำคัญ (ไม่ต้องทดสอบทุกอย่าง)
- ใช้ Mocks & Stubs เมื่อจำเป็น
ถ้าคุณยังไม่เคยเขียน Test ก่อนเขียนโค้ด ลองเริ่มต้นด้วย TDD ดูนะครับ แล้วก็มาเขียนโค้ดกัน เราจะเจอกันใหม่ในบล็อคต่อไปนะครับ ขอบคุณที่มาอ่านบล็อคผมครับ