# Jest By Example

## สารบัญ

0. [บทนำ & Setup](#0-บทนำ--setup)
1. [ตัวอย่างที่ 1: ฟังก์ชัน Simple (Calculator)](#1-ตัวอย่างที่-1-ฟังก์ชัน-simple-calculator)
2. [ตัวอย่างที่ 2: การ Validate Input](#2-ตัวอย่างที่-2-การ-validate-input)
3. [ตัวอย่างที่ 3: Object & Methods](#3-ตัวอย่างที่-3-object--methods)
4. [ตัวอย่างที่ 4: Async & Await (API Call)](#4-ตัวอย่างที่-4-async--await-api-call)
5. [ตัวอย่างที่ 5: Mocking & Spying](#5-ตัวอย่างที่-5-mocking--spying)
6. [ตัวอย่างที่ 6: Class Testing (User Account)](#6-ตัวอย่างที่-6-class-testing-user-account)
7. [ตัวอย่างที่ 7: Library Management System (Real Project)](#7-ตัวอย่างที่-7-library-management-system-real-project)
8. [Common Patterns & Tips](#8-common-patterns--tips)

---

## 0. บทนำ & Setup

ทฤษฎีดีแต่ **ตัวอย่าง Code จริง** ช่วยให้เข้าใจได้เร็วขึ้น 10 เท่า

- ✅ โค้ดจริงที่คนพัฒนาใช้งาน
- ✅ ข้อผิดพลาดที่ Student มักทำ
- ✅ วิธีแก้ที่เหมาะสม
- ✅ Why + How ในตัวอย่างเดียวกัน

### 🎓 Jest คืออะไร?

Jest เป็น **Test Framework** ของ JavaScript ที่ช่วยให้เขียน Tests ง่าย เร็ว และชัดเจน

```
┌──────────────────────────────────────────────────────────┐
│ ปัญหา: เมื่อ Code ใหญ่ขึ้นเราจะรู้สึกใจสั่นเมื่อต้อง  Refactor code    │
├──────────────────────────────────────────────────────────┤
│ Jest Test ตรวจสอบว่า:                                     │
│   ✅ Function ทำงานถูก                                   │
│   ✅ Input ผิด → Error ถูกหรือไม่                           │
│   ✅ External Service (API) ถูก Mock หรือไม่               │
│   ✅ Refactor ไม่ Break Logic เดิม                         │
└──────────────────────────────────────────────────────────┘
```

### 📊 Test ทำให้อะไร?

| สิ่งที่เกิด       | ปัญหา                 | Jest ช่วย          |
| ----------------- | --------------------- | ------------------ |
| **Refactor Code** | กลัวจะ Break Logic    | ✅ Test ตรวจสอบ    |
| **Add Feature**   | Existing Feature พัง? | ✅ Test บอกเลย     |
| **Fix Bug**       | Bug ม้ายกลับมา?       | ✅ Regression Test |
| **Code Review**   | เพื่อนบอกอะไร?        | ✅ Test บอกชัดเจน  |
| **Deploy**        | ใจสั่นไหม             | ✅ Test ผ่านหมด    |

### 🔄 Jest Testing Workflow

```
1. เขียน Function (src/calculator.js)
                ↓
2. เขียน Test (tests/calculator.test.js)
                ↓
3. รัน Test ด้วย: npm test
                ↓
4. ดู ผ่าน ✅ หรือ พัง ❌
                ↓
5. แก้ Code ให้ Test ผ่าน
                ↓
6. ทำซ้ำ จนกว่า Code ถูกต้อง
```

### Project Setup

```bash
# สร้างโปรเจกต์
mkdir jest-examples
cd jest-examples

# สร้าง package.json
npm init -y

# ติดตั้ง Jest
npm install --save-dev jest

# สร้าง folders
mkdir src tests

# อัปเดต package.json scripts
# เพิ่ม: "test": "jest"
```

---

## 1. ตัวอย่างที่ 1: ฟังก์ชัน Simple (Calculator)

### 📝 โจทย์: เขียน Calculator Functions

**โค้ดจริง** (`src/calculator.js`):

```javascript
// 🎯 ฟังก์ชัน: บวก 2 ตัวเลข
function add(a, b) {
  return a + b;
}

// 🎯 ฟังก์ชัน: ลบ 2 ตัวเลข
function subtract(a, b) {
  return a - b;
}

// 🎯 ฟังก์ชัน: คูณ 2 ตัวเลข
function multiply(a, b) {
  return a * b;
}

// 🎯 ฟังก์ชัน: หาร 2 ตัวเลข
function divide(a, b) {
  // ⚠️ ต้อง Handle ก่อน divide by zero!
  if (b === 0) {
    throw new Error("Cannot divide by zero");
  }
  return a / b;
}

module.exports = { add, subtract, multiply, divide };
```

### ✅ Test File (`tests/calculator.test.js`):

#### 🏗️ ก่อนอ่าน Code ต้องรู้ Anatomy ของ Test

```javascript
test("should add two positive numbers", () => {
  expect(add(2, 3)).toBe(5);
});

// ↓ แบ่งออกเป็น 3 ส่วน:

// 1️⃣ SETUP - ตั้งค่า Input
const a = 2;
const b = 3;

// 2️⃣ ACTION - เรียก Function ที่ต้อง Test
const result = add(a, b);

// 3️⃣ ASSERT - ตรวจสอบ Output
expect(result).toBe(5); // ✅ ถ้า = 5 PASS, ถ้า ≠ 5 FAIL

// เรียกกันว่า "AAA Pattern" (Arrange, Act, Assert)
```

#### 🔑 Key Jest Functions:

| Function       | ความหมาย              | ตัวอย่าง                            |
| -------------- | --------------------- | ----------------------------------- |
| **test()**     | สร้าง 1 Test Case     | `test("should add", () => { ... })` |
| **describe()** | จัดกลุ่ม Tests        | `describe("add()", () => { ... })`  |
| **expect()**   | เริ่ม Assertion       | `expect(result)`                    |
| **.toBe()**    | ตรวจสอบ Value เท่ากัน | `expect(5).toBe(5)` ✅              |
| **.toThrow()** | ตรวจสอบ Error         | `expect(() => fn()).toThrow()`      |

#### 📖 อ่าน Test Code นี่ยังไง:

```javascript
// 📚 นำเข้า Jest Test Runner
const { add, subtract, multiply, divide } = require("../src/calculator");

// 📚 Describe Group - จัดกลุ่ม Tests
describe("Calculator Functions", () => {
  // ✅ Test: add() ทำงานถูก
  describe("add()", () => {
    // ✅ Test Case 1: บวก 2 ตัวเลข บวก
    // อ่านว่า: "should add two positive numbers"
    test("should add two positive numbers", () => {
      // SETUP: 2 + 3
      // ACTION: add(2, 3)
      // ASSERT: ผล = 5
      expect(add(2, 3)).toBe(5);
      // ✅ PASS เพราะ 2 + 3 = 5 จริง
    });

    // ✅ Test Case 2: บวก ตัวลบ + ตัวบวก
    test("should add positive and negative numbers", () => {
      expect(add(5, -2)).toBe(3); // 5 + (-2) = 3 ✅
    });

    // ✅ Test Case 3: บวก 0
    test("should add with zero", () => {
      expect(add(10, 0)).toBe(10); // Identity element
    });

    // ✅ Test Case 4: บวก Decimal
    test("should add decimal numbers", () => {
      expect(add(1.5, 2.5)).toBeCloseTo(4.0);
      // ⚠️ ทำไม toBeCloseTo?
      // Float Math ไม่แน่นอน (0.1 + 0.2 = 0.30000000000000004 ❌)
      // toBeCloseTo() ยอมให้ Error เล็กน้อย
    });
  });

  // ✅ Test: subtract() ทำงานถูก
  describe("subtract()", () => {
    test("should subtract two numbers", () => {
      expect(subtract(10, 3)).toBe(7);
    });

    test("should handle negative result", () => {
      expect(subtract(3, 10)).toBe(-7); // ปกติ
    });
```

    });

});

// ✅ Test: multiply() ทำงานถูก
describe("multiply()", () => {
test("should multiply two positive numbers", () => {
expect(multiply(4, 5)).toBe(20);
});

    // ⚠️ ข้อเสีย: Float Precision
    test("should multiply decimal numbers", () => {
      expect(multiply(0.1, 0.2)).toBeCloseTo(0.02);
    });

});

// ✅ Test: divide() ทำงานถูก
describe("divide()", () => {
test("should divide two numbers", () => {
expect(divide(10, 2)).toBe(5);
});

    // 🔴 Test Edge Case: Divide by zero ต้อง Throw Error!
    test("should throw error when dividing by zero", () => {
      // ⚠️ ไม่ใช่: expect(divide(10, 0)).toBe(Infinity)
      // ✅ ถูก: ควร throw Error
      expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
    });

    // ⚠️ ตรวจสอบ Error Type
    test("should throw Error type when dividing by zero", () => {
      expect(() => divide(10, 0)).toThrow(Error);
    });

});
});

````

### 🏃 รัน Tests:

```bash
npm test
# ✅ PASS 7 tests
# ✅ All functions work correctly!
````

### 🎓 ข้อเรียนรู้:

| สิ่งที่เรียน          | ตัวอย่าง                       | ทำไมสำคัญ            |
| --------------------- | ------------------------------ | -------------------- |
| **Basic Test**        | `expect(add(2,3)).toBe(5)`     | ทดสอบ Happy Path     |
| **Edge Cases**        | `subtract(3, 10)` = -7         | จับ Negative Cases   |
| **Float Precision**   | `toBeCloseTo()` แทน `toBe()`   | Float Math ไม่แน่นอน |
| **Error Handling**    | `expect(() => fn()).toThrow()` | Throw Error ถูก?     |
| **Test Organization** | `describe()` + `test()`        | Code ชัดเจน          |

---

## 2. ตัวอย่างที่ 2: การ Validate Input

### 📝 โจทย์: Validate Form Data

**โค้ดจริง** (`src/validator.js`):

```javascript
// 🎯 Validate Email Format
function validateEmail(email) {
  // Simple regex สำหรับ Email validation
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// 🎯 Validate Password Strength
function validatePassword(password) {
  // Password ต้อง: ≥ 8 characters, มี number, มี uppercase
  if (password.length < 8) {
    return { valid: false, reason: "Password too short (min 8)" };
  }

  if (!/\d/.test(password)) {
    return { valid: false, reason: "Password must contain a number" };
  }

  if (!/[A-Z]/.test(password)) {
    return { valid: false, reason: "Password must contain uppercase" };
  }

  return { valid: true, reason: "Password is strong" };
}

// 🎯 Validate Username
function validateUsername(username) {
  // Username: 3-20 characters, alphanumeric + underscore
  const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;

  if (!usernameRegex.test(username)) {
    throw new Error("Invalid username format");
  }

  return true;
}

module.exports = { validateEmail, validatePassword, validateUsername };
```

### ✅ Test File (`tests/validator.test.js`):

```javascript
const {
  validateEmail,
  validatePassword,
  validateUsername,
} = require("../src/validator");

describe("Input Validation", () => {
  describe("validateEmail()", () => {
    // ✅ Valid Emails
    test("should accept valid email", () => {
      expect(validateEmail("user@example.com")).toBe(true);
      expect(validateEmail("john.doe@company.co.uk")).toBe(true);
    });

    // ❌ Invalid Emails
    test("should reject email without @", () => {
      expect(validateEmail("userexample.com")).toBe(false);
    });

    test("should reject email without domain", () => {
      expect(validateEmail("user@")).toBe(false);
    });

    test("should reject email with space", () => {
      expect(validateEmail("user @example.com")).toBe(false);
    });

    // 🔴 Edge Cases
    test("should reject empty string", () => {
      expect(validateEmail("")).toBe(false);
    });

    test("should reject email with multiple @", () => {
      expect(validateEmail("user@@example.com")).toBe(false);
    });
  });

  describe("validatePassword()", () => {
    // ✅ Valid Password
    test("should accept strong password", () => {
      const result = validatePassword("MyPassword123");
      expect(result.valid).toBe(true);
      expect(result.reason).toBe("Password is strong");
    });

    // ❌ Too Short
    test("should reject password < 8 chars", () => {
      const result = validatePassword("Pass1");
      expect(result.valid).toBe(false);
      expect(result.reason).toContain("too short");
    });

    // ❌ No Uppercase
    test("should reject password without uppercase", () => {
      const result = validatePassword("password123");
      expect(result.valid).toBe(false);
      expect(result.reason).toContain("uppercase");
    });

    // ❌ No Number
    test("should reject password without number", () => {
      const result = validatePassword("PasswordOnly");
      expect(result.valid).toBe(false);
      expect(result.reason).toContain("number");
    });

    // 🎓 ทำไมแบบนี้?
    // - ตรวจสอบ .valid AND .reason
    // - ตรวจสอบ Exact Message (หรือ toContain)
  });

  describe("validateUsername()", () => {
    // ✅ Valid Usernames
    test("should accept valid username", () => {
      expect(validateUsername("john_doe")).toBe(true);
      expect(validateUsername("user123")).toBe(true);
    });

    // ❌ Too Short
    test("should throw error for username < 3 chars", () => {
      expect(() => validateUsername("ab")).toThrow();
    });

    // ❌ Too Long
    test("should throw error for username > 20 chars", () => {
      expect(() => validateUsername("verylongusernamethat")).toThrow();
    });

    // ❌ Invalid Characters
    test("should throw error for username with special chars", () => {
      expect(() => validateUsername("user-name")).toThrow();
      expect(() => validateUsername("user@123")).toThrow();
    });

    // ❌ Space
    test("should throw error for username with space", () => {
      expect(() => validateUsername("user name")).toThrow();
    });
  });
});
```

### 🏃 รัน Tests:

```bash
npm test validator
# ✅ PASS 20 tests
# ✅ All validations work!
```

### 🎓 ข้อเรียนรู้:

| Concept                       | Example                                             | ทำไมสำคัญ                        |
| ----------------------------- | --------------------------------------------------- | -------------------------------- |
| **Test Multiple Cases**       | 6 email cases                                       | Regex ต้องครอบคลุม Edge Cases    |
| **Test Both Valid & Invalid** | .toBe(true/false)                                   | ส่วนใหญ่ Test สำหรับ Invalid     |
| **Check Error Messages**      | `.toContain("uppercase")`                           | User ต้อง รู้ว่า Error เพราะอะไร |
| **Throw vs Return**           | `validateUsername` throw, `validatePassword` return | Design Impact!                   |

---

## 3. ตัวอย่างที่ 3: Object & Methods

### 📝 โจทย์: User Account Management

**โค้ดจริง** (`src/user.js`):

```javascript
class User {
  constructor(name, email, age) {
    this.name = name;
    this.email = email;
    this.age = age;
    this.createdAt = new Date();
  }

  // 🎯 ตรวจสอบว่า User เป็น Adult ไหม
  isAdult() {
    return this.age >= 18;
  }

  // 🎯 Update User Profile
  updateProfile(updates) {
    if (updates.name) {
      this.name = updates.name;
    }
    if (updates.email) {
      this.email = updates.email;
    }
    if (updates.age !== undefined) {
      this.age = updates.age;
    }
    return this;
  }

  // 🎯 Get User Info as Object
  getInfo() {
    return {
      name: this.name,
      email: this.email,
      age: this.age,
      isAdult: this.isAdult(),
      createdAt: this.createdAt,
    };
  }

  // 🎯 Check if User can Access Adult Content
  canAccessAdultContent() {
    return this.isAdult();
  }
}

module.exports = User;
```

### ✅ Test File (`tests/user.test.js`):

```javascript
const User = require("../src/user");

describe("User Class", () => {
  // 🏗️ Setup - สร้าง Test User ก่อนแต่ละ Test
  let user;

  beforeEach(() => {
    // ⚠️ ทำไมต้อง beforeEach?
    // - แต่ละ Test ต้องได้ User ใหม่ (ไม่ share state)
    // - ถ้า Test 1 เปลี่ยน user.age Test 2 จะได้ค่าผิด
    user = new User("John Doe", "john@example.com", 25);
  });

  // ✅ Constructor & Properties
  describe("Constructor", () => {
    test("should create user with correct properties", () => {
      expect(user.name).toBe("John Doe");
      expect(user.email).toBe("john@example.com");
      expect(user.age).toBe(25);
    });

    test("should set createdAt timestamp", () => {
      // ⚠️ ปัญหา: createdAt = new Date() จะเปลี่ยนทุกครั้ง!
      // ✅ แก้: ใช้ toEqual + เช็ค Type
      expect(user.createdAt).toBeInstanceOf(Date);
      expect(user.createdAt.getTime()).toBeLessThanOrEqual(Date.now());
    });
  });

  // ✅ isAdult() Method
  describe("isAdult()", () => {
    test("should return true for age >= 18", () => {
      expect(user.isAdult()).toBe(true);
    });

    test("should return false for age < 18", () => {
      const teenager = new User("Jane", "jane@example.com", 16);
      expect(teenager.isAdult()).toBe(false);
    });

    // 🔴 Edge Case: Age = 18 exactly
    test("should return true for age = 18 (boundary)", () => {
      const adult = new User("Bob", "bob@example.com", 18);
      expect(adult.isAdult()).toBe(true);
    });
  });

  // ✅ updateProfile() Method
  describe("updateProfile()", () => {
    test("should update name when provided", () => {
      user.updateProfile({ name: "Jane Doe" });
      expect(user.name).toBe("Jane Doe");
    });

    test("should update email when provided", () => {
      user.updateProfile({ email: "jane@example.com" });
      expect(user.email).toBe("jane@example.com");
    });

    test("should update age when provided", () => {
      user.updateProfile({ age: 30 });
      expect(user.age).toBe(30);
    });

    // ✅ Partial Update (อัปเดตบางส่วน)
    test("should update only provided fields", () => {
      const originalEmail = user.email;
      user.updateProfile({ name: "New Name" });

      expect(user.name).toBe("New Name");
      expect(user.email).toBe(originalEmail); // ไม่เปลี่ยน
    });

    // ⚠️ Method Chaining
    test("should return this for method chaining", () => {
      const result = user.updateProfile({ name: "Updated" });
      expect(result).toBe(user); // return this
    });

    // 🎓 ทำไม test method chaining?
    // - ถ้า API อนุญาต chaining ต้อง test!
    // - user.updateProfile({name}).updateProfile({age}) ต้องทำงาน
  });

  // ✅ getInfo() Method
  describe("getInfo()", () => {
    test("should return user info object", () => {
      const info = user.getInfo();

      expect(info).toEqual({
        name: "John Doe",
        email: "john@example.com",
        age: 25,
        isAdult: true,
        createdAt: user.createdAt,
      });
    });

    // ⚠️ toEqual vs toBe
    // - toBe: ตรวจสอบ Reference (===)
    // - toEqual: ตรวจสอบ Value (deep equal)
    test("should return new object (not reference)", () => {
      const info = user.getInfo();
      expect(info).not.toBe(user); // ต่างกัน
      expect(info.name).toBe(user.name); // แต่ value เหมือน
    });
  });

  // ✅ canAccessAdultContent() Method
  describe("canAccessAdultContent()", () => {
    test("should return true for age >= 18", () => {
      expect(user.canAccessAdultContent()).toBe(true);
    });

    test("should return false for age < 18", () => {
      const kid = new User("Kid", "kid@example.com", 10);
      expect(kid.canAccessAdultContent()).toBe(false);
    });
  });
});
```

### 🏃 รัน Tests:

```bash
npm test user
# ✅ PASS 15 tests
```

### 🎓 ข้อเรียนรู้:

| Concept              | Example                             | ทำไมสำคัญ                             |
| -------------------- | ----------------------------------- | ------------------------------------- |
| **beforeEach()**     | ทำการ setup ก่อนแต่ละ test          | Test ต้อง Isolated ไม่ share state    |
| **toBe vs toEqual**  | `toBe`: Reference, `toEqual`: Value | Objects ต้องใช้ `toEqual`             |
| **Boundary Testing** | age = 18 exactly                    | Off-by-one errors มักเกิดที่ boundary |
| **Method Chaining**  | return this                         | Test ฟีเจอร์ที่ API support           |
| **Date Testing**     | `toBeInstanceOf(Date)`              | Dynamic values ต้องสอบ Type           |

---

## 4. ตัวอย่างที่ 4: Async & Await (API Call)

### 📝 โจทย์: Fetch Book Data from API

**โค้ดจริง** (`src/bookService.js`):

```javascript
// 🎯 Fetch Book by ID from API
async function getBookById(bookId) {
  // ⚠️ Assume API URL
  const url = `https://api.example.com/books/${bookId}`;

  const response = await fetch(url);

  // Handle Error Response
  if (!response.ok) {
    throw new Error(`Book not found (Status ${response.status})`);
  }

  const data = await response.json();
  return data;
}

// 🎯 Search Books by Query
async function searchBooks(query) {
  if (!query || query.trim() === "") {
    throw new Error("Search query cannot be empty");
  }

  const url = `https://api.example.com/books/search?q=${encodeURIComponent(query)}`;
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("Search failed");
  }

  const { results } = await response.json();
  return results;
}

// 🎯 Fetch Multiple Books
async function getMultipleBooks(bookIds) {
  // Promise.all = รอให้ทุก Promise เสร็จ (Parallel)
  const promises = bookIds.map((id) => getBookById(id));
  return Promise.all(promises);
}

module.exports = { getBookById, searchBooks, getMultipleBooks };
```

### ✅ Test File (`tests/bookService.test.js`):

```javascript
const {
  getBookById,
  searchBooks,
  getMultipleBooks,
} = require("../src/bookService");

// 🎯 Mock global fetch (ไม่ต้องจริง API Call!)
describe("Book Service - Async Operations", () => {
  // ⚠️ Mock fetch ก่อนแต่ละ test
  beforeEach(() => {
    global.fetch = jest.fn();
  });

  // 🧹 Clean up หลังแต่ละ test
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe("getBookById()", () => {
    // ✅ Test: Fetch Book Successfully
    test("should fetch book data successfully", async () => {
      // 📝 Mock API Response
      const mockBook = {
        id: 1,
        title: "JavaScript Guide",
        author: "John Doe",
        pages: 350,
      };

      global.fetch.mockResolvedValueOnce({
        ok: true,
        json: jest.fn().mockResolvedValueOnce(mockBook),
      });

      // 🏃 Call Function
      const book = await getBookById(1);

      // ✅ Assert Result
      expect(book).toEqual(mockBook);
      expect(book.title).toBe("JavaScript Guide");
    });

    // ✅ Verify Fetch was Called with Correct URL
    test("should call fetch with correct URL", async () => {
      const mockBook = { id: 1, title: "Test Book" };

      global.fetch.mockResolvedValueOnce({
        ok: true,
        json: jest.fn().mockResolvedValueOnce(mockBook),
      });

      await getBookById(123);

      // 🔍 Verify fetch was called
      expect(global.fetch).toHaveBeenCalledWith(
        "https://api.example.com/books/123",
      );
      expect(global.fetch).toHaveBeenCalledTimes(1);
    });

    // ❌ Test: API Error (404 Not Found)
    test("should throw error when book not found", async () => {
      global.fetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
      });

      // ⚠️ async test ต้องมี await
      await expect(getBookById(999)).rejects.toThrow("Book not found");
    });

    // ❌ Test: Network Error
    test("should throw error on network failure", async () => {
      global.fetch.mockRejectedValueOnce(new Error("Network error"));

      await expect(getBookById(1)).rejects.toThrow("Network error");
    });
  });

  describe("searchBooks()", () => {
    // ❌ Test: Empty Query
    test("should throw error for empty query", async () => {
      // ⚠️ ต้อง await แม้ว่าจะ throw ทันที
      await expect(searchBooks("")).rejects.toThrow(
        "Search query cannot be empty",
      );
      await expect(searchBooks("   ")).rejects.toThrow(); // whitespace
    });

    // ✅ Test: Successful Search
    test("should return search results", async () => {
      const mockResults = [
        { id: 1, title: "Book 1" },
        { id: 2, title: "Book 2" },
      ];

      global.fetch.mockResolvedValueOnce({
        ok: true,
        json: jest.fn().mockResolvedValueOnce({ results: mockResults }),
      });

      const results = await searchBooks("javascript");

      expect(results).toEqual(mockResults);
      expect(results).toHaveLength(2);
    });

    // ✅ Verify URL Encoding
    test("should properly encode search query", async () => {
      global.fetch.mockResolvedValueOnce({
        ok: true,
        json: jest.fn().mockResolvedValueOnce({ results: [] }),
      });

      await searchBooks("hello world");

      expect(global.fetch).toHaveBeenCalledWith(
        "https://api.example.com/books/search?q=hello%20world",
      );
    });
  });

  describe("getMultipleBooks()", () => {
    // ✅ Test: Fetch Multiple Books in Parallel
    test("should fetch multiple books", async () => {
      const mockBooks = [
        { id: 1, title: "Book 1" },
        { id: 2, title: "Book 2" },
        { id: 3, title: "Book 3" },
      ];

      // Mock fetch สำหรับแต่ละ call
      mockBooks.forEach((book) => {
        global.fetch.mockResolvedValueOnce({
          ok: true,
          json: jest.fn().mockResolvedValueOnce(book),
        });
      });

      const books = await getMultipleBooks([1, 2, 3]);

      expect(books).toEqual(mockBooks);
      expect(global.fetch).toHaveBeenCalledTimes(3); // 3 calls
    });

    // ❌ Test: One Book Fails (Promise.all behavior)
    test("should fail if one book fetch fails", async () => {
      // 2 Success
      global.fetch.mockResolvedValueOnce({
        ok: true,
        json: jest.fn().mockResolvedValueOnce({ id: 1, title: "Book 1" }),
      });

      // 1 Fail
      global.fetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
      });

      // Promise.all rejects หากมี error
      await expect(getMultipleBooks([1, 2, 3])).rejects.toThrow();
    });
  });
});
```

### 🏃 รัน Tests:

```bash
npm test bookService
# ✅ PASS 12 tests
# ✅ No actual API calls!
```

### 🎓 ข้อเรียนรู้:

| Concept                    | Example              | ทำไมสำคัญ                |
| -------------------------- | -------------------- | ------------------------ |
| **jest.fn()**              | Mock fetch           | ไม่ต้องจริง API Call     |
| **mockResolvedValue**      | Successful Response  | Test Happy Path          |
| **mockRejectedValue**      | Network Error        | Test Error Cases         |
| **await expect().rejects** | Async Error Testing  | Async Tests ต้องนี่แหละ! |
| **jest.clearAllMocks()**   | Reset after test     | ไม่ให้ Mock interfere    |
| **toHaveBeenCalled**       | Verify function call | Test Side Effects        |

---

## 5. ตัวอย่างที่ 5: Mocking & Spying

### 📝 โจทย์: Email Notification Service

**โค้ดจริง** (`src/emailService.js`):

```javascript
// External Service (ปกติจะจริง)
class EmailProvider {
  static send(to, subject, body) {
    // จริง: เชื่อมต่อ SMTP Server
    throw new Error("Should use mock in tests!");
  }
}

