# สัปดาห์ที่ 7: การทดสอบกล่องขาวและการทดสอบหน่วย (White Box Testing & Unit Testing)

---

## วัตถุประสงค์การเรียนรู้ (Learning Objectives)

เมื่อจบสัปดาห์นี้ นักศึกษาจะสามารถ:

- อธิบายความแตกต่างระหว่าง การทดสอบกล่องขาวและดำ (White Box และ Black Box Testing) (Explain differences)
- คำนวณและวิเคราะห์ ความครอบคลุมของโค้ด (Code Coverage) (Statement, Branch, Path) - Calculate and analyze
- คำนวณ ความซับซ้อนของโค้ด (Cyclomatic Complexity) ของโค้ด (Calculate CC)
- เขียน การทดสอบหน่วย (Unit Tests) ด้วย Jest Framework (Write unit tests)
- ใช้ การเลียนแบบการทดสอบ (Test Doubles) (Mock, Stub, Spy) อย่างถูกต้อง (Use test doubles correctly)
- วัดและวิเคราะห์ ความครอบคลุมของการทดสอบ (Test Coverage) (Measure coverage)
- เข้าใจหลักการของ การพัฒนาโดยยึดการทดสอบ (Test-Driven Development (TDD)) (Understand TDD principles)

---

## ส่วนที่ 1: ประเมินภูมิฐานการทดสอบกล่องขาว (White Box Testing Fundamentals)

### 1.1 การทดสอบกล่องขาวคืออะไร? (What is White Box Testing?)

**คำจำกัดความ:**
การทดสอบกล่องขาว (White Box Testing) (หรือ Glass Box Testing, Structural Testing, Clear Box Testing) คือการทดสอบซอฟต์แวร์โดยมีความรู้เกี่ยวกับโครงสร้างภายในของโค้ด

**ความแตกต่างกับ การทดสอบกล่องดำ (Black Box Testing) (Differences from Black Box):**

| Aspect                   | Black Box Testing          | White Box Testing                   |
| ------------------------ | -------------------------- | ----------------------------------- |
| **ความรู้เกี่ยวกับโค้ด** | ไม่รู้โครงสร้างภายใน       | รู้โครงสร้างและ logic ภายใน         |
| **Focus**                | Input/Output, Requirements | Code structure, Paths, Logic        |
| **ทำโดย**                | Testers, End Users         | Developers, Technical Testers       |
| **เทคนิค**               | EP, BVA, Decision Table    | Statement Coverage, Branch Coverage |
| **ระดับการทดสอบ**        | System, Acceptance         | Unit, Integration                   |

### 1.2 เมตริกการครอบคลุมโค้ด (Code Coverage Metrics)

#### **1.2.1 การครอบคลุมคำสั่ง (Statement Coverage) (Line Coverage)**

**คำจำกัดความ:** เปอร์เซ็นต์ของ statements ที่ถูก execute โดย test cases

**สูตร (Formula):**

```
Statement Coverage = (Number of Executed Statements / Total Statements) × 100%
```

**ตัวอย่าง (Example):**

```javascript
function calculateDiscount(price, isMember) {
  let discount = 0; // Statement 1

  if (isMember) {
    // Statement 2
    discount = price * 0.1; // Statement 3
  }

  return price - discount; // Statement 4
}
```

**Test Case 1:** `calculateDiscount(100, true)`

- Executes: Statements 1, 2, 3, 4 = **100% coverage**

**Test Case 2:** `calculateDiscount(100, false)`

- Executes: Statements 1, 2, 4 = **75% coverage** (misses statement 3)

---

#### **1.2.2 การครอบคลุมสาขา (Branch Coverage) (Decision Coverage)**

**คำจำกัดความ:** เปอร์เซ็นต์ของ decision branches (true/false) ที่ถูกทดสอบ

**สูตร:**

```
Branch Coverage = (Number of Executed Branches / Total Branches) × 100%
```

**ตัวอย่าง:**

```javascript
function validateAge(age) {
  if (age >= 18) {
    // Branch Point 1
    return "Adult";
  } else {
    return "Minor";
  }
}
```

**Branches:**

- Branch 1: age >= 18 is TRUE
- Branch 2: age >= 18 is FALSE

**Test Cases for 100% Branch Coverage:**

- Test Case 1: `validateAge(20)` → covers TRUE branch
- Test Case 2: `validateAge(15)` → covers FALSE branch

---

#### **1.2.3 การครอบคลุมเส้นทาง (Path Coverage)**

