เชี่ยวชาญการพัฒนาโดยใช้การทดสอบเป็นตัวนำ (TDD) ใน JavaScript คู่มือฉบับสมบูรณ์นี้ครอบคลุมวงจร Red-Green-Refactor การนำไปใช้จริงด้วย Jest และแนวทางปฏิบัติที่ดีที่สุดสำหรับการพัฒนาสมัยใหม่
การพัฒนาโดยใช้การทดสอบเป็นตัวนำ (TDD) ใน JavaScript: คู่มือฉบับสมบูรณ์สำหรับนักพัฒนาทั่วโลก
ลองจินตนาการถึงสถานการณ์นี้: คุณได้รับมอบหมายให้แก้ไขโค้ดส่วนสำคัญในระบบเก่าขนาดใหญ่ คุณรู้สึกหวาดหวั่น การเปลี่ยนแปลงของคุณจะทำให้ส่วนอื่นพังหรือไม่? คุณจะแน่ใจได้อย่างไรว่าระบบยังคงทำงานได้ตามที่ตั้งใจไว้? ความกลัวต่อการเปลี่ยนแปลงนี้เป็นปัญหาที่พบบ่อยในการพัฒนาซอฟต์แวร์ ซึ่งมักนำไปสู่ความคืบหน้าที่ล่าช้าและแอปพลิเคชันที่เปราะบาง แต่จะเป็นอย่างไรถ้ามีวิธีสร้างซอฟต์แวร์ด้วยความมั่นใจ สร้างตาข่ายนิรภัยที่ดักจับข้อผิดพลาดก่อนที่จะไปถึงขั้นโปรดักชัน? นี่คือคำมั่นสัญญาของการพัฒนาโดยใช้การทดสอบเป็นตัวนำ (Test-Driven Development หรือ TDD)
TDD ไม่ใช่แค่เทคนิคการทดสอบ แต่เป็นแนวทางที่มีวินัยในการออกแบบและพัฒนาซอฟต์แวร์ มันกลับด้านโมเดล "เขียนโค้ดแล้วค่อยทดสอบ" แบบดั้งเดิม ด้วย TDD คุณจะเขียนเทสต์ที่ล้มเหลว ก่อนที่คุณจะเขียนโค้ดโปรดักชันเพื่อให้เทสต์นั้นผ่าน การกลับด้านง่ายๆ นี้ส่งผลกระทบอย่างลึกซึ้งต่อคุณภาพของโค้ด การออกแบบ และการบำรุงรักษา คู่มือนี้จะให้มุมมองที่ครอบคลุมและปฏิบัติได้จริงเกี่ยวกับการนำ TDD ไปใช้ใน JavaScript ซึ่งออกแบบมาสำหรับนักพัฒนามืออาชีพทั่วโลก
การพัฒนาโดยใช้การทดสอบเป็นตัวนำ (TDD) คืออะไร?
โดยแก่นแท้แล้ว การพัฒนาโดยใช้การทดสอบเป็นตัวนำคือกระบวนการพัฒนาที่อาศัยการทำซ้ำในวงจรการพัฒนาที่สั้นมาก แทนที่จะเขียนฟีเจอร์แล้วค่อยทดสอบ TDD ยืนยันว่าต้องเขียนเทสต์ก่อน เทสต์นี้จะล้มเหลวอย่างแน่นอนเพราะฟีเจอร์ยังไม่มีอยู่จริง จากนั้นหน้าที่ของนักพัฒนาคือการเขียนโค้ดที่ง่ายที่สุดเท่าที่จะเป็นไปได้เพื่อให้เทสต์นั้นผ่าน เมื่อผ่านแล้ว โค้ดจะถูกทำความสะอาดและปรับปรุงให้ดีขึ้น วงจรพื้นฐานนี้เรียกว่าวงจร "Red-Green-Refactor"
จังหวะของ TDD: Red-Green-Refactor
วงจรสามขั้นตอนนี้คือหัวใจของ TDD การทำความเข้าใจและฝึกฝนจังหวะนี้เป็นพื้นฐานสำคัญในการฝึกฝนเทคนิคนี้ให้เชี่ยวชาญ
- 🔴 Red — เขียนเทสต์ที่ล้มเหลว: คุณเริ่มต้นด้วยการเขียนเทสต์อัตโนมัติสำหรับฟังก์ชันการทำงานใหม่ เทสต์นี้ควรกำหนดสิ่งที่คุณ ต้องการ ให้โค้ดทำ เนื่องจากคุณยังไม่ได้เขียนโค้ดใดๆ เพื่อใช้งาน เทสต์นี้จึงรับประกันว่าจะล้มเหลว เทสต์ที่ล้มเหลวไม่ใช่ปัญหา แต่เป็นความคืบหน้า มันพิสูจน์ว่าเทสต์ทำงานอย่างถูกต้อง (สามารถล้มเหลวได้) และตั้งเป้าหมายที่ชัดเจนและเป็นรูปธรรมสำหรับขั้นตอนต่อไป
- 🟢 Green — เขียนโค้ดที่ง่ายที่สุดเพื่อให้ผ่าน: ตอนนี้เป้าหมายของคุณมีเพียงหนึ่งเดียว: ทำให้เทสต์ผ่าน คุณควรเขียนโค้ดโปรดักชันในปริมาณที่น้อยที่สุดที่จำเป็นเพื่อเปลี่ยนเทสต์จากสีแดงเป็นสีเขียว นี่อาจรู้สึกขัดกับสัญชาตญาณ โค้ดอาจจะยังไม่สวยงามหรือมีประสิทธิภาพ แต่นั่นไม่เป็นไร จุดเน้นในที่นี้คือการทำตามข้อกำหนดที่กำหนดโดยเทสต์เท่านั้น
- 🔵 Refactor — ปรับปรุงโค้ด: เมื่อคุณมีเทสต์ที่ผ่านแล้ว คุณก็มีตาข่ายนิรภัย คุณสามารถทำความสะอาดและปรับปรุงโค้ดของคุณได้อย่างมั่นใจโดยไม่ต้องกลัวว่าจะทำให้ฟังก์ชันการทำงานเสียหาย นี่คือขั้นตอนที่คุณจัดการกับ code smells, ลบโค้ดที่ซ้ำซ้อน, ปรับปรุงความชัดเจน และเพิ่มประสิทธิภาพ คุณสามารถรันชุดเทสต์ของคุณได้ตลอดเวลาในระหว่างการปรับปรุงโค้ดเพื่อให้แน่ใจว่าคุณไม่ได้สร้างข้อผิดพลาดใหม่ (regression) ขึ้นมา หลังจากปรับปรุงโค้ดแล้ว เทสต์ทั้งหมดควรยังคงเป็นสีเขียว
เมื่อวงจรสำหรับฟังก์ชันการทำงานเล็กๆ หนึ่งส่วนเสร็จสมบูรณ์ คุณก็เริ่มต้นใหม่อีกครั้งด้วยเทสต์ที่ล้มเหลวใหม่สำหรับส่วนถัดไป
กฎสามข้อของ TDD
Robert C. Martin (มักรู้จักในชื่อ "Uncle Bob") ซึ่งเป็นบุคคลสำคัญในขบวนการซอฟต์แวร์ Agile ได้กำหนดกฎง่ายๆ สามข้อที่รวบรวมวินัยของ TDD:
- คุณจะไม่เขียนโค้ดโปรดักชันใดๆ เว้นแต่จะเป็นการทำให้ unit test ที่ล้มเหลวผ่าน
- คุณจะไม่เขียน unit test เกินกว่าที่จำเป็นเพื่อให้มันล้มเหลว และการคอมไพล์ไม่ผ่านถือเป็นความล้มเหลว
- คุณจะไม่เขียนโค้ดโปรดักชันเกินกว่าที่จำเป็นเพื่อให้ unit test ที่ล้มเหลวเพียงหนึ่งเดียวผ่าน
การปฏิบัติตามกฎเหล่านี้บังคับให้คุณเข้าสู่วงจร Red-Green-Refactor และทำให้มั่นใจได้ว่า 100% ของโค้ดโปรดักชันของคุณถูกเขียนขึ้นเพื่อตอบสนองความต้องการที่เฉพาะเจาะจงและผ่านการทดสอบแล้ว
ทำไมคุณควรนำ TDD มาใช้? กรณีศึกษาทางธุรกิจในระดับโลก
แม้ว่า TDD จะให้ประโยชน์มหาศาลแก่นักพัฒนาแต่ละคน แต่พลังที่แท้จริงของมันจะปรากฏในระดับทีมและธุรกิจ โดยเฉพาะอย่างยิ่งในสภาพแวดล้อมการทำงานที่กระจายตัวอยู่ทั่วโลก
- เพิ่มความมั่นใจและความเร็ว: ชุดเทสต์ที่ครอบคลุมทำหน้าที่เป็นตาข่ายนิรภัย สิ่งนี้ช่วยให้ทีมสามารถเพิ่มฟีเจอร์ใหม่หรือปรับปรุงโค้ดที่มีอยู่ได้อย่างมั่นใจ นำไปสู่ความเร็วในการพัฒนาที่ยั่งยืนสูงขึ้น คุณใช้เวลาน้อยลงในการทดสอบ regression แบบแมนนวลและการดีบัก และมีเวลามากขึ้นในการส่งมอบคุณค่า
- การออกแบบโค้ดที่ดีขึ้น: การเขียนเทสต์ก่อนบังคับให้คุณต้องคิดว่าโค้ดของคุณจะถูกใช้งานอย่างไร คุณคือผู้บริโภคคนแรกของ API ของคุณเอง สิ่งนี้นำไปสู่ซอฟต์แวร์ที่ออกแบบได้ดีขึ้นโดยธรรมชาติ ด้วยโมดูลที่เล็กลง มีจุดประสงค์ชัดเจนขึ้น และมีการแบ่งแยกหน้าที่ (separation of concerns) ที่ชัดเจนกว่า
- เอกสารที่มีชีวิต: สำหรับทีมระดับโลกที่ทำงานข้ามเขตเวลาและวัฒนธรรมที่แตกต่างกัน เอกสารที่ชัดเจนเป็นสิ่งสำคัญอย่างยิ่ง ชุดเทสต์ที่เขียนอย่างดีเป็นรูปแบบหนึ่งของเอกสารที่มีชีวิตและสามารถทำงานได้จริง นักพัฒนาคนใหม่สามารถอ่านเทสต์เพื่อทำความเข้าใจได้อย่างแม่นยำว่าโค้ดส่วนนั้นๆ ควรทำอะไรและทำงานอย่างไรในสถานการณ์ต่างๆ ซึ่งแตกต่างจากเอกสารแบบดั้งเดิมตรงที่มันจะไม่มีวันล้าสมัย
- ลดต้นทุนรวมในการเป็นเจ้าของ (TCO): บั๊กที่ถูกตรวจพบในช่วงต้นของวงจรการพัฒนามีค่าใช้จ่ายในการแก้ไขถูกกว่าอย่างทวีคูณเมื่อเทียบกับบั๊กที่พบในขั้นโปรดักชัน TDD สร้างระบบที่แข็งแกร่งซึ่งง่ายต่อการบำรุงรักษาและขยายในอนาคต ซึ่งช่วยลด TCO ของซอฟต์แวร์ในระยะยาว
การตั้งค่าสภาพแวดล้อม TDD สำหรับ JavaScript ของคุณ
ในการเริ่มต้น TDD ใน JavaScript คุณต้องมีเครื่องมือสองสามอย่าง ซึ่งระบบนิเวศของ JavaScript สมัยใหม่มีตัวเลือกที่ยอดเยี่ยมให้เลือก
องค์ประกอบหลักของ Testing Stack
- Test Runner: โปรแกรมที่ค้นหาและรันเทสต์ของคุณ มันให้โครงสร้าง (เช่น บล็อก `describe` และ `it`) และรายงานผลลัพธ์ Jest และ Mocha เป็นสองตัวเลือกที่ได้รับความนิยมสูงสุด
- Assertion Library: เครื่องมือที่ให้ฟังก์ชันสำหรับตรวจสอบว่าโค้ดของคุณทำงานตามที่คาดไว้หรือไม่ ช่วยให้คุณสามารถเขียนคำสั่งเช่น `expect(result).toBe(true)` ได้ Chai เป็นไลบรารียอดนิยมที่แยกออกมา ในขณะที่ Jest มีไลบรารี assertion ที่ทรงพลังรวมอยู่ด้วย
- Mocking Library: เครื่องมือสำหรับสร้าง "ของปลอม" ของส่วนที่ต้องพึ่งพา เช่น การเรียก API หรือการเชื่อมต่อฐานข้อมูล สิ่งนี้ช่วยให้คุณสามารถทดสอบโค้ดของคุณแบบแยกส่วนได้ Jest มีความสามารถในการทำ mocking ที่ยอดเยี่ยมในตัว
เพื่อความเรียบง่ายและครบวงจร เราจะใช้ Jest สำหรับตัวอย่างของเรา เป็นตัวเลือกที่ยอดเยี่ยมสำหรับทีมที่มองหาประสบการณ์แบบ "ไม่ต้องกำหนดค่า" (zero-configuration)
การตั้งค่าทีละขั้นตอนด้วย Jest
มาตั้งค่าโปรเจกต์ใหม่สำหรับ TDD กัน
1. เริ่มต้นโปรเจกต์ของคุณ: เปิดเทอร์มินัลและสร้างไดเรกทอรีโปรเจกต์ใหม่
mkdir js-tdd-project
cd js-tdd-project
npm init -y
2. ติดตั้ง Jest: เพิ่ม Jest เข้าไปในโปรเจกต์ของคุณในฐานะ development dependency
npm install --save-dev jest
3. กำหนดค่า test script: เปิดไฟล์ `package.json` ของคุณ ค้นหาส่วน `"scripts"` และแก้ไขสคริปต์ `"test"` ขอแนะนำอย่างยิ่งให้เพิ่มสคริปต์ `"test:watch"` ซึ่งมีค่าอย่างยิ่งสำหรับเวิร์กโฟลว์ TDD
"scripts": {
"test": "jest",
"test:watch": "jest --watchAll"
}
แฟล็ก `--watchAll` บอกให้ Jest รันเทสต์ใหม่โดยอัตโนมัติทุกครั้งที่มีการบันทึกไฟล์ สิ่งนี้ให้ผลตอบรับทันที ซึ่งเหมาะสำหรับวงจร Red-Green-Refactor
แค่นั้นแหละ! สภาพแวดล้อมของคุณพร้อมแล้ว Jest จะค้นหาไฟล์เทสต์ที่มีชื่อเป็น `*.test.js`, `*.spec.js` หรืออยู่ในไดเรกทอรี `__tests__` โดยอัตโนมัติ
TDD ในทางปฏิบัติ: การสร้างโมดูล `CurrencyConverter`
มาประยุกต์ใช้วงจร TDD กับปัญหาที่ปฏิบัติได้จริงและเป็นที่เข้าใจในระดับโลก: การแปลงค่าเงินระหว่างสกุลเงินต่างๆ เราจะสร้างโมดูล `CurrencyConverter` ทีละขั้นตอน
Iteration ที่ 1: การแปลงด้วยอัตราคงที่แบบง่าย
🔴 RED: เขียนเทสต์แรกที่ล้มเหลว
ความต้องการแรกของเราคือการแปลงจำนวนเงินที่ระบุจากสกุลเงินหนึ่งไปยังอีกสกุลเงินหนึ่งโดยใช้อัตราคงที่ สร้างไฟล์ใหม่ชื่อ `CurrencyConverter.test.js`
// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');
describe('CurrencyConverter', () => {
it('should convert an amount from USD to EUR correctly', () => {
// จัดเตรียม (Arrange)
const amount = 10; // 10 USD
const expected = 9.2; // สมมติอัตราคงที่ 1 USD = 0.92 EUR
// ดำเนินการ (Act)
const result = CurrencyConverter.convert(amount, 'USD', 'EUR');
// ตรวจสอบ (Assert)
expect(result).toBe(expected);
});
});
ตอนนี้ รัน test watcher จากเทอร์มินัลของคุณ:
npm run test:watch
เทสต์จะล้มเหลวอย่างสิ้นเชิง Jest จะรายงานบางอย่างเช่น `TypeError: Cannot read properties of undefined (reading 'convert')` นี่คือสถานะ RED ของเรา เทสต์ล้มเหลวเพราะ `CurrencyConverter` ยังไม่มีอยู่
🟢 GREEN: เขียนโค้ดที่ง่ายที่สุดเพื่อให้ผ่าน
ตอนนี้ มาทำให้เทสต์ผ่านกันเถอะ สร้างไฟล์ `CurrencyConverter.js`
// CurrencyConverter.js
const rates = {
USD: {
EUR: 0.92
}
};
const CurrencyConverter = {
convert(amount, from, to) {
return amount * rates[from][to];
}
};
module.exports = CurrencyConverter;
ทันทีที่คุณบันทึกไฟล์นี้ Jest จะรันเทสต์อีกครั้ง และมันจะเปลี่ยนเป็นสี GREEN เราได้เขียนโค้ดในปริมาณที่น้อยที่สุดเพื่อตอบสนองความต้องการของเทสต์
🔵 REFACTOR: ปรับปรุงโค้ด
โค้ดนั้นเรียบง่าย แต่เราสามารถเริ่มคิดถึงการปรับปรุงได้แล้ว อ็อบเจกต์ `rates` ที่ซ้อนกันนั้นค่อนข้างตายตัว สำหรับตอนนี้มันยังสะอาดพอ สิ่งที่สำคัญที่สุดคือเรามีฟีเจอร์ที่ทำงานได้และได้รับการปกป้องโดยเทสต์ ไปยังความต้องการถัดไปกันเถอะ
Iteration ที่ 2: การจัดการกับสกุลเงินที่ไม่รู้จัก
🔴 RED: เขียนเทสต์สำหรับสกุลเงินที่ไม่ถูกต้อง
จะเกิดอะไรขึ้นถ้าเราพยายามแปลงเป็นสกุลเงินที่เราไม่รู้จัก? มันควรจะโยนข้อผิดพลาด (throw an error) มา มากำหนดพฤติกรรมนี้ในเทสต์ใหม่ในไฟล์ `CurrencyConverter.test.js` กัน
// ในไฟล์ CurrencyConverter.test.js, ภายใน describe block
it('should throw an error for unknown currencies', () => {
// จัดเตรียม (Arrange)
const amount = 10;
// ดำเนินการ & ตรวจสอบ (Act & Assert)
// เราห่อการเรียกฟังก์ชันด้วย arrow function เพื่อให้ toThrow ของ Jest ทำงานได้
expect(() => {
CurrencyConverter.convert(amount, 'USD', 'XYZ');
}).toThrow('Unknown currency: XYZ');
});
บันทึกไฟล์ ตัวรันเทสต์จะแสดงความล้มเหลวใหม่ทันที มันเป็นสี RED เพราะโค้ดของเราไม่ได้โยนข้อผิดพลาด แต่พยายามเข้าถึง `rates['USD']['XYZ']` ซึ่งส่งผลให้เกิด `TypeError` เทสต์ใหม่ของเราได้ระบุข้อบกพร่องนี้อย่างถูกต้อง
🟢 GREEN: ทำให้เทสต์ใหม่ผ่าน
มาแก้ไข `CurrencyConverter.js` เพื่อเพิ่มการตรวจสอบความถูกต้องกัน
// CurrencyConverter.js
const rates = {
USD: {
EUR: 0.92,
GBP: 0.80
},
EUR: {
USD: 1.08
}
};
const CurrencyConverter = {
convert(amount, from, to) {
if (!rates[from] || !rates[from][to]) {
// ตรวจสอบว่าสกุลเงินใดที่ไม่รู้จักเพื่อข้อความแสดงข้อผิดพลาดที่ดีขึ้น
const unknownCurrency = !rates[from] ? from : to;
throw new Error(`Unknown currency: ${unknownCurrency}`);
}
return amount * rates[from][to];
}
};
module.exports = CurrencyConverter;
บันทึกไฟล์ ตอนนี้เทสต์ทั้งสองผ่านแล้ว เรากลับมาสู่สถานะ GREEN
🔵 REFACTOR: ทำให้มันสะอาดขึ้น
ฟังก์ชัน `convert` ของเราเริ่มใหญ่ขึ้น ตรรกะการตรวจสอบความถูกต้องปะปนอยู่กับการคำนวณ เราสามารถแยกตรรกะการตรวจสอบออกเป็นฟังก์ชันส่วนตัว (private function) เพื่อปรับปรุงความสามารถในการอ่าน แต่สำหรับตอนนี้มันยังจัดการได้อยู่ สิ่งสำคัญคือเรามีอิสระที่จะทำการเปลี่ยนแปลงเหล่านี้เพราะเทสต์ของเราจะบอกเราหากเราทำอะไรพัง
Iteration ที่ 3: การดึงอัตราแลกเปลี่ยนแบบอะซิงโครนัส
การฮาร์ดโค้ดอัตราแลกเปลี่ยนนั้นไม่สมจริง มาปรับปรุงโมดูลของเราเพื่อดึงอัตราแลกเปลี่ยนจาก API ภายนอก (ที่จำลองขึ้น) กัน
🔴 RED: เขียนเทสต์แบบ async ที่จำลองการเรียก API
ก่อนอื่น เราต้องปรับโครงสร้าง converter ของเรา ตอนนี้มันจะต้องเป็นคลาสที่เราสามารถสร้างอินสแตนซ์ได้ อาจจะด้วย API client เรายังต้องจำลอง `fetch` API ด้วย ซึ่ง Jest ทำให้เรื่องนี้ง่าย
มาเขียนไฟล์เทสต์ของเราใหม่เพื่อรองรับความเป็นจริงใหม่ที่เป็นแบบอะซิงโครนัสนี้ เราจะเริ่มด้วยการทดสอบ happy path อีกครั้ง
// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');
// Mock the external dependency
global.fetch = jest.fn();
beforeEach(() => {
// Clear mock history before each test
fetch.mockClear();
});
describe('CurrencyConverter', () => {
it('should fetch rates and convert correctly', async () => {
// จัดเตรียม (Arrange)
// Mock การตอบสนองของ API ที่สำเร็จ
fetch.mockResolvedValueOnce({
json: () => Promise.resolve({ rates: { EUR: 0.92 } })
});
const converter = new CurrencyConverter('https://api.exchangerates.com');
const amount = 10; // 10 USD
// ดำเนินการ (Act)
const result = await converter.convert(amount, 'USD', 'EUR');
// ตรวจสอบ (Assert)
expect(result).toBe(9.2);
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith('https://api.exchangerates.com/latest?base=USD');
});
// เราจะเพิ่มเทสต์สำหรับกรณี API ล้มเหลว ฯลฯ ด้วย
});
การรันโค้ดนี้จะส่งผลให้เกิดสี RED ท่วมท้น `CurrencyConverter` เก่าของเราไม่ใช่คลาส ไม่มีเมธอด `async` และไม่ได้ใช้ `fetch`
🟢 GREEN: นำตรรกะ async ไปใช้
ตอนนี้ มาเขียน `CurrencyConverter.js` ใหม่เพื่อตอบสนองความต้องการของเทสต์
// CurrencyConverter.js
class CurrencyConverter {
constructor(apiUrl) {
this.apiUrl = apiUrl;
}
async convert(amount, from, to) {
const response = await fetch(`${this.apiUrl}/latest?base=${from}`);
if (!response.ok) {
throw new Error('Failed to fetch exchange rates.');
}
const data = await response.json();
const rate = data.rates[to];
if (!rate) {
throw new Error(`Unknown currency: ${to}`);
}
// ปัดเศษอย่างง่ายเพื่อหลีกเลี่ยงปัญหาทศนิยมในเทสต์
const convertedAmount = amount * rate;
return parseFloat(convertedAmount.toFixed(2));
}
}
module.exports = CurrencyConverter;
เมื่อคุณบันทึก เทสต์ควรจะเปลี่ยนเป็นสี GREEN สังเกตว่าเราได้เพิ่มตรรกะการปัดเศษเพื่อจัดการกับความไม่แม่นยำของทศนิยม ซึ่งเป็นปัญหาที่พบบ่อยในการคำนวณทางการเงิน
🔵 REFACTOR: ปรับปรุงโค้ด async
เมธอด `convert` กำลังทำหลายอย่าง: การดึงข้อมูล, การจัดการข้อผิดพลาด, การแยกวิเคราะห์ข้อมูล และการคำนวณ เราสามารถปรับปรุงโค้ดนี้ได้โดยการสร้างคลาส `RateFetcher` แยกต่างหากซึ่งรับผิดชอบเฉพาะการสื่อสารกับ API เท่านั้น จากนั้น `CurrencyConverter` ของเราก็จะใช้ fetcher นี้ ซึ่งเป็นไปตามหลักการ Single Responsibility Principle และทำให้ทั้งสองคลาสง่ายต่อการทดสอบและบำรุงรักษา TDD นำทางเราไปสู่การออกแบบที่สะอาดขึ้นนี้
รูปแบบและรูปแบบที่ควรหลีกเลี่ยงใน TDD (Patterns and Anti-Patterns)
ขณะที่คุณฝึกฝน TDD คุณจะค้นพบรูปแบบที่ทำงานได้ดีและรูปแบบที่ควรหลีกเลี่ยงซึ่งก่อให้เกิดความติดขัด
รูปแบบที่ดีที่ควรปฏิบัติตาม
- Arrange, Act, Assert (AAA): จัดโครงสร้างเทสต์ของคุณเป็นสามส่วนที่ชัดเจน Arrange คือการตั้งค่า, Act คือการเรียกใช้โค้ดที่ต้องการทดสอบ, และ Assert คือการยืนยันว่าผลลัพธ์ถูกต้อง สิ่งนี้ทำให้เทสต์อ่านและเข้าใจง่าย
- ทดสอบพฤติกรรมเดียวในแต่ละครั้ง: เทสต์แต่ละกรณีควรตรวจสอบพฤติกรรมที่เฉพาะเจาะจงเพียงอย่างเดียว สิ่งนี้ทำให้เห็นได้ชัดเจนว่าอะไรเสียเมื่อเทสต์ล้มเหลว
- ใช้ชื่อเทสต์ที่สื่อความหมาย: ชื่อเทสต์เช่น `it('should throw an error if the amount is negative')` มีคุณค่ามากกว่า `it('test 1')` มาก
รูปแบบที่ควรหลีกเลี่ยง
- การทดสอบรายละเอียดการ υλοποίηση (Implementation Details): เทสต์ควรเน้นที่ public API ("อะไร") ไม่ใช่การ υλοποίηση ภายใน ("อย่างไร") การทดสอบเมธอด private ทำให้เทสต์ของคุณเปราะบางและทำให้การปรับปรุงโค้ดยากขึ้น
- การละเลยขั้นตอน Refactor: นี่คือข้อผิดพลาดที่พบบ่อยที่สุด การข้ามการปรับปรุงโค้ดนำไปสู่หนี้ทางเทคนิค (technical debt) ทั้งในโค้ดโปรดักชันและชุดเทสต์ของคุณ
- การเขียนเทสต์ขนาดใหญ่และช้า: Unit test ควรจะรวดเร็ว หากต้องพึ่งพาฐานข้อมูลจริง, การเรียกผ่านเครือข่าย, หรือระบบไฟล์ มันจะช้าและไม่น่าเชื่อถือ ใช้ mocks และ stubs เพื่อแยกส่วน (isolate) unit ของคุณ
TDD ในวงจรการพัฒนาที่กว้างขึ้น
TDD ไม่ได้อยู่แยกเดี่ยว มันผสมผสานอย่างสวยงามกับแนวทางปฏิบัติ Agile และ DevOps สมัยใหม่ โดยเฉพาะสำหรับทีมระดับโลก
- TDD และ Agile: User story หรือเกณฑ์การยอมรับ (acceptance criterion) จากเครื่องมือจัดการโปรเจกต์ของคุณสามารถแปลโดยตรงเป็นชุดของเทสต์ที่ล้มเหลวได้ สิ่งนี้ทำให้มั่นใจได้ว่าคุณกำลังสร้างสิ่งที่ธุรกิจต้องการอย่างแท้จริง
- TDD และ Continuous Integration/Continuous Deployment (CI/CD): TDD เป็นรากฐานของไปป์ไลน์ CI/CD ที่เชื่อถือได้ ทุกครั้งที่นักพัฒนา push code ระบบอัตโนมัติ (เช่น GitHub Actions, GitLab CI, หรือ Jenkins) สามารถรันชุดเทสต์ทั้งหมดได้ หากมีเทสต์ใดล้มเหลว การ build จะถูกหยุด ป้องกันไม่ให้บั๊กไปถึงโปรดักชัน สิ่งนี้ให้ผลตอบรับที่รวดเร็วและอัตโนมัติสำหรับทั้งทีม โดยไม่คำนึงถึงเขตเวลา
- TDD vs. BDD (Behavior-Driven Development): BDD เป็นส่วนขยายของ TDD ที่มุ่งเน้นไปที่การทำงานร่วมกันระหว่างนักพัฒนา, QA, และผู้มีส่วนได้ส่วนเสียทางธุรกิจ มันใช้รูปแบบภาษาธรรมชาติ (Given-When-Then) เพื่ออธิบายพฤติกรรม บ่อยครั้งที่ไฟล์ฟีเจอร์ของ BDD จะขับเคลื่อนการสร้าง unit test แบบ TDD หลายๆ ตัว
บทสรุป: การเดินทางของคุณกับ TDD
การพัฒนาโดยใช้การทดสอบเป็นตัวนำเป็นมากกว่ากลยุทธ์การทดสอบ—มันคือการเปลี่ยนแปลงกระบวนทัศน์ในวิธีที่เราเข้าถึงการพัฒนาซอฟต์แวร์ มันส่งเสริมวัฒนธรรมของคุณภาพ, ความมั่นใจ, และการทำงานร่วมกัน วงจร Red-Green-Refactor ให้จังหวะที่มั่นคงซึ่งนำทางคุณไปสู่โค้ดที่สะอาด, แข็งแกร่ง, และบำรุงรักษาง่าย ชุดเทสต์ที่ได้มาจะกลายเป็นตาข่ายนิรภัยที่ปกป้องทีมของคุณจาก regressions และเป็นเอกสารที่มีชีวิตที่ช่วยให้สมาชิกใหม่เริ่มต้นได้
ช่วงการเรียนรู้อาจรู้สึกสูงชัน และความเร็วในช่วงแรกอาจดูช้าลง แต่ผลตอบแทนระยะยาวในด้านการลดเวลาดีบัก, การออกแบบซอฟต์แวร์ที่ดีขึ้น, และความมั่นใจของนักพัฒนาที่เพิ่มขึ้นนั้นมีค่ามหาศาล การเดินทางสู่การเป็นผู้เชี่ยวชาญ TDD คือการเดินทางของวินัยและการฝึกฝน
เริ่มต้นวันนี้ เลือกฟีเจอร์เล็กๆ ที่ไม่สำคัญในโปรเจกต์ถัดไปของคุณและมุ่งมั่นกับกระบวนการนี้ เขียนเทสต์ก่อน ดูมันล้มเหลว ทำให้มันผ่าน และที่สำคัญที่สุดคือ refactor สัมผัสกับความมั่นใจที่มาจากชุดเทสต์สีเขียว แล้วในไม่ช้าคุณจะสงสัยว่าคุณเคยสร้างซอฟต์แวร์ด้วยวิธีอื่นได้อย่างไร