// Our Service
class EmailService {
  constructor() {
    this.sentEmails = [];
  }

  // 🎯 Send Email
  sendEmail(to, subject, body) {
    // ⚠️ Call External Service
    EmailProvider.send(to, subject, body);

    // Track what we sent
    this.sentEmails.push({ to, subject, body, sentAt: new Date() });

    return { success: true, to, subject };
  }

  // 🎯 Send to Multiple Users
  sendBulkEmail(recipients, subject, body) {
    let successCount = 0;

    for (const email of recipients) {
      try {
        this.sendEmail(email, subject, body);
        successCount++;
      } catch (error) {
        console.error(`Failed to send to ${email}:`, error);
      }
    }

    return { successCount, total: recipients.length };
  }

  // 🎯 Get Sent Emails History
  getSentEmails() {
    return this.sentEmails;
  }
}

module.exports = { EmailService, EmailProvider };
```

### ✅ Test File (`tests/emailService.test.js`):

```javascript
const { EmailService, EmailProvider } = require("../src/emailService");

describe("Email Service - Mocking & Spying", () => {
  let emailService;

  beforeEach(() => {
    emailService = new EmailService();
    // Mock EmailProvider.send()
    jest.spyOn(EmailProvider, "send").mockImplementation(() => {
      // ทำเป็น Success
    });
  });

  afterEach(() => {
    jest.restoreAllMocks(); // Restore original implementation
  });

  describe("sendEmail()", () => {
    // ✅ Test: Send Email Successfully
    test("should send email and track it", () => {
      emailService.sendEmail("user@example.com", "Hello", "Body");

      // ✅ Check return value
      expect(
        emailService.sendEmail("test@example.com", "Subject", "Body"),
      ).toEqual({ success: true, to: "test@example.com", subject: "Subject" });
    });

    // ✅ Test: Verify EmailProvider was Called
    test("should call EmailProvider.send with correct args", () => {
      emailService.sendEmail(
        "john@example.com",
        "Welcome",
        "Welcome to our site",
      );

      expect(EmailProvider.send).toHaveBeenCalledWith(
        "john@example.com",
        "Welcome",
        "Welcome to our site",
      );
    });

    // ✅ Test: Track Sent Email History
    test("should add email to history", () => {
      emailService.sendEmail("user@example.com", "Test", "Test Body");

      const history = emailService.getSentEmails();
      expect(history).toHaveLength(1);
      expect(history[0].to).toBe("user@example.com");
      expect(history[0].subject).toBe("Test");
    });

    // ⚠️ Test: timestamp
    test("should record send timestamp", () => {
      const before = Date.now();
      emailService.sendEmail("test@example.com", "Subject", "Body");
      const after = Date.now();

      const email = emailService.getSentEmails()[0];
      const sentTime = email.sentAt.getTime();

      expect(sentTime).toBeGreaterThanOrEqual(before);
      expect(sentTime).toBeLessThanOrEqual(after);
    });
  });

  describe("sendBulkEmail()", () => {
    // ✅ Test: Send to Multiple Recipients
    test("should send to all recipients", () => {
      const recipients = [
        "user1@example.com",
        "user2@example.com",
        "user3@example.com",
      ];
      const result = emailService.sendBulkEmail(
        recipients,
        "Newsletter",
        "News content",
      );

      expect(result).toEqual({ successCount: 3, total: 3 });
      expect(EmailProvider.send).toHaveBeenCalledTimes(3);
    });

    // ✅ Test: Track Each Email
    test("should track all sent emails", () => {
      const recipients = ["a@example.com", "b@example.com"];
      emailService.sendBulkEmail(recipients, "Subject", "Body");

      const history = emailService.getSentEmails();
      expect(history).toHaveLength(2);
      expect(history[0].to).toBe("a@example.com");
      expect(history[1].to).toBe("b@example.com");
    });

    // ❌ Test: Handle Error Cases
    test("should handle email send failures gracefully", () => {
      const recipients = ["good@example.com", "bad@example.com"];

      // Make 2nd call fail
      EmailProvider.send.mockImplementationOnce(() => {
        // 1st call: success
      });

      EmailProvider.send.mockImplementationOnce(() => {
        throw new Error("SMTP Error");
      });

      // ⚠️ Spy on console.error
      jest.spyOn(console, "error").mockImplementation(() => {});

      const result = emailService.sendBulkEmail(recipients, "Subject", "Body");

      expect(result.successCount).toBe(1);
      expect(result.total).toBe(2);

      // 🎓 ทำไม spy on console.error?
      // - ต้องตรวจสอบว่า Error logging ทำงาน
      console.error.mockRestore();
    });
  });

  describe("Spy & Mock Verification", () => {
    // 🔍 Test: Verify Exact Call Count
    test("should call EmailProvider exactly as many times as recipients", () => {
      emailService.sendEmail("a@example.com", "Subject", "Body");
      emailService.sendEmail("b@example.com", "Subject", "Body");

      expect(EmailProvider.send).toHaveBeenCalledTimes(2);
    });

    // 🔍 Test: Verify First vs Last Call
    test("should call with correct first call args", () => {
      emailService.sendEmail("first@example.com", "First", "Body");
      emailService.sendEmail("second@example.com", "Second", "Body");

      const calls = EmailProvider.send.mock.calls;
      expect(calls[0][0]).toBe("first@example.com");
      expect(calls[1][0]).toBe("second@example.com");
    });
  });
});
```

### 🏃 รัน Tests:

```bash
npm test emailService
# ✅ PASS 12 tests
```

### 🎓 ข้อเรียนรู้:

| Concept                    | Example                   | ทำไมสำคัญ                                    |
| -------------------------- | ------------------------- | -------------------------------------------- |
| **jest.spyOn()**           | Spy + Mock External       | ทดสอบ Internal Logic โดยไม่ต้อง Real Service |
| **mockImplementation()**   | Control function behavior | Simulate Success/Failure                     |
| **toHaveBeenCalled**       | Verify side effects       | Test ว่า External Function ถูก call          |
| **jest.restoreAllMocks()** | Cleanup                   | ป้องกัน Mock interfere tests                 |
| **console.error spy**      | Verify error logging      | Test Error Handling Path                     |

---

## 6. ตัวอย่างที่ 6: Class Testing (User Account)

### 📝 โจทย์: Bank Account Management

**โค้ดจริง** (`src/bankAccount.js`):

```javascript
class BankAccount {
  constructor(accountHolder, initialBalance = 0) {
    this.accountHolder = accountHolder;
    this.balance = initialBalance;
    this.transactions = [];

    if (initialBalance > 0) {
      this.transactions.push({
        type: "initial",
        amount: initialBalance,
        date: new Date(),
      });
    }
  }