**คำจำกัดความ:** เปอร์เซ็นต์ของ **combinations ของทุก branch** ที่ถูกทดสอบ

**ตัวอย่าง:**

```javascript
function processOrder(amount, isPremium, hasDiscount) {
  let finalAmount = amount;

  if (isPremium) {
    // Decision 1: if → 2 paths
    finalAmount = amount * 0.9;
  }
  if (hasDiscount) {
    // Decision 2: if → 2 paths
    finalAmount = finalAmount * 0.95;
  }
  return finalAmount;
}
```

**Possible Paths (ทั้ง 4 combination):**

| Path | isPremium | hasDiscount | ผลลัพธ์             |
| ---- | --------- | ----------- | ------------------- |
| 1    | false     | false       | amount (ไม่ลด)      |
| 2    | true      | false       | amount × 0.9        |
| 3    | false     | true        | amount × 0.95       |
| 4    | true      | true        | amount × 0.9 × 0.95 |

**Total Paths = 2^n** (n = number of decisions) = 2² = **4 paths** ✅

---

**📊 เปรียบเทียบ 3 ประเภท Coverage:**

| ประเภท        | ครอบคลุมอะไร        | ตัวอย่าง          | ความสำคัญ             |
| ------------- | ------------------- | ----------------- | --------------------- |
| **Statement** | ทุก line of code    | Run function once | ⭐ พื้นฐาน            |
| **Branch**    | Every if true/false | Test both if/else | ⭐⭐⭐ สำคัญ          |
| **Path**      | ทุก combination     | Test all 4 paths  | ⭐⭐⭐⭐⭐ หนักที่สุด |

**หลักการ:** Statement ⊂ Branch ⊂ Path (เพิ่มเข้มข้น)

---

### 1.3 ความซับซ้อนของวัฏจักร (Cyclomatic Complexity)

**คำจำกัดความ:** Cyclomatic Complexity = วัดจำนวนเส้นทางการทำงานที่แตกต่างกัน

> ยิ่งมี **if/else, loop, switch** มาก ➜ ยิ่งมีเส้นทางมาก ➜ ยิ่งซับซ้อน ➜ ยิ่งยากทดสอบ

**📌 สูตรง่าย (Easy Formula) - ใช้สูตรนี้เป็นหลัก:**

```
CC = (จำนวน Decision Points) + 1

Decision Points = if, else if, case, for, while, &&, ||, ?:
```

**ตัวอย่าง:**

```javascript
function calculatePrice(qty, membership, discount) {
  let total = qty * 100; // ← ไม่นับ

  if (membership) {
    // ← Decision 1 (if)
    total = total * 0.9;
  }

  if (discount) {
    // ← Decision 2 (if)
    total = total * 0.95;
  }

  return total; // ← ไม่นับ
}
```

**Decision Points = 2 (if statements)**
**CC = 2 + 1 = 3** ✅

---

**📌 สูตรเทคนิค (เพื่อความเข้าใจลึก):**

```
CC = E - N + 2P

Where:
  E = Number of edges (ลูกศร)
  N = Number of nodes (สี่เหลี่ยม)
  P = Connected components (โดยปกติ = 1)
```

ในตัวอย่างข้างบน:

- Edges (E) = 6 ลูกศร
- Nodes (N) = 4 โหนด
- CC = 6 - 4 + 2(1) = 4 - 1 = **3** ✅ (ตรงกัน!)

**ตัวอย่างการคำนวณ (Example Calculation):**

```javascript
function calculateShipping(weight, distance, isExpress) {
  let cost = 0;

  if (weight > 10) {
    // Decision 1
    cost = 50;
  } else {
    cost = 20;
  }

  if (distance > 100) {
    // Decision 2
    cost = cost * 1.5;
  }

  if (isExpress) {
    // Decision 3
    cost = cost * 2;
  }

  return cost;
}
```

**คำนวณ CC:**

- Decision Points = 3 (if statements)
- CC = 3 + 1 = **4**

**ความหมาย (Meaning):**

- CC = 1-5: ไม่ต้องกังวล, ความเสี่ยงต่ำ (Simple, low risk)
- CC = 6-10: ปรับปรุงอีกนิดหน่อย, ซับซ้อนปานกลาง (Moderate complexity)
- CC = 11-20: ต้องแก้ไข -แยกเป็นฟังก์ชันเล็กๆ, ซับซ้อน, ความเสี่ยงสูง (Complex, high risk)
- CC > 20: ต้องจัดโครงสร้างใหม่, ซับซ้อนมาก, ทดสอบไม่ได้ (Very complex, untestable)

