ไทย

เชี่ยวชาญการพัฒนาโดยใช้การทดสอบเป็นตัวนำ (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 การทำความเข้าใจและฝึกฝนจังหวะนี้เป็นพื้นฐานสำคัญในการฝึกฝนเทคนิคนี้ให้เชี่ยวชาญ

เมื่อวงจรสำหรับฟังก์ชันการทำงานเล็กๆ หนึ่งส่วนเสร็จสมบูรณ์ คุณก็เริ่มต้นใหม่อีกครั้งด้วยเทสต์ที่ล้มเหลวใหม่สำหรับส่วนถัดไป

กฎสามข้อของ TDD

Robert C. Martin (มักรู้จักในชื่อ "Uncle Bob") ซึ่งเป็นบุคคลสำคัญในขบวนการซอฟต์แวร์ Agile ได้กำหนดกฎง่ายๆ สามข้อที่รวบรวมวินัยของ TDD:

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

การปฏิบัติตามกฎเหล่านี้บังคับให้คุณเข้าสู่วงจร Red-Green-Refactor และทำให้มั่นใจได้ว่า 100% ของโค้ดโปรดักชันของคุณถูกเขียนขึ้นเพื่อตอบสนองความต้องการที่เฉพาะเจาะจงและผ่านการทดสอบแล้ว

ทำไมคุณควรนำ TDD มาใช้? กรณีศึกษาทางธุรกิจในระดับโลก

แม้ว่า TDD จะให้ประโยชน์มหาศาลแก่นักพัฒนาแต่ละคน แต่พลังที่แท้จริงของมันจะปรากฏในระดับทีมและธุรกิจ โดยเฉพาะอย่างยิ่งในสภาพแวดล้อมการทำงานที่กระจายตัวอยู่ทั่วโลก

การตั้งค่าสภาพแวดล้อม TDD สำหรับ JavaScript ของคุณ

ในการเริ่มต้น TDD ใน JavaScript คุณต้องมีเครื่องมือสองสามอย่าง ซึ่งระบบนิเวศของ JavaScript สมัยใหม่มีตัวเลือกที่ยอดเยี่ยมให้เลือก

องค์ประกอบหลักของ Testing Stack

เพื่อความเรียบง่ายและครบวงจร เราจะใช้ 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 คุณจะค้นพบรูปแบบที่ทำงานได้ดีและรูปแบบที่ควรหลีกเลี่ยงซึ่งก่อให้เกิดความติดขัด

รูปแบบที่ดีที่ควรปฏิบัติตาม

รูปแบบที่ควรหลีกเลี่ยง

TDD ในวงจรการพัฒนาที่กว้างขึ้น

TDD ไม่ได้อยู่แยกเดี่ยว มันผสมผสานอย่างสวยงามกับแนวทางปฏิบัติ Agile และ DevOps สมัยใหม่ โดยเฉพาะสำหรับทีมระดับโลก

บทสรุป: การเดินทางของคุณกับ TDD

การพัฒนาโดยใช้การทดสอบเป็นตัวนำเป็นมากกว่ากลยุทธ์การทดสอบ—มันคือการเปลี่ยนแปลงกระบวนทัศน์ในวิธีที่เราเข้าถึงการพัฒนาซอฟต์แวร์ มันส่งเสริมวัฒนธรรมของคุณภาพ, ความมั่นใจ, และการทำงานร่วมกัน วงจร Red-Green-Refactor ให้จังหวะที่มั่นคงซึ่งนำทางคุณไปสู่โค้ดที่สะอาด, แข็งแกร่ง, และบำรุงรักษาง่าย ชุดเทสต์ที่ได้มาจะกลายเป็นตาข่ายนิรภัยที่ปกป้องทีมของคุณจาก regressions และเป็นเอกสารที่มีชีวิตที่ช่วยให้สมาชิกใหม่เริ่มต้นได้

ช่วงการเรียนรู้อาจรู้สึกสูงชัน และความเร็วในช่วงแรกอาจดูช้าลง แต่ผลตอบแทนระยะยาวในด้านการลดเวลาดีบัก, การออกแบบซอฟต์แวร์ที่ดีขึ้น, และความมั่นใจของนักพัฒนาที่เพิ่มขึ้นนั้นมีค่ามหาศาล การเดินทางสู่การเป็นผู้เชี่ยวชาญ TDD คือการเดินทางของวินัยและการฝึกฝน

เริ่มต้นวันนี้ เลือกฟีเจอร์เล็กๆ ที่ไม่สำคัญในโปรเจกต์ถัดไปของคุณและมุ่งมั่นกับกระบวนการนี้ เขียนเทสต์ก่อน ดูมันล้มเหลว ทำให้มันผ่าน และที่สำคัญที่สุดคือ refactor สัมผัสกับความมั่นใจที่มาจากชุดเทสต์สีเขียว แล้วในไม่ช้าคุณจะสงสัยว่าคุณเคยสร้างซอฟต์แวร์ด้วยวิธีอื่นได้อย่างไร