  // 🎯 Deposit Money
  deposit(amount) {
    if (amount <= 0) {
      throw new Error("Deposit amount must be positive");
    }

    this.balance += amount;
    this.transactions.push({
      type: "deposit",
      amount,
      date: new Date(),
    });

    return this.balance;
  }

  // 🎯 Withdraw Money
  withdraw(amount) {
    if (amount <= 0) {
      throw new Error("Withdraw amount must be positive");
    }

    if (amount > this.balance) {
      throw new Error("Insufficient balance");
    }

    this.balance -= amount;
    this.transactions.push({
      type: "withdraw",
      amount,
      date: new Date(),
    });

    return this.balance;
  }

  // 🎯 Get Account Balance
  getBalance() {
    return this.balance;
  }

  // 🎯 Transfer to Another Account
  transferTo(otherAccount, amount) {
    if (!(otherAccount instanceof BankAccount)) {
      throw new Error("Invalid recipient account");
    }

    this.withdraw(amount); // May throw
    otherAccount.deposit(amount);

    return true;
  }

  // 🎯 Get Transaction History
  getTransactionHistory() {
    return [...this.transactions]; // Return copy
  }
}

module.exports = BankAccount;
```

### ✅ Test File (`tests/bankAccount.test.js`):

```javascript
const BankAccount = require("../src/bankAccount");