---

### 1.4 กราฟการไหลของการควบคุม (Flow Graph / Control Flow Graph)

**ความหมาย:** โครงสร้างที่แสดงเส้นทางการไหลของโปรแกรมจาก start ไป end

**ตัวอย่าง Code:**

```javascript
function calculatePrice(quantity, hasMembership) {
  let price = quantity * 100; // Node 1
  if (hasMembership) {
    // Node 2 (Decision)
    price = price * 0.9; // Node 3
  }
  return price; // Node 4
}
```

**Flow Graph (โครงสร้างเส้นทาง):**

```
        [START]
          |
          v
    [Node 1: price = qty × 100]
          |
          v
    [Node 2: Decision hasMembership?]
         /  \
       Yes   No
       /      \
      v        v
[Node 3:   [Node 4:
price×0.9]  return]
      \      /
       \    /
        v  v
      [END]
```

**การวาด Flow Graph:**

1. **Nodes** = สี่เหลี่ยม = statements หรือ decisions
2. **Edges** = ลูกศร = ทิศทางการไหลของโปรแกรม
3. **Decision Node** = มี 2 ลูกศรออก (true/false)
4. หาจำนวน **independent paths** = เส้นทางจาก start ไป end

**ในตัวอย่างนี้:**

- Path 1: START → Node 1 → Node 2 (false) → Node 4 → END
- Path 2: START → Node 1 → Node 2 (true) → Node 3 → Node 4 → END
- **Total = 2 paths**

---

## ส่วนที่ 2: ประเมินการทดสอบหน่วย (Unit Testing) - 45 นาที

### 2.1 การทดสอบหน่วยคืออะไร (What is Unit Testing?)

**คำจำกัดความ:**
การทดสอบหน่วย (Unit Testing) คือการทดสอบ “หน่วยที่เล็กที่สุด” ของโค้ด (function, method, class) แยกต่างหากจาก dependencies

**ลักษณะของการทดสอบที่ดี (Characteristics of Good Tests):**

- **เร็ว** - run ได้เร็ว (milliseconds) - run in milliseconds
- **อิสระ** - ไม่ขึ้นกับ external dependencies (ฐานข้อมูล, API, File)
- **ทำซ้ำได้** - รันหลายครั้ง ต้องได้ผลลัพธ์เหมือนเดิม
- **ชัดเจน** - ผ่านหรือไม่ผ่านชัดเจน - pass or fail clearly
- **เขียนในเวลาที่เหมาะสม** - เขียน Test พร้อมกับ การเขียน Feature ไม่ใช่เขียนโค้ด 1 เดือน แล้วเขียน Test ทีหลัง

### 2.2 ประโยชน์ของการทดสอบหน่วย (Unit Testing Benefits)

**ประโยชน์ (Benefits):**

1. **หาบั๊กได้เร็ว** (Find bugs early) - พบปัญหาตอน development
2. **เอกสารที่ทำงานได้** (Living documentation) - tests = documentation
3. **Refactor ได้อย่างมั่นใจ** (Safe refactoring) - มี safety net
4. **Design ดีขึ้น** (Better design) - บังคับให้เขียนโค้ดแบบ modular
5. **ลด debugging time** (Reduce debugging) - รู้ว่าส่วนไหนเสีย

### 2.3 Jest Framework

**ติดตั้ง (Installation):**

```bash
npm install --save-dev jest
```

**Configuration (package.json):**

```json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}
```

**โครงสร้างพื้นฐาน (Basic Test Structure):**

```javascript
// book.js
class Book {
  constructor(title, author, isbn) {
    if (!title || !author) {
      throw new Error("Title and author are required");
    }
    this.title = title;
    this.author = author;
    this.isbn = isbn;
    this.available = true;
  }

  borrow() {
    if (!this.available) {
      throw new Error("Book is not available");
    }
    this.available = false;
  }

  returnBook() {
    this.available = true;
  }
}

module.exports = Book;
```