describe("Bank Account Class", () => {
  let account;

  beforeEach(() => {
    account = new BankAccount("John Doe", 1000);
  });

  describe("Constructor", () => {
    test("should create account with correct properties", () => {
      expect(account.accountHolder).toBe("John Doe");
      expect(account.getBalance()).toBe(1000);
    });

    test("should initialize with zero balance if not specified", () => {
      const emptyAccount = new BankAccount("Jane");
      expect(emptyAccount.getBalance()).toBe(0);
    });

    test("should record initial deposit in transactions", () => {
      const history = account.getTransactionHistory();
      expect(history).toHaveLength(1);
      expect(history[0].type).toBe("initial");
      expect(history[0].amount).toBe(1000);
    });

    test("should not add transaction if initial balance is 0", () => {
      const emptyAccount = new BankAccount("Jane", 0);
      expect(emptyAccount.getTransactionHistory()).toHaveLength(0);
    });
  });

  describe("deposit()", () => {
    test("should add positive amount to balance", () => {
      const newBalance = account.deposit(500);
      expect(newBalance).toBe(1500);
      expect(account.getBalance()).toBe(1500);
    });

    test("should record deposit transaction", () => {
      account.deposit(200);
      const history = account.getTransactionHistory();

      const lastTransaction = history[history.length - 1];
      expect(lastTransaction.type).toBe("deposit");
      expect(lastTransaction.amount).toBe(200);
    });

    test("should throw error for negative amount", () => {
      expect(() => account.deposit(-100)).toThrow(
        "Deposit amount must be positive",
      );
    });

    test("should throw error for zero amount", () => {
      expect(() => account.deposit(0)).toThrow(
        "Deposit amount must be positive",
      );
    });

    // ⚠️ Test return value
    test("should return new balance after deposit", () => {
      expect(account.deposit(100)).toBe(1100);
      expect(account.deposit(200)).toBe(1300);
    });
  });

  describe("withdraw()", () => {
    test("should deduct amount from balance", () => {
      const newBalance = account.withdraw(300);
      expect(newBalance).toBe(700);
      expect(account.getBalance()).toBe(700);
    });

    test("should record withdraw transaction", () => {
      account.withdraw(100);
      const lastTransaction =
        account.getTransactionHistory()[
          account.getTransactionHistory().length - 1
        ];

      expect(lastTransaction.type).toBe("withdraw");
      expect(lastTransaction.amount).toBe(100);
    });

    test("should throw error for negative amount", () => {
      expect(() => account.withdraw(-50)).toThrow(
        "Withdraw amount must be positive",
      );
    });

    test("should throw error for zero amount", () => {
      expect(() => account.withdraw(0)).toThrow(
        "Withdraw amount must be positive",
      );
    });

    // ❌ Insufficient Balance
    test("should throw error if balance is insufficient", () => {
      expect(() => account.withdraw(2000)).toThrow("Insufficient balance");
    });

    // ⚠️ Boundary: Withdraw exact balance
    test("should allow withdrawal of exact balance", () => {
      const newBalance = account.withdraw(1000);
      expect(newBalance).toBe(0);
    });

    // ⚠️ Boundary: Withdraw just below balance
    test("should not allow withdrawal above balance", () => {
      expect(() => account.withdraw(1001)).toThrow("Insufficient balance");
    });

    // 🎓 ทำไมต้อง test boundary?
    // - off-by-one errors มักเกิดที่ boundary (1000 vs 1001)
  });

  describe("transferTo()", () => {
    let recipientAccount;

    beforeEach(() => {
      recipientAccount = new BankAccount("Jane Doe", 500);
    });

    test("should transfer money to another account", () => {
      account.transferTo(recipientAccount, 300);

      expect(account.getBalance()).toBe(700);
      expect(recipientAccount.getBalance()).toBe(800);
    });

    test("should record transactions in both accounts", () => {
      account.transferTo(recipientAccount, 200);

      const senderHistory = account.getTransactionHistory();
      const recipientHistory = recipientAccount.getTransactionHistory();

      expect(senderHistory[senderHistory.length - 1].type).toBe("withdraw");
      expect(recipientHistory[recipientHistory.length - 1].type).toBe(
        "deposit",
      );
    });

    // ❌ Invalid Recipient
    test("should throw error for invalid recipient", () => {
      expect(() => account.transferTo({}, 100)).toThrow(
        "Invalid recipient account",
      );
      expect(() => account.transferTo(null, 100)).toThrow(
        "Invalid recipient account",
      );
      expect(() => account.transferTo("not an account", 100)).toThrow(
        "Invalid recipient account",
      );
    });

    // ❌ Insufficient Balance
    test("should throw error if insufficient balance for transfer", () => {
      expect(() => account.transferTo(recipientAccount, 2000)).toThrow(
        "Insufficient balance",
      );

      // Verify no transaction occurred
      expect(account.getBalance()).toBe(1000); // unchanged
      expect(recipientAccount.getBalance()).toBe(500); // unchanged
    });

    test("should not partially transfer if it fails", () => {
      const beforeBalance = account.getBalance();

      try {
        account.transferTo(recipientAccount, 2000);
      } catch (error) {
        // Error is expected
      }

      // Verify no change
      expect(account.getBalance()).toBe(beforeBalance);
    });
  });

  describe("getTransactionHistory()", () => {
    test("should return copy, not reference", () => {
      const history1 = account.getTransactionHistory();
      history1.push({ fake: "transaction" }); // Modify copy

      const history2 = account.getTransactionHistory();
      expect(history2).toHaveLength(1); // Original unchanged
    });

    test("should show all transactions in order", () => {
      account.deposit(100);
      account.withdraw(50);
      account.deposit(200);

      const history = account.getTransactionHistory();
      expect(history).toHaveLength(4);
      expect(history[0].type).toBe("initial");
      expect(history[1].type).toBe("deposit");
      expect(history[2].type).toBe("withdraw");
      expect(history[3].type).toBe("deposit");
    });
  });
});
```

### 🏃 รัน Tests:

```bash
npm test bankAccount
# ✅ PASS 25 tests
```

### 🎓 ข้อเรียนรู้:

| Concept                   | Example                          | ทำไมสำคัญ                     |
| ------------------------- | -------------------------------- | ----------------------------- |
| **Boundary Testing**      | withdraw(1000) vs withdraw(1001) | Off-by-one errors มักเกิด     |
| **State Testing**         | Check balance after operation    | ต้องแน่ใจว่า State ถูก        |
| **Transaction Recording** | Verify in history                | Side effects ต้อง test        |
| **Error States**          | Insufficient balance             | ต้องทดสอบ Error Path          |
| **Data Encapsulation**    | Return copy ไม่ reference        | ป้องกัน External modification |
| **Transaction Rollback**  | ไม่ให้ Partial Transfer          | Atomicity สำคัญ               |

---

## 7. ตัวอย่างที่ 7: Library Management System (Real Project)

### 📝 โจทย์: Complete Book Management System

**โค้ดจริง** (`src/library.js`):

```javascript
class Library {
  constructor(name) {
    this.name = name;
    this.books = [];
    this.borrowHistory = [];
  }

  // 🎯 Add Book to Library
  addBook(bookId, title, author, copies) {
    if (!bookId || !title || !author) {
      throw new Error("Book details are required");
    }

    if (copies <= 0) {
      throw new Error("Copies must be positive");
    }

    const existingBook = this.books.find((b) => b.id === bookId);
    if (existingBook) {
      throw new Error("Book already exists");
    }

    this.books.push({
      id: bookId,
      title,
      author,
      copies,
      available: copies,
    });
  }

  // 🎯 Borrow Book
  borrowBook(bookId, memberId) {
    const book = this.books.find((b) => b.id === bookId);

    if (!book) {
      throw new Error("Book not found");
    }

    if (book.available === 0) {
      throw new Error("Book not available");
    }

    book.available--;

    this.borrowHistory.push({
      bookId,
      memberId,
      action: "borrow",
      date: new Date(),
    });

    return true;
  }

  // 🎯 Return Book
  returnBook(bookId, memberId) {
    const book = this.books.find((b) => b.id === bookId);

    if (!book) {
      throw new Error("Book not found");
    }

    if (book.available >= book.copies) {
      throw new Error("Cannot return more copies than total");
    }

    book.available++;

    this.borrowHistory.push({
      bookId,
      memberId,
      action: "return",
      date: new Date(),
    });

    return true;
  }