```javascript
// book.test.js
const Book = require("./book");

describe("Book", () => {
  describe("Constructor", () => {
    test("should create book with valid data", () => {
      const book = new Book("1984", "George Orwell", "978-0-452-28423-4");

      expect(book.title).toBe("1984");
      expect(book.author).toBe("George Orwell");
      expect(book.isbn).toBe("978-0-452-28423-4");
      expect(book.available).toBe(true);
    });

    test("should throw error when title is missing", () => {
      expect(() => {
        new Book("", "George Orwell", "123");
      }).toThrow("Title and author are required");
    });

    test("should throw error when author is missing", () => {
      expect(() => {
        new Book("1984", "", "123");
      }).toThrow("Title and author are required");
    });
  });

  describe("borrow()", () => {
    test("should mark book as unavailable when borrowed", () => {
      const book = new Book("Test", "Author", "123");
      book.borrow();

      expect(book.available).toBe(false);
    });

    test("should throw error when borrowing unavailable book", () => {
      const book = new Book("Test", "Author", "123");
      book.borrow();

      expect(() => {
        book.borrow();
      }).toThrow("Book is not available");
    });
  });

  describe("returnBook()", () => {
    test("should mark book as available when returned", () => {
      const book = new Book("Test", "Author", "123");
      book.borrow();
      book.returnBook();

      expect(book.available).toBe(true);
    });
  });
});
```

### 2.4 การยืนยันการทดสอบ (Jest Assertions)

**Common Matchers (ตัวจับคู่ทั่วไป):**

```javascript
// Equality
expect(value).toBe(expected); // ===
expect(value).toEqual(expected); // deep equality
expect(value).not.toBe(expected); // !==

// ความจริง (Truthiness)
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(5);
expect(value).toBeCloseTo(0.3); // floating point

// Strings
expect(string).toMatch(/pattern/);
expect(string).toContain("substring");

// Arrays
expect(array).toContain(item);
expect(array).toHaveLength(3);

// Objects
expect(object).toHaveProperty("key");
expect(object).toMatchObject({ key: value });

// Exceptions (ข้อยกเว้น)
expect(() => fn()).toThrow();
expect(() => fn()).toThrow(Error);
expect(() => fn()).toThrow("error message");
```

---

## ส่วนที่ 3: Test Doubles (30 นาที)

### 3.1 Test Doubles คือ อะไร

**คำจำกัดความ:** วัตถุหรือโค้ดจำลองที่ใช้แทนที่ Production Object จริงในระหว่างการทำ Software Testing (เช่น Unit Test) เพื่อควบคุมสภาพแวดล้อมให้ทดสอบได้ง่ายและแม่นยำขึ้น โดยลดการพึ่งพา (Dependencies) เช่น ฐานข้อมูลหรือ API จริง ช่วยให้เทสเร็วขึ้น จำลอง Error ได้ และไม่ส่งผลกระทบต่อข้อมูลจริง

**ประเภทของ Test Doubles**

**📌 ทำไมต้อง Test Doubles?**

```
❌ ปัญหา: Database, API, File system
   → เรียก DB จริง ❌ ช้า, ยาก, ขึ้นอยู่กับ env

✅ วิธี: Test Doubles
   → จำลอง DB/API ✅ เร็ว, อิสระ, ควบคุมได้
```

---

#### **3.1.1 Stub (ตอบแบบคงที่)**

**ความหมาย:** วัตถุจำลองที่ให้ผลลัพธ์ (Canned Answers) ตามที่กำหนดไว้ล่วงหน้า เพื่อบังคับทิศทางของโค้ด, ตอบคำถามแบบเดิมเสมอ

```javascript
// ❌ ปัญหา: Database จริง
const realDatabase = new Database();
await realDatabase.save({ title: "Book" });

// ✅ ใช้ Stub: ตอบแบบคงที่
const databaseStub = {
  save: () => Promise.resolve({ id: 1, title: "Book" }),
};

test("should load book", async () => {
  const result = await databaseStub.save({ title: "Book" });
  expect(result.id).toBe(1); // ✅ ผ่านเสมอ (predictable)
});
```

**ลักษณะ:** ไม่ต้องบันทึก, ตอบเหมือนทุกครั้ง

---

#### **3.1.2 Mock (บันทึกการเรียก)**

**ความหมาย:** วัตถุที่ตั้งค่ามาเพื่อตรวจสอบพฤติกรรม (Behavior) โดยเฉพาะ ว่ามีการเรียกใช้งาน Method ที่ถูกต้องหรือไม่

```javascript
// ❌ ปัญหา: ต้องรู้ว่า Email ส่งไปไหม
function registerUser(email) {
  sendEmail(email); // ส่งจริง ❌
}

// ✅ ใช้ Mock: ตรวจสอบการเรียก
const mockEmail = jest.fn().mockResolvedValue(true);

test("should send email", async () => {
  await registerUser("john@example.com", mockEmail);

  // ✅ ตรวจสอบว่า:
  expect(mockEmail).toHaveBeenCalledWith("john@example.com");
  expect(mockEmail).toHaveBeenCalledTimes(1);
});
```