  // 🎯 Get Book Info
  getBook(bookId) {
    return this.books.find((b) => b.id === bookId);
  }

  // 🎯 Get Available Books
  getAvailableBooks() {
    return this.books.filter((b) => b.available > 0);
  }

  // 🎯 Search Books by Author
  searchByAuthor(author) {
    return this.books.filter((b) =>
      b.author.toLowerCase().includes(author.toLowerCase()),
    );
  }
}

module.exports = Library;
```

### ✅ Test File (`tests/library.test.js`):

```javascript
const Library = require("../src/library");

describe("Library Management System", () => {
  let library;

  beforeEach(() => {
    library = new Library("City Library");
  });

  describe("Library Initialization", () => {
    test("should create library with name", () => {
      expect(library.name).toBe("City Library");
      expect(library.books).toHaveLength(0);
    });
  });

  describe("addBook()", () => {
    test("should add book to library", () => {
      library.addBook(1, "JavaScript Guide", "John Doe", 5);

      expect(library.books).toHaveLength(1);
      const book = library.getBook(1);
      expect(book.title).toBe("JavaScript Guide");
      expect(book.copies).toBe(5);
      expect(book.available).toBe(5);
    });

    test("should add multiple books", () => {
      library.addBook(1, "Book 1", "Author 1", 3);
      library.addBook(2, "Book 2", "Author 2", 2);

      expect(library.books).toHaveLength(2);
    });

    // ❌ Validation Tests
    test("should throw error if book ID is missing", () => {
      expect(() => library.addBook(null, "Title", "Author", 5)).toThrow(
        "Book details are required",
      );
    });

    test("should throw error if title is missing", () => {
      expect(() => library.addBook(1, null, "Author", 5)).toThrow(
        "Book details are required",
      );
    });

    test("should throw error for duplicate book ID", () => {
      library.addBook(1, "Book 1", "Author 1", 5);
      expect(() =>
        library.addBook(1, "Different Title", "Author 2", 3),
      ).toThrow("Book already exists");
    });

    test("should throw error for invalid copies", () => {
      expect(() => library.addBook(1, "Title", "Author", 0)).toThrow(
        "Copies must be positive",
      );
      expect(() => library.addBook(1, "Title", "Author", -5)).toThrow(
        "Copies must be positive",
      );
    });
  });

  describe("borrowBook()", () => {
    beforeEach(() => {
      library.addBook(1, "JavaScript Guide", "John Doe", 3);
      library.addBook(2, "Python Basics", "Jane Smith", 1);
    });

    test("should borrow book successfully", () => {
      const result = library.borrowBook(1, "member123");

      expect(result).toBe(true);
      expect(library.getBook(1).available).toBe(2);
    });

    test("should allow multiple users to borrow same book", () => {
      library.borrowBook(1, "member1");
      library.borrowBook(1, "member2");

      expect(library.getBook(1).available).toBe(1);
    });

    test("should record borrow in history", () => {
      library.borrowBook(1, "member123");

      expect(library.borrowHistory).toHaveLength(1);
      expect(library.borrowHistory[0]).toEqual({
        bookId: 1,
        memberId: "member123",
        action: "borrow",
        date: expect.any(Date),
      });
    });

    // ❌ Error Cases
    test("should throw error if book not found", () => {
      expect(() => library.borrowBook(999, "member123")).toThrow(
        "Book not found",
      );
    });

    test("should throw error if book not available", () => {
      library.borrowBook(2, "member1"); // Only 1 copy
      expect(() => library.borrowBook(2, "member2")).toThrow(
        "Book not available",
      );
    });

    test("should not borrow if no copies available", () => {
      library.borrowBook(2, "member1");
      const before = library.borrowHistory.length;

      try {
        library.borrowBook(2, "member2");
      } catch (error) {
        // Expected
      }

      // Verify no history added
      expect(library.borrowHistory).toHaveLength(before);
    });
  });

  describe("returnBook()", () => {
    beforeEach(() => {
      library.addBook(1, "Book 1", "Author 1", 2);
      library.borrowBook(1, "member1");
    });

    test("should return book successfully", () => {
      const result = library.returnBook(1, "member1");

      expect(result).toBe(true);
      expect(library.getBook(1).available).toBe(2);
    });

    test("should record return in history", () => {
      library.returnBook(1, "member1");

      const lastTransaction =
        library.borrowHistory[library.borrowHistory.length - 1];
      expect(lastTransaction.action).toBe("return");
    });

    // ❌ Error Cases
    test("should throw error if book not found", () => {
      expect(() => library.returnBook(999, "member1")).toThrow(
        "Book not found",
      );
    });

    test("should throw error if returning more copies than total", () => {
      expect(() => library.returnBook(1, "member1")).toThrow(
        "Cannot return more copies than total",
      );
    });
  });

  describe("getAvailableBooks()", () => {
    beforeEach(() => {
      library.addBook(1, "Book 1", "Author 1", 3);
      library.addBook(2, "Book 2", "Author 2", 1);
      library.addBook(3, "Book 3", "Author 3", 0);
    });

    test("should return only books with available copies", () => {
      const available = library.getAvailableBooks();

      expect(available).toHaveLength(2);
      expect(available.map((b) => b.id)).toEqual([1, 2]);
    });

    test("should not include sold out books", () => {
      const available = library.getAvailableBooks();

      expect(available.find((b) => b.id === 3)).toBeUndefined();
    });

    test("should return empty array if no books available", () => {
      library.borrowBook(1, "m1");
      library.borrowBook(1, "m2");
      library.borrowBook(1, "m3");
      library.borrowBook(2, "m4");

      const available = library.getAvailableBooks();

      expect(available).toHaveLength(0);
    });
  });

  describe("searchByAuthor()", () => {
    beforeEach(() => {
      library.addBook(1, "JavaScript Guide", "John Doe", 5);
      library.addBook(2, "Python Basics", "Jane Smith", 3);
      library.addBook(3, "Advanced JavaScript", "John Doe", 2);
    });

    test("should find books by author", () => {
      const books = library.searchByAuthor("John Doe");

      expect(books).toHaveLength(2);
      expect(books.map((b) => b.id)).toEqual([1, 3]);
    });

    test("should be case insensitive", () => {
      const books1 = library.searchByAuthor("john doe");
      const books2 = library.searchByAuthor("JOHN DOE");

      expect(books1).toHaveLength(2);
      expect(books2).toHaveLength(2);
    });

    test("should return empty array if no match", () => {
      const books = library.searchByAuthor("Unknown Author");

      expect(books).toHaveLength(0);
    });

    test("should match partial author name", () => {
      const books = library.searchByAuthor("John");

      expect(books).toHaveLength(2);
    });
  });

  describe("Integration Tests (Real Scenarios)", () => {
    beforeEach(() => {
      // Setup library with books
      library.addBook(1, "JavaScript: The Good Parts", "Douglas Crockford", 2);
      library.addBook(2, "Clean Code", "Robert C. Martin", 1);
    });

    test("scenario: borrow and return book", () => {
      // User borrows book
      library.borrowBook(1, "john_doe");
      expect(library.getBook(1).available).toBe(1);

      // User returns book
      library.returnBook(1, "john_doe");
      expect(library.getBook(1).available).toBe(2);

      // History shows both actions
      expect(library.borrowHistory).toHaveLength(2);
    });

    test("scenario: multiple users borrow, one returns", () => {
      library.borrowBook(1, "user1");
      library.borrowBook(1, "user2");
      expect(library.getBook(1).available).toBe(0);

      library.returnBook(1, "user1");
      expect(library.getBook(1).available).toBe(1);
    });

    test("scenario: track borrowing patterns", () => {
      const bookId = 1;

      // User1 borrow-return-borrow
      library.borrowBook(bookId, "user1");
      library.returnBook(bookId, "user1");
      library.borrowBook(bookId, "user1");

      // User2 borrow
      library.borrowBook(bookId, "user2");

      // Check all transactions
      const user1Transactions = library.borrowHistory.filter(
        (t) => t.memberId === "user1",
      );
      expect(user1Transactions).toHaveLength(3);

      // All copies are borrowed
      expect(library.getBook(bookId).available).toBe(0);
    });
  });
});
```

### 🏃 รัน Tests:

```bash
npm test library
# ✅ PASS 35 tests
# ✅ Complete Library System Tests!
```

### 🎓 ข้อเรียนรู้:

| Concept               | Example                    | ทำไมสำคัญ                   |
| --------------------- | -------------------------- | --------------------------- |
| **Integration Tests** | Borrow-Return scenario     | Test ทั้งหมด Flow ร่วมกัน   |
| **Setup/Cleanup**     | beforeEach with data       | ทุก test ต้อง Fresh State   |
| **Validation**        | Missing fields, Duplicates | ตรวจสอบ Input Validation    |
| **State Changes**     | Check available count      | Verify Side Effects         |
| **Error Paths**       | Sold out, Not found        | Test ทั้ง Happy + Sad paths |
| **Real Scenarios**    | Use Cases                  | Test ทำให้ Bug ปรากฏจริง    |

---

## 8. Common Patterns & Tips

### 🎯 Pattern 1: Test Organization (Arrange-Act-Assert)

```javascript
describe("Feature", () => {
  describe("Function A", () => {
    test("should do X", () => {
      // 1️⃣ ARRANGE - ตั้งค่า input
      const input = "hello";

      // 2️⃣ ACT - เรียก function
      const result = functionA(input);

      // 3️⃣ ASSERT - ตรวจสอบ output
      expect(result).toBe("expected");
    });

    test("should do Y", () => {});
  });

  describe("Function B", () => {
    test("should do X", () => {});
  });
});
```

**ทำไมต้อง:**

- ✅ Code ชัดเจน, ทุกคนเข้าใจได้
- ✅ Easy to read = Easy to debug
- ✅ Report ดีขึ้น (ทราบว่าที่ไหนพัง)
- ✅ Organized = Easier to maintain

**ข้อพึงระวัง:**

```javascript
// ❌ ไม่ดี: ทำหลายอย่างในครั้งเดียว
test("it works", () => {
  expect(fnA()).toBe(1);
  expect(fnB()).toBe(2);
  expect(fnC()).toBe(3);
  // ถ้า fnB fail หนึ่ง fn C ไม่รันตรวจสอบ
});