**ลักษณะ:** บันทึกการเรียก, ตรวจสอบ arguments

---

#### **3.1.3 Spy (Mock + ทำได้จริง)**

**ความหมาย:** Stub รูปแบบหนึ่งที่สามารถบันทึกข้อมูลการเรียกใช้งาน (เช่น ถูกเรียกกี่ครั้ง, ส่ง Parameter อะไรมา) เพื่อนำมาตรวจสอบ

```javascript
const realDatabase = new Database();
const spyDatabase = jest.spyOn(realDatabase, "save");

// ทำงานจริง + บันทึกการเรียก
await realDatabase.save({ title: "Book" });

// ✅ ตรวจสอบ
expect(spyDatabase).toHaveBeenCalled();
expect(realDatabase.books).toContain(/* book ที่แท้จริง */);

spyDatabase.mockRestore();
```

**ลักษณะ:** ทำงานจริง + บันทึกการเรียก (hybrid)

---

#### **3.1.4 Fake (การจำลองที่ใช้ได้)**

**ความหมาย:** วัตถุที่มีการทำงานจริง แต่เป็นรูปแบบที่ง่ายกว่าและไม่เหมาะใช้งานจริง (เช่น ใช้ In-Memory Database แทน SQL Server จริง)

```javascript
class FakeDatabase {
  constructor() {
    this.books = [];
  }

  async save(book) {
    this.books.push(book);
    return { id: this.books.length, ...book };
  }

  async find(id) {
    return this.books[id - 1] || null;
  }
}

test("should save and find book", async () => {
  const fakeDb = new FakeDatabase();

  // จริงๆ ทำ: save ลง in-memory
  const result = await fakeDb.save({ title: "Book" });

  // จริงๆ ทำ: หา book
  const found = await fakeDb.find(result.id);

  expect(found.title).toBe("Book"); // ✅ ทำงานจริง
});
```

**ลักษณะ:** ทำงานจริง, เร็ว (in-memory), ไม่ต้องติดต่อ DB โดยตรง

---

**📊 เปรียบเทียบ 4 ประเภท:**

|            | Stub         | Mock            | Spy          | Fake         |
| ---------- | ------------ | --------------- | ------------ | ------------ |
| **ทำงาน**  | ❌ ไม่       | ❌ ไม่          | ✅ ใช่       | ✅ ใช่       |
| **บันทึก** | ❌ ไม่       | ✅ ใช่          | ✅ ใช่       | ❌ ไม่       |
| **ใช้**    | ให้ข้อมูล    | ตรวจสองการเรียก | ทำ + บันทึก  | จำลอง DB     |
| **Jest**   | plain object | jest.fn()       | jest.spyOn() | custom class |

### 3.2 การใช้ฟังก์ชัน Mock ของ Jest (Using Jest Mock Functions)

**ความหมาย:** Mock = ฟังก์ชันเทียมที่บันทึกว่าถูกเรียกไหม

**ปัญหา:** เมื่อเขียน Unit Test ไม่ควรเรียก API/Database จริง (ช้า, ยาก, ไม่อิสระ)
**วิธี:** ใช้ `jest.fn()` สร้าง Mock ฟังก์ชันแทน (เร็ว, อิสระ, ควบคุมได้)

---

#### **3.2.1 Mock พื้นฐาน (Basic Mock)**

**ความหมาย:** สร้าง Mock ฟังก์ชันและตรวจสอบว่าถูกเรียกไหม

```javascript
const mockFn = jest.fn(); // สร้าง mock ฟังก์ชัน
mockFn("arg1", "arg2"); // เรียก mock

// ตรวจสอบการเรียก:
expect(mockFn).toHaveBeenCalled(); // ถูกเรียกไหม?
expect(mockFn).toHaveBeenCalledWith("arg1", "arg2"); // เรียกกับ arguments ไหน?
expect(mockFn).toHaveBeenCalledTimes(1); // เรียกกี่ครั้ง?
```

**ตัวอย่าง (Example) - การทดสอบการส่ง Email:**

```javascript
// ปัญหา: ต้องส่ง Email จริง
function registerUser(email, emailService) {
  saveUser(email);
  emailService.send(email, "Welcome!"); // ส่ง Email จริง (ช้า)
}

// ใช้ Mock:
test("should send email when registering", () => {
  const mockEmail = jest.fn(); // Mock ฟังก์ชัน
  registerUser("john@example.com", mockEmail);

  // ตรวจสอบว่าส่ง Email ไป
  expect(mockEmail).toHaveBeenCalledWith("john@example.com", "Welcome!");
  expect(mockEmail).toHaveBeenCalledTimes(1);
});
```

---

#### **3.2.2 กำหนดค่าที่ Mock คืนกลับ (Mock Return Values)**

**ความหมาย:** ให้ Mock ฟังก์ชันตอบค่าที่กำหนด

```javascript
const mockFn = jest
  .fn()
  .mockReturnValue("default") // ตอบอันนี้ทุกครั้งทั่วไป
  .mockReturnValueOnce("first call") // ครั้งแรก → "first call"
  .mockReturnValueOnce("second call"); // ครั้งที่สอง → "second call"

console.log(mockFn()); // 'first call'
console.log(mockFn()); // 'second call'
console.log(mockFn()); // 'default'  (ครั้งถัดๆ ไป)
```

**ตัวอย่าง - การทดสอบการเรียก API:**

```javascript
// ปัญหา: ต้องเรียก API เพื่อรู้ว่า User ชื่ออะไร
function getUserName(userId, api) {
  const user = api.getUser(userId); // เรียก API จริง ❌
  return user.name;
}

// ใช้ Mock:
test("should get user name from API", () => {
  const mockApi = {
    getUser: jest.fn().mockReturnValue({
      id: 1,
      name: "John",
    }),
  };

  const name = getUserName(1, mockApi);

  expect(name).toBe("John");
  expect(mockApi.getUser).toHaveBeenCalledWith(1);
});
```

---

#### **3.2.3 Mock Async Functions (ฟังก์ชัน Promise/Async)**

**ความหมาย:** Mock ฟังก์ชันที่คืนค่า Promise (เช่น API call)

```javascript
// ให้ Mock ตอบ Promise.resolve (สำเร็จ)
const mockAsync = jest.fn().mockResolvedValue("success");

// ให้ Mock ตอบ Promise.reject (ล้มเหลว)
const mockAsync = jest.fn().mockRejectedValue(new Error("Network Error"));
```

**ตัวอย่าง - การทดสอบการ Fetch ข้อมูล:**

```javascript
// ปัญหา: ต้องเรียก API จริง
async function fetchUserData(userId, api) {
  return await api.getUser(userId); // เรียก API จริง ❌
}

// ใช้ Mock:
test("should fetch user data successfully", async () => {
  const mockApi = {
    getUser: jest.fn().mockResolvedValue({ id: 1, name: "John" }), // ตอบเป็น Promise
  };

  const user = await fetchUserData(1, mockApi);

  expect(user.name).toBe("John");
  expect(mockApi.getUser).toHaveBeenCalledWith(1);
});

// ทดสอบ error case
test("should handle API error", async () => {
  const mockApi = {
    getUser: jest.fn().mockRejectedValue(new Error("Network Error")), // ตอบเป็น reject
  };

  await expect(fetchUserData(1, mockApi)).rejects.toThrow("Network Error");
});
```

---

#### **3.2.4 Mock โมดูล (Mock Modules / jest.mock())**

**ความหมาย:** ปิดโมดูลจริง (เช่น Database) แล้วเลียนแบบให้อยู่ในการควบคุม

```javascript
// ปิดโมดูล Database จริง
jest.mock("./database");
const Database = require("./database");

// กำหนดว่า Database ควรทำอะไร
Database.mockImplementation(() => {
  return {
    connect: jest.fn().mockResolvedValue(true),
    save: jest.fn().mockResolvedValue({ id: 1 }),
  };
});
```

**ตัวอย่าง - การทดสอบการ Save ลง Database:**

```javascript
// ปัญหา: ต้องเชื่อมต่อ Database จริง
function saveBook(book, db) {
  return db.save(book); // Save ลง DB จริง ❌
}

// ใช้ Mock:
jest.mock("./database");
const Database = require("./database");

test("should save book to database", async () => {
  const mockDb = {
    save: jest.fn().mockResolvedValue({ id: 1, title: "Jest Guide" }),
  };

  const result = await saveBook({ title: "Jest Guide" }, mockDb);

  expect(result.id).toBe(1);
  expect(result.title).toBe("Jest Guide");
  expect(mockDb.save).toHaveBeenCalledWith({ title: "Jest Guide" });
});
```

---

#### **3.2.5 สรุป Jest Mock Methods**