// ✅ ดี: แยกออก
test("fnA should return 1", () => {
  expect(fnA()).toBe(1);
});
test("fnB should return 2", () => {
  expect(fnB()).toBe(2);
});
test("fnC should return 3", () => {
  expect(fnC()).toBe(3);
});
// แต่ละ test fail จะรู้ว่าตัวไหนพัง
```

---

### 🎯 Pattern 2: Setup & Cleanup (beforeEach vs beforeAll)

```javascript
describe("User Service", () => {
  let user;

  // 🔄 beforeEach: รัน ก่อนแต่ละ test
  beforeEach(() => {
    // ✅ ทำได้: สร้าง Fresh User สำหรับแต่ละ Test
    user = new User("John", "john@example.com", 25);

    // ✅ ทำได้: Reset Mock
    jest.clearAllMocks();
  });

  // 🔄 afterEach: รัน หลังแต่ละ test
  afterEach(() => {
    // ✅ ทำได้: Clear database
    // ✅ ทำได้: Cleanup files
    jest.restoreAllMocks();
  });

  // 📌 beforeAll: รัน 1 ครั้งก่อน All Tests
  beforeAll(() => {
    // ✅ ทำได้: Start Database Server
    // ✅ ทำได้: Load Config
    console.log("Testing User Service");
  });

  // 📌 afterAll: รัน 1 ครั้งหลัง All Tests
  afterAll(() => {
    // ✅ ทำได้: Stop Database Server
    // ✅ ทำได้: Cleanup resources
    console.log("Done testing User Service");
  });

  test("should create user", () => {
    expect(user.name).toBe("John");
  });

  test("should update user", () => {
    // ⚠️ user ต่าง ๆ กัน (fresh จาก beforeEach)
    user.name = "Jane";
    expect(user.name).toBe("Jane");
  });
});
```

**เมื่อไหร่ใช้อัน?**

| สถานการณ์             | ใช้          | ทำไม                     |
| --------------------- | ------------ | ------------------------ |
| **Create Fresh Data** | `beforeEach` | ทุก Test ต้อง Isolated   |
| **Setup Database**    | `beforeAll`  | ไม่ต้องเสียเวลา Repeat   |
| **Mock API**          | `beforeEach` | ทุก Test ต้อง Clean Mock |
| **Load Config File**  | `beforeAll`  | ใช้ครั้งเดียว            |

---

### 🎯 Pattern 3: Skip & Focus Tests

```javascript
// 🏃 Skip test - ไม่รัน (เหมือน Comment Out)
test.skip("this test will not run", () => {
  // Useful: ทดสอบ Feature ที่ยัง Incomplete
  expect(1).toBe(2); // ไม่ error เพราะ skip
});

// 🔍 Only run this test - รัน เฉพาะอันนี้ (Debug)
test.only("only this runs", () => {
  // Useful: Debug 1 Test ตัวเดียว ไม่ต้องรันทั้งหมด
  expect(1).toBe(1); // ✅ PASS
});

// 📝 Todo test - Mark ว่า ยังไม่ implement
test.todo("implement this test later");
// Useful: Track tests ที่ ยังต้องเขียน
```

**⚠️ ข้อควรระวัง:**

```javascript
// ❌ ห้ามทำ: ลืม .skip ก่อน commit
test.skip("user can login", () => {
  // เพื่อน run test แล้วเห็นว่า pass ทั้งหมด
  // แต่จริง ๆ ลืม test login!
});

// ✅ ทำ: ใช้ CI/CD ให้เตือน
// package.json: "test:ci": "jest --detectOpenHandles"
```

---

### 🎯 Pattern 4: Parameterized Tests (หลายค่า)

```javascript
// ❌ ไม่ดี: ซ้ำ ๆ
test("should return 4 for input 2", () => {
  expect(multiply(2, 2)).toBe(4);
});
test("should return 6 for input 3", () => {
  expect(multiply(3, 2)).toBe(6);
});
test("should return 10 for input 5", () => {
  expect(multiply(5, 2)).toBe(10);
});

// ✅ ดี: ใช้ Parameterized
const testCases = [
  { input: 2, expected: 4 },
  { input: 3, expected: 6 },
  { input: 5, expected: 10 },
];

testCases.forEach(({ input, expected }) => {
  test(`should return ${expected} for input ${input}`, () => {
    expect(multiply(input, 2)).toBe(expected);
  });
});

// ✅ ดี: ใช้ test.each()
test.each([
  [2, 4],
  [3, 6],
  [5, 10],
])("multiply(%i, 2) should equal %i", (input, expected) => {
  expect(multiply(input, 2)).toBe(expected);
});
```

**ข้อดี:**

- ✅ DRY (Don't Repeat Yourself)
- ✅ ง่ายเพิ่ม Test Cases
- ✅ Report ชัดเจน (แต่ละ case)

---

### 🎯 Pattern 5: Mocking Best Practices

```javascript
// ✅ Good: Mock specific function
beforeEach(() => {
  jest.spyOn(Math, "random").mockReturnValue(0.5);
});

// ✅ Good: Restore after test
afterEach(() => {
  jest.restoreAllMocks();
});

// ✅ Good: Verify mock was called
test("should use random", () => {
  const result = getRandomUser();
  expect(Math.random).toHaveBeenCalled();
});

// ❌ Bad: Leaving mocks around
// jest.spyOn(Math, "random").mockReturnValue(0.5);
// ไม่ restore → Next test ใช้ Mock value ด้วย!
```

**Mock Lifecycle:**

```
beforeEach() {
  ✅ Create Mock
  ✅ Setup return values
}
  ↓
test() {
  ✅ Use Mock
  ✅ Verify calls
}
  ↓
afterEach() {
  ✅ Restore (ปล่อยให้ Mock หายไป)
  ✅ Cleanup
}
```

---

### 🎯 Pattern 6: Error Message Testing

```javascript
// ✅ Check exact error message
test("should throw with exact message", () => {
  expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
});

// ✅ Check partial message (more flexible)
test("should throw with keyword", () => {
  expect(() => divide(10, 0)).toThrow(/divide/);
  // Match: "Cannot divide by zero" ✅
  // Match: "divide error" ✅
  // Not match: "error" ❌ (ต้องมี "divide")
});

// ✅ Check Error type
test("should throw Error type", () => {
  expect(() => divide(10, 0)).toThrow(Error);
  expect(() => JSON.parse("invalid")).toThrow(SyntaxError);
});