| เมธอด                     | ความหมาย             | ตัวอย่าง                                 |
| ------------------------- | -------------------- | ---------------------------------------- |
| `jest.fn()`               | สร้าง Mock function  | `jest.fn()`                              |
| `mockReturnValue()`       | ตอบค่าคงที่เสมอ      | `.mockReturnValue("hello")`              |
| `mockReturnValueOnce()`   | ตอบค่าเฉพาะครั้งนั้น | `.mockReturnValueOnce("first")`          |
| `mockResolvedValue()`     | ตอบ Promise.resolve  | `.mockResolvedValue({id: 1})`            |
| `mockRejectedValue()`     | ตอบ Promise.reject   | `.mockRejectedValue(Error)`              |
| `mockImplementation()`    | กำหนด implementation | `.mockImplementation(() => ...)`         |
| `toHaveBeenCalled()`      | ถูกเรียกไหม?         | `expect(mock).toHaveBeenCalled()`        |
| `toHaveBeenCalledWith()`  | เรียกกับ arg ไหน?    | `expect(mock).toHaveBeenCalledWith("x")` |
| `toHaveBeenCalledTimes()` | เรียกกี่ครั้ง?       | `expect(mock).toHaveBeenCalledTimes(1)`  |

**ข้อดี:** เร็ว (ไม่ต้องติดต่อ API/DB จริง) | อิสระ | ควบคุมได้ | ตรวจสอบได้ว่าเรียกไหม

---

## 4: Test-Driven Development (TDD) Overview - ภาพรวม (20 นาที)

### 4.1 TDD คืออะไร (What is TDD?)

**คำจำกัดความ:**
Test-Driven Development คือ development process ที่เขียน test ก่อนเขียน production code

### 4.2 Red-Green-Refactor Cycle

```
🔴 RED → Write a failing test
    ↓
🟢 GREEN → Write minimum code to pass
    ↓
🔵 REFACTOR → Improve code quality
    ↓
    (repeat)
```

**ตัวอย่าง TDD Flow:**

**Step 1: RED - Write Failing Test**

```javascript
describe("calculateTotal", () => {
  test("should calculate total with tax", () => {
    const result = calculateTotal(100, 0.07);
    expect(result).toBe(107);
  });
});

// Run test → FAILS (function doesn't exist)
```

**Step 2: GREEN - Write Minimum Code** (เขียนโค้ดแค่พอให้ test ผ่าน)

```javascript
// ❌ ไม่เขียนแบบนี้:
function calculateTotal(price, tax) {
  return price * (1 + tax); // ✅ ผ่าน test
  // แต่มี edge cases ที่ไม่จัดการ
}

// ✅ เขียนแค่พอให้ pass ก่อน:
function calculateTotal(price, tax) {
  return price + price * tax; // ✅ pass
}

// Run test → PASSES ✅
```

**Step 3: REFACTOR - Improve Code** (ปรับปรุงโค้ดโดยยังให้ test ผ่าน)

```javascript
// ยังคง pass แต่ดีขึ้น:
function calculateTotal(price, taxRate) {
  if (price < 0 || taxRate < 0) {
    throw new Error("Price and tax must be positive");
  }
  return price * (1 + taxRate); // โค้ดดีกว่า
}

// Run test → STILL PASSES ✅ (test ไม่เปลี่ยน!)
```

**📌 สิ่งสำคัญ:** ทุก step ต้อง run test ให้ผ่าน!

### 4.3 ประโยชน์ของ TDD (Benefits of TDD) - ประโยชน์ของ TDD

**ข้อดี (Advantages):**

- **Better Design** - บังคับให้คิดถึง API design ก่อน
- **Living Documentation** - tests อธิบาย behavior
- **Confidence** - refactor ได้โดยไม่กลัวพังสิ่งเดิม
- **Less Debugging** - catch bugs early
- **Complete Coverage** - test ครอบคลุมทุก feature

**ข้อเสีย (Disadvantages):**

- **Learning Curve** - ต้องฝึกฝน
- **Initial Slowdown** - ช้ากว่าตอนแรก
- **Not Always Suitable** - บาง context ไม่เหมาะ (prototyping, UI)

### 4.4 เมื่อไหรควรใช้ TDD? (When to use TDD?) - เมื่อไหรควรใช้ TDD?

**ใช้ TDD เมื่อ (Use TDD when):**

- Requirements ชัดเจน (Clear requirements)
- Business logic ซับซ้อน (Complex logic)
- Critical functionality (ฟังก์ชันสำคัญ)
- Team มีประสบการณ์ (Experienced team)

**ไม่ต้องใช้ TDD เมื่อ (Don't use TDD when):**

- Exploratory coding / Prototyping
- UI/UX design ที่ต้อง experiment
- Simple CRUD operations
- Deadline ฉุกเฉิน (แต่ต้องเขียน tests ภายหลัง!)

---

## Code Coverage Demo

### Running Coverage Report

```bash
npm test -- --coverage
```

**Sample Coverage Output:**

```
--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files           |   85.71 |    83.33 |     100 |   85.71 |
 book.js            |   85.71 |    83.33 |     100 |   85.71 | 15-16
--------------------|---------|----------|---------|---------|-------------------
```

**ความหมาย Coverage Types:**

- **% Stmts** (Statement): \_\_ บรรทัดโค้ด ที่ถูก execute
- **% Branch**: if/else conditions ที่ถูกทดสอบ (true/false)
- **% Funcs**: ฟังก์ชัน ที่ถูกเรียก
- **% Lines**: บรรทัดโค้ด (เหมือน Stmts)

**นักเรียนต้องสนใจหลัก:** `% Stmts` และ `% Branch`

---

**🎯 เป้าหมายความครอบคลุม (Coverage Goals):**

| เป้าหมาย | การตีความ         | หมายเหตุ               |
| -------- | ----------------- | ---------------------- |
| **80%+** | ✅ ยอมรับได้      | สำหรับอุตสาหกรรม       |
| **90%+** | ✅ ดีมาก          | ส่วนใหญ่ใช้            |
| **95%+** | ✅⭐ ยอดเยี่ยม    | Critical projects      |
| **100%** | 🔴 **ไม่จำเป็น!** | ⚠️ Diminishing returns |

**⚠️ ข้อเตือน: 100% Coverage ≠ Bug-Free Code**

```javascript
// 100% coverage แต่ยังมี bug
function divide(a, b) {
  return a / b; // ✅ covered ทั้งหมด
}

test("divide", () => {
  expect(divide(10, 2)).toBe(5); // ✅ passed
});

// ❌ bug: ไม่เช็ค divide by zero!
divide(10, 0); // Infinity (bug!)
```

**เหตุผล:** Coverage = "มี test หรือไม่" ไม่ใช่ "Test ดีหรือไม่"

👉 **ลดเป้าหมายเป็น 80-90% แล้วเขียน Test ที่ดี ดีกว่า 100% Coverage ที่ไม่ดี**

---

## Summary & Best Practices - สรุปและแนวปฏิบัติที่ดี

### Unit Testing Best Practices

1. **Test Naming**: ชื่อ test ต้องอธิบาย behavior

   ```javascript
   // Bad
   test("test1", () => {});

   // Good
   test("should throw error when book title is empty", () => {});
   ```

2. **Arrange-Act-Assert (AAA) Pattern**:

   ```javascript
   test("should calculate discount correctly", () => {
     // Arrange
     const price = 100;
     const discountRate = 0.1;

     // Act
     const result = calculateDiscount(price, discountRate);

     // Assert
     expect(result).toBe(90);
   });
   ```

3. **One Assertion per Test** (guideline - ไม่ใช่กฎแน่น):

   ```javascript
   // ✅ ดี - เล็กน้อยหลายคำสั่ง ก็ได้ถ้า test ตรวจสอบ "เรื่องเดียว"
   test("should create book with correct properties", () => {
     const book = new Book("Title", "Author");
     expect(book.title).toBe("Title"); // ← 2 assertions ก็ได้
     expect(book.author).toBe("Author");
   });

   // ❌ ไม่ดี - ตรวจสอบ"+หลายเรื่องต่างกันมาก"
   test("should create book", () => {
     const book = new Book("Title", "Author");
     expect(book.title).toBe("Title");
     expect(book.price).toBe(100); // ← ต่างแนว
     expect(book.isAvailable).toBe(true); // ← ต่างแนว
     expect(book.validate()).toBe(true); // ← ต่างแนว
   });
   ```

   **💡 หลักการ:** 1 test = 1 concept (ตรวจสอบคอนเซปต์เดียว เขียนหลายคำสั่งก็ได้)

4. **Use describe blocks** for organization:

   ```javascript
   describe("Book", () => {
     describe("Constructor", () => {
       test("case 1", () => {});
       test("case 2", () => {});
     });

     describe("borrow()", () => {
       test("case 1", () => {});
     });
   });
   ```

5. **Test Edge Cases (ทดสอบกรณีพิเศษ)**:
   - Null/undefined inputs
   - Empty strings/arrays
   - Boundary values
   - Invalid data types

---