// ✅ Check both type and message
test("should throw RangeError with message", () => {
  const fn = () => {
    throw new RangeError("Value out of range");
  };

  expect(fn).toThrow(RangeError);
  expect(fn).toThrow(/range/i); // case-insensitive
});
```

**ทำไมต้อง Test Error Messages?**

- ✅ Error Messages บอก User ว่า Error อะไร
- ✅ Developer นั่งหา Bug ช่วยจาก Message
- ✅ Quality = Good Error Messages

**ทำไม:** Error Messages บอก User ว่าเกิด Error อะไร

---

### 💡 Tips for Better Tests

#### 1️⃣ One Assertion Per Test (ปกติ)

```javascript
// ❌ ไม่ดี: Multiple assertions
test("should handle user data", () => {
  const user = new User("John", "john@example.com", 25);
  expect(user.name).toBe("John"); // Assertion 1
  expect(user.email).toBe("john@example.com"); // Assertion 2
  expect(user.isAdult()).toBe(true); // Assertion 3
  // ถ้า Assertion 2 fail → Assertion 3 ไม่รัน
  // ยากรู้ว่า Error จากอะไร
});

// ✅ ดี: One thing per test
test("should set user name correctly", () => {
  const user = new User("John", "john@example.com", 25);
  expect(user.name).toBe("John");
});

test("should set user email correctly", () => {
  const user = new User("John", "john@example.com", 25);
  expect(user.email).toBe("john@example.com");
});

test("should identify adult correctly", () => {
  const user = new User("John", "john@example.com", 25);
  expect(user.isAdult()).toBe(true);
});

// ⚠️ Exception: Setup & Verification OK
test("should create user with all correct data", () => {
  // SETUP
  const user = new User("John", "john@example.com", 25);

  // ASSERT - multiple checks ตรวจสอบ State เดียวกัน
  expect(user.name).toBe("John");
  expect(user.email).toBe("john@example.com");
  expect(user.isAdult()).toBe(true);
  // OK เพราะทั้งหมด test "User Creation"
});
```

**ทำไมต้อง One Assertion?**

- ✅ Test Fail → ชัดเจน ตัวไหนพัง
- ✅ Easy to Debug
- ✅ Test ประเมินได้ง่าย

---

#### 2️⃣ Descriptive Test Names (สื่อ ได้ชัดเจน)

```javascript
// ❌ ไม่ดี: ไม่เข้าใจ Test ทำอะไร
test("it works", () => {});
test("test add", () => {});
test("negative", () => {}); // Negative บ่อยไหน?

// ✅ ดี: ชัดเจน ว่า Test ทำอะไร
test("should return sum of two positive numbers", () => {
  expect(add(2, 3)).toBe(5);
});

test("should throw error when dividing by zero", () => {
  expect(() => divide(10, 0)).toThrow();
});

test("should return empty array when no books available", () => {
  const library = new Library();
  expect(library.getAvailableBooks()).toEqual([]);
});

// ✅ Good Test Names = Self-documenting code
// ได้ Specification ฟรี!
```

**Test Name Formula:**

```
"should [do what] when [condition]"

Examples:
- "should add two positive numbers"
- "should throw error when dividing by zero"
- "should return user object when id is valid"
- "should update email when new email is provided"
```

---

#### 3️⃣ Test Public Interface, Not Implementation

```javascript
// ❌ ไม่ดี: Test internal details
class Calculator {
  _result = 0; // Private field

  calculate(a, b) {
    this._result = a + b; // Internal state
    return this._result;
  }
}

test("should store result internally", () => {
  const calc = new Calculator();
  calc.calculate(2, 3);
  expect(calc._result).toBe(5); // ❌ Testing private _result
  // ปัญหา: ถ้า Developer เปลี่ยน _result เป็น _total
  // Test break แม้ว่า Public API ยังทำงาน
});

// ✅ ดี: Test public API
test("should return sum of two numbers", () => {
  const calc = new Calculator();
  const result = calc.calculate(2, 3); // ✅ Public method
  expect(result).toBe(5); // ✅ Return value
  // OK: Implementation ที่ไหน ไม่สำคัญ
  // ตราบใดที่ calculate() return ถูก
});
```

**ทำไมต้อง?**

- ✅ Tests ยืนยาว (ไม่ต้อง update ทุก refactor)
- ✅ Implementation freedom (Developer refactor ได้)
- ✅ Better abstraction

---

#### 4️⃣ Test Behavior, Not Implementation

```javascript
// ❌ ไม่ดี: Implementation-focused
test("should call fetchBooks exactly 3 times", () => {
  const mockFetch = jest.fn();
  // Focus: "call 3 times" = implementation detail

  // ถ้า Developer optimize เป็น 2 calls → Test fail!
  // แม้ผล Output ยังถูก
});

// ✅ ดี: Behavior-focused
test("should return array of 3 books from API", () => {
  const books = await getBooks();
  // Focus: "return 3 books" = behavior

  // Developer optimize fetch ได้เพื่อ-ต้อง update test
  expect(books).toHaveLength(3);
  expect(books[0]).toHaveProperty("title");
});

// ✅ ดี: Multiple ways to get same behavior
test("should display error when API fails", () => {
  // Either:
  // - Retry 3 times then show error ✅
  // - Retry 5 times then show error ✅
  // - Show error immediately ✅
  // สิ่งสำคัญ: User เห็น Error ไม่ว่าจะกี่ครั้ง

  expect(screen.getByText(/error/i)).toBeVisible();
});
```

**Behavior vs Implementation:**

```
Behavior = "What should happen"
Implementation = "How it happens"

Example:
Behavior:    "User can login successfully"
Impl 1:      "Call POST /api/login 1 time"
Impl 2:      "Call POST /api/login 3 times (with retry)"

Both implementations = same behavior ✅
Test should check behavior, not impl detail
```

---

#### 5️⃣ Keep Tests Fast

```javascript
// ❌ Slow: Real API + Real Database
test("should save book", async () => {
  const response = await fetch("https://real-api.com/books", {
    method: "POST",
    body: JSON.stringify({ title: "Book" }),
  });
  // ⚠️ Test takes 2-5 seconds
  // ⚠️ Test might fail due to network, not code
  // ⚠️ Can't test offline
});

// ✅ Fast: Mock API + Mock Database
test("should save book", async () => {
  global.fetch = jest.fn().mockResolvedValueOnce({
    ok: true,
    json: () => Promise.resolve({ id: 1 }),
  });

  const result = await saveBook({ title: "Book" });
  expect(result.id).toBe(1);
  // ⚠️ Test takes <1ms
  // ✅ Test independent dari network
  // ✅ Can test offline
});

// ✅ Fast: Parallel Tests
// package.json: "test": "jest --maxWorkers=4"
// Run 4 tests at the same time → 4x faster!
```

**Speed Tips:**
| สิ่งที่ทำ | ผลลัพธ์ | Speed |
|---------|--------|-------|
| Real API | Slow | ❌ 2-5s |
| Mock API | Fast | ✅ <1ms |
| Real DB | Slow | ❌ 1-2s |
| Mock DB | Fast | ✅ <1ms |
| Serial | Slow | ❌ 50s (50 tests) |
| Parallel (4x) | Fast | ✅ 12.5s (50 tests) |

---

### 🚀 Quick Command Reference

```bash
# ✅ Run all tests
npm test

# 🔄 Run tests in watch mode (re-run when file changes)
npm test -- --watch

# 🎯 Run specific test file
npm test calculator

# 🔍 Run tests matching pattern
npm test -- --testNamePattern="add"
# Runs only tests with "add" ใน name

# 📊 Run with coverage report
npm test -- --coverage
# Shows: Lines, Branches, Functions, Statements covered

# 📸 Update snapshots (after intentional change)
npm test -- --updateSnapshot

# 🐛 Debug a single test (inspect-brk)
node --inspect-brk node_modules/.bin/jest --runInBand
# Open: chrome://inspect ใน Chrome DevTools

# ⚡ Run only failed tests
npm test -- --onlyChanged

# 📝 Show verbose output
npm test -- --verbose
```

---

## สรุป

**ตัวอย่าง 7 นี้แสดงให้เห็น:**

1. ✅ Simple Functions → Complex Classes
2. ✅ Happy Path → Error Cases
3. ✅ Unit Tests → Integration Tests
4. ✅ Basic Mocking → Advanced Spying
5. ✅ Single Functions → Real Projects

### 📚 Learning Path

```
Week 1: Learn Example 1 + 2 (Functions + Validation)
Week 2: Learn Example 3 + 4 (Classes + Async)
Week 3: Learn Example 5 + 6 (Mocking + Advanced Classes)
Week 4: Learn Example 7 (Real Project)
```

### 🎯 Next Steps

1. **Write tests สำหรับ Project ของคุณ**
2. **Setup CI/CD (GitHub Actions) ให้ auto-test**
3. **Check Coverage: `npm test -- --coverage`**
4. **Aim for 80%+ coverage**
5. **Refactor with confidence! ✨**

**ท้ายสุด:** Jest Testing = **ทำให้ Code ปลอดภัย และ Refactor ได้ง่าย**
