# คู่มือการใช้งาน Jest Framework

## สารบัญ

1. [บทนำ Jest Framework](#1-บทนำ-jest-framework)
2. [การติดตั้งและตั้งค่าเบื้องต้น](#2-การติดตั้งและตั้งค่าเบื้องต้น)
3. [โครงสร้างพื้นฐานของ Test](#3-โครงสร้างพื้นฐานของ-test)
4. [Matchers (การตรวจสอบค่า)](#4-matchers-การตรวจสอบค่า)
5. [การจัดกลุ่ม Test ด้วย describe](#5-การจัดกลุ่ม-test-ด้วย-describe)
6. [Setup และ Teardown](#6-setup-และ-teardown)
7. [การทดสอบ Asynchronous Code](#7-การทดสอบ-asynchronous-code)
8. [Mocking (การจำลองข้อมูล)](#8-mocking-การจำลองข้อมูล)
9. [Code Coverage](#9-code-coverage)
10. [Best Practices และ Tips](#10-best-practices-และ-tips)
11. [แบบฝึกหัด](#11-แบบฝึกหัด)

---

## 1. บทนำ Jest Framework

### 1.1 Jest คืออะไร?

**Jest** เป็น JavaScript Testing Framework ที่พัฒนาโดย Facebook (Meta) ออกแบบมาเพื่อให้ใช้งานง่าย รวดเร็ว และมีฟีเจอร์ครบครัน เหมาะสำหรับการทดสอบ:

- Unit Testing
- Integration Testing
- Snapshot Testing
- Mocking

### 1.2 ทำไมต้องใช้ Jest?

| คุณสมบัติ              | รายละเอียด                                           |
| ---------------------- | ---------------------------------------------------- |
| **Zero Configuration** | ใช้งานได้ทันทีโดยไม่ต้องตั้งค่ามาก                   |
| **Fast**               | รัน tests แบบ parallel และมี intelligent test runner |
| **Built-in Mocking**   | มีระบบ mock ในตัว ไม่ต้องติดตั้งเพิ่ม                |
| **Code Coverage**      | รองรับ coverage report ในตัว                         |
| **Watch Mode**         | รัน tests อัตโนมัติเมื่อไฟล์เปลี่ยน                  |
| **Snapshot Testing**   | บันทึก output และเปรียบเทียบอัตโนมัติ                |

### 1.3 Jest ในบริบทของ Testing Pyramid

```
        /\
       /  \      E2E Tests (Playwright)
      /----\
     /      \    Integration Tests (Jest + supertest)
    /--------\
   /          \  Unit Tests (Jest) ← เราอยู่ตรงนี้!
  --------------
```

Jest เป็นเครื่องมือหลักสำหรับ **Unit Testing** และ **Integration Testing** ในหลักสูตรนี้

---

## 2. การติดตั้งและตั้งค่าเบื้องต้น

### 2.1 ข้อกำหนดเบื้องต้น (Prerequisites)

- Node.js เวอร์ชัน 18 LTS หรือสูงกว่า
- npm หรือ yarn
- VS Code (แนะนำ)

ตรวจสอบเวอร์ชัน Node.js:

```bash
node --version
# ควรได้ v18.x.x หรือสูงกว่า
```

### 2.2 การติดตั้ง Jest

#### วิธีที่ 1: สร้างโปรเจกต์ใหม่

```bash
# สร้างโฟลเดอร์โปรเจกต์
mkdir my-testing-project
cd my-testing-project

# สร้าง package.json
npm init -y

# ติดตั้ง Jest
npm install --save-dev jest
```

#### วิธีที่ 2: เพิ่มเข้าโปรเจกต์ที่มีอยู่แล้ว

```bash
npm install --save-dev jest
```

### 2.3 การตั้งค่า package.json

เพิ่ม scripts สำหรับรัน tests:

```json
{
  "name": "my-testing-project",
  "version": "1.0.0",
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:verbose": "jest --verbose"
  },
  "devDependencies": {
    "jest": "^29.7.0"
  }
}
```

### 2.4 การตั้งค่า Jest Configuration (ทางเลือก)

สร้างไฟล์ `jest.config.js` ที่ root ของโปรเจกต์:

```javascript
module.exports = {
  // ใช้ Node.js environment (สำหรับ backend testing)
  testEnvironment: "node",

  // กำหนด patterns สำหรับหาไฟล์ test
  testMatch: ["**/__tests__/**/*.js", "**/*.test.js", "**/*.spec.js"],

  // ไฟล์/โฟลเดอร์ที่ต้องการข้าม
  testPathIgnorePatterns: ["/node_modules/"],

  // ตั้งค่า coverage
  collectCoverageFrom: ["src/**/*.js", "!src/**/*.test.js"],

  // กำหนด threshold ของ coverage
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },

  // แสดงรายละเอียดการรัน
  verbose: true,
};
```

### 2.5 โครงสร้างโฟลเดอร์แนะนำ

```
my-testing-project/
├── src/
│   ├── models/
│   │   ├── Book.js
│   │   └── User.js
│   ├── services/
│   │   ├── bookService.js
│   │   └── userService.js
│   └── utils/
│       └── validators.js
├── tests/
│   ├── unit/
│   │   ├── Book.test.js
│   │   └── User.test.js
│   └── integration/
│       └── bookService.test.js
├── package.json
└── jest.config.js
```

หรือใช้โครงสร้างแบบ Co-located (ไฟล์ test อยู่คู่กับ source):

```
my-testing-project/
├── src/
│   ├── models/
│   │   ├── Book.js
│   │   ├── Book.test.js      ← อยู่คู่กัน
│   │   ├── User.js
│   │   └── User.test.js
│   └── services/
│       ├── bookService.js
│       └── bookService.test.js
├── package.json
└── jest.config.js
```

### 2.6 VS Code Extensions แนะนำ

- **Jest** (Orta): รัน tests จาก VS Code
- **Jest Snippets**: ช่วยเขียน test เร็วขึ้น
- **Coverage Gutters**: แสดง coverage ใน editor

---

## 3. โครงสร้างพื้นฐานของ Test

### 3.1 ไฟล์ Test แรก

สร้างไฟล์ `sum.js`:

```javascript
// src/sum.js
function sum(a, b) {
  return a + b;
}

module.exports = sum;
```

สร้างไฟล์ test `sum.test.js`:

```javascript
// tests/sum.test.js
const sum = require("../src/sum");

test("adds 1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toBe(3);
});
```

รันทดสอบ:

```bash
npm test
```

ผลลัพธ์:

```
 PASS  tests/sum.test.js
  ✓ adds 1 + 2 to equal 3 (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
```

### 3.2 โครงสร้างของ Test

```javascript
test("ชื่อของ test case", () => {
  // Arrange - เตรียมข้อมูล
  const input = 5;

  // Act - เรียกใช้ฟังก์ชัน
  const result = double(input);

  // Assert - ตรวจสอบผลลัพธ์
  expect(result).toBe(10);
});
```

### 3.3 test() vs it()

`test()` และ `it()` ทำงานเหมือนกัน เลือกใช้ตามความชอบ:

```javascript
// แบบใช้ test()
test("should return correct sum", () => {
  expect(sum(2, 3)).toBe(5);
});

// แบบใช้ it() - อ่านเป็นประโยคภาษาอังกฤษ
it("should return correct sum", () => {
  expect(sum(2, 3)).toBe(5);
});
```

### 3.4 การข้าม Test หรือรันเฉพาะบาง Test

```javascript
// ข้าม test นี้ไปก่อน
test.skip("this test will be skipped", () => {
  // ...
});

// รันเฉพาะ test นี้
test.only("only this test will run", () => {
  // ...
});

// รัน test หลายครั้ง (useful สำหรับ random tests)
test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 2, 4],
])("adds %i + %i to equal %i", (a, b, expected) => {
  expect(sum(a, b)).toBe(expected);
});
```

---

## 4. Matchers (การตรวจสอบค่า)

**Matchers** คือฟังก์ชันที่ใช้ตรวจสอบว่าค่าที่ได้ตรงกับที่คาดหวังหรือไม่

### 4.1 Basic Matchers

#### 🎯 ความแตกต่าง `toBe` vs `toEqual` (สำคัญมาก!)

**`toBe`** ใช้ **Strict Equality (===)** - ตรวจสอบ Reference ว่าเป็นคนเดียวกันในหน่วยความจำหรือไม่

- ✅ ใช้กับ Primitive Types (number, string, boolean, null, undefined)
- ❌ ไม่ใช้กับ Object/Array เพราะแม้คำมือนจะเหมือนกัน แต่เป็นคนละตัว

**`toEqual`** ใช้ **Deep Equality** - ตรวจสอบค่าภายใน Object/Array ว่าเหมือนกันหรือไม่ (ไม่สนใจ Reference)

- ✅ ใช้กับ Object/Array โดยเปรียบเทียบค่าแต่ละ Property/Element
- ✅ ใช้กับ Primitive Types ได้เหมือน toBe

#### ตัวอย่างที่ชัดเจน

```javascript
// ❌ โอ้โห! โค้ดนี้จะ FAIL
const user1 = { name: "John", age: 30 };
const user2 = { name: "John", age: 30 };

expect(user1).toBe(user2);
// FAIL ❌ เพราะ user1 และ user2 เป็นคนละตัวในหน่วยความจำ
//     แม้ว่า properties ทั้งหมดจะเหมือนกัน

// ✅ โค้ดนี้จะ PASS
expect(user1).toEqual(user2);
// PASS ✅ เพราะ toEqual เปรียบเทียบค่าภายใน (name === "John" && age === 30)
```

#### การใช้ Basic Matchers ที่ถูกต้อง

```javascript
// toBe - สำหรับค่า primitive เท่านั้น
test("toBe example", () => {
  expect(2 + 2).toBe(4); // ✅ ตัวเลข
  expect("hello").toBe("hello"); // ✅ ข้อความ
  expect(true).toBe(true); // ✅ Boolean
});

// toEqual - สำหรับ object/array และ primitive
test("toEqual example", () => {
  const data = { name: "John" };
  expect(data).toEqual({ name: "John" }); // ✅ เปรียบเทียบค่าภายใน

  const arr = [1, 2, 3];
  expect(arr).toEqual([1, 2, 3]); // ✅ เปรียบเทียบค่าแต่ละ element
});

// not - กลับค่าการตรวจสอบ (ใช้ได้กับ matcher ทั้งหมด)
test("not example", () => {
  expect(2 + 2).not.toBe(5); // ✅ ไม่เท่ากับ 5
  expect("hello").not.toBe("world"); // ✅ ไม่เท่ากับ "world"
  expect({ name: "John" }).not.toEqual({ name: "Jane" }); // ✅ Object ต่างกัน
});
```

### 4.2 Truthiness Matchers

```javascript
test("truthiness matchers", () => {
  // toBeNull - ตรวจสอบ null
  expect(null).toBeNull();

  // toBeUndefined - ตรวจสอบ undefined
  expect(undefined).toBeUndefined();

  // toBeDefined - ตรวจสอบว่าไม่ใช่ undefined
  expect("hello").toBeDefined();

  // toBeTruthy - ตรวจสอบว่าเป็น truthy value
  expect(1).toBeTruthy();
  expect("hello").toBeTruthy();
  expect(true).toBeTruthy();

  // toBeFalsy - ตรวจสอบว่าเป็น falsy value
  expect(0).toBeFalsy();
  expect("").toBeFalsy();
  expect(null).toBeFalsy();
  expect(undefined).toBeFalsy();
  expect(false).toBeFalsy();
});
```

### 4.3 Number Matchers

```javascript
test("number matchers", () => {
  const value = 2 + 2;

  // toBeGreaterThan - มากกว่า
  expect(value).toBeGreaterThan(3);

  // toBeGreaterThanOrEqual - มากกว่าหรือเท่ากับ
  expect(value).toBeGreaterThanOrEqual(4);

  // toBeLessThan - น้อยกว่า
  expect(value).toBeLessThan(5);

  // toBeLessThanOrEqual - น้อยกว่าหรือเท่ากับ
  expect(value).toBeLessThanOrEqual(4);

  // toBeCloseTo - สำหรับตัวเลขทศนิยม (หลีกเลี่ยงปัญหา floating point)
  expect(0.1 + 0.2).toBeCloseTo(0.3);
});
```

### 4.4 String Matchers

```javascript
test("string matchers", () => {
  const message = "Hello, World!";

  // toMatch - ตรวจสอบด้วย regex หรือ string
  expect(message).toMatch(/World/);
  expect(message).toMatch("World");

  // toContain - ตรวจสอบว่ามี substring
  expect(message).toContain("Hello");

  // toHaveLength - ตรวจสอบความยาว
  expect(message).toHaveLength(13);
});
```

### 4.5 Array/Object Matchers

```javascript
test("array matchers", () => {
  const fruits = ["apple", "banana", "orange"];

  // toContain - ตรวจสอบว่ามี element
  expect(fruits).toContain("banana");

  // toHaveLength - ตรวจสอบจำนวน elements
  expect(fruits).toHaveLength(3);

  // toContainEqual - ตรวจสอบว่ามี object ที่ตรงกัน
  const users = [
    { id: 1, name: "John" },
    { id: 2, name: "Jane" },
  ];
  expect(users).toContainEqual({ id: 1, name: "John" });
});

test("object matchers", () => {
  const user = {
    name: "John",
    age: 30,
    address: {
      city: "Bangkok",
      country: "Thailand",
    },
  };

  // toHaveProperty - ตรวจสอบว่ามี property
  expect(user).toHaveProperty("name");
  expect(user).toHaveProperty("name", "John");
  expect(user).toHaveProperty("address.city", "Bangkok");

  // toMatchObject - ตรวจสอบ subset ของ object
  expect(user).toMatchObject({
    name: "John",
    age: 30,
  });
});
```

### 4.6 Exception Matchers

```javascript
function throwError() {
  throw new Error("Something went wrong");
}

function throwCustomError() {
  throw new TypeError("Invalid type");
}

test("exception matchers", () => {
  // toThrow - ตรวจสอบว่า throw error
  expect(() => throwError()).toThrow();

  // ตรวจสอบ error message
  expect(() => throwError()).toThrow("Something went wrong");
  expect(() => throwError()).toThrow(/wrong/);

  // ตรวจสอบ error type
  expect(() => throwCustomError()).toThrow(TypeError);
});
```

### 4.7 สรุป Matchers ที่ใช้บ่อย

| Matcher               | ใช้ตรวจสอบ                     |
| --------------------- | ------------------------------ |
| `toBe(value)`         | Primitive values (===)         |
| `toEqual(value)`      | Objects/Arrays (deep equality) |
| `toBeNull()`          | null                           |
| `toBeUndefined()`     | undefined                      |
| `toBeTruthy()`        | truthy values                  |
| `toBeFalsy()`         | falsy values                   |
| `toBeGreaterThan(n)`  | ตัวเลข > n                     |
| `toBeLessThan(n)`     | ตัวเลข < n                     |
| `toMatch(regex)`      | String patterns                |
| `toContain(item)`     | Array contains item            |
| `toHaveLength(n)`     | String/Array length            |
| `toHaveProperty(key)` | Object has property            |
| `toThrow()`           | Function throws error          |

---

## 5. การจัดกลุ่ม Test ด้วย describe

### 5.1 พื้นฐาน describe

`describe()` ใช้จัดกลุ่ม tests ที่เกี่ยวข้องกันไว้ด้วยกัน:

```javascript
describe("Calculator", () => {
  test("adds two numbers", () => {
    expect(1 + 2).toBe(3);
  });

  test("subtracts two numbers", () => {
    expect(5 - 3).toBe(2);
  });

  test("multiplies two numbers", () => {
    expect(3 * 4).toBe(12);
  });
});
```

### 5.2 Nested describe

จัดโครงสร้างแบบซ้อนกันได้:

```javascript
describe("Book", () => {
  describe("constructor", () => {
    test("should create book with title", () => {
      const book = new Book("1984");
      expect(book.title).toBe("1984");
    });

    test("should throw error for empty title", () => {
      expect(() => new Book("")).toThrow();
    });
  });

  describe("borrow()", () => {
    test("should decrease available copies", () => {
      const book = new Book("1984");
      book.totalCopies = 5;
      book.borrow();
      expect(book.availableCopies).toBe(4);
    });

    test("should not allow borrowing when no copies available", () => {
      const book = new Book("1984");
      book.totalCopies = 0;
      expect(() => book.borrow()).toThrow("No copies available");
    });
  });
});
```

### 5.3 ตัวอย่างการทดสอบ Class จริง

สมมติมี class Book:

```javascript
// src/models/Book.js
class Book {
  constructor(title, author, isbn) {
    if (!title || title.trim() === "") {
      throw new Error("Title is required");
    }
    if (!author || author.trim() === "") {
      throw new Error("Author is required");
    }

    this.title = title;
    this.author = author;
    this.isbn = isbn || null;
    this.totalCopies = 1;
    this.borrowedCopies = 0;
    this.status = "available";
  }

  get availableCopies() {
    return this.totalCopies - this.borrowedCopies;
  }

  borrow() {
    if (this.availableCopies <= 0) {
      throw new Error("No copies available");
    }
    this.borrowedCopies++;
    if (this.availableCopies === 0) {
      this.status = "unavailable";
    }
    return true;
  }

  return() {
    if (this.borrowedCopies <= 0) {
      throw new Error("No borrowed copies to return");
    }
    this.borrowedCopies--;
    this.status = "available";
    return true;
  }

  isAvailable() {
    return this.availableCopies > 0;
  }
}

module.exports = Book;
```

เขียน test:

```javascript
// tests/unit/Book.test.js
const Book = require("../../src/models/Book");

describe("Book", () => {
  // ตัวแปรที่ใช้ร่วมกัน
  let book;

  // รันก่อนแต่ละ test
  beforeEach(() => {
    book = new Book(
      "The Great Gatsby",
      "F. Scott Fitzgerald",
      "978-0743273565",
    );
  });

  describe("constructor", () => {
    test("should create book with required properties", () => {
      expect(book.title).toBe("The Great Gatsby");
      expect(book.author).toBe("F. Scott Fitzgerald");
      expect(book.isbn).toBe("978-0743273565");
    });

    test("should set default values", () => {
      expect(book.totalCopies).toBe(1);
      expect(book.borrowedCopies).toBe(0);
      expect(book.status).toBe("available");
    });

    test("should allow null isbn", () => {
      const bookWithoutIsbn = new Book("Test", "Author");
      expect(bookWithoutIsbn.isbn).toBeNull();
    });

    test("should throw error for empty title", () => {
      expect(() => new Book("", "Author")).toThrow("Title is required");
    });

    test("should throw error for whitespace-only title", () => {
      expect(() => new Book("   ", "Author")).toThrow("Title is required");
    });

    test("should throw error for empty author", () => {
      expect(() => new Book("Title", "")).toThrow("Author is required");
    });
  });

  describe("availableCopies getter", () => {
    test("should calculate available copies correctly", () => {
      book.totalCopies = 5;
      book.borrowedCopies = 2;
      expect(book.availableCopies).toBe(3);
    });

    test("should return 0 when all copies borrowed", () => {
      book.totalCopies = 3;
      book.borrowedCopies = 3;
      expect(book.availableCopies).toBe(0);
    });
  });

  describe("borrow()", () => {
    test("should increase borrowed copies", () => {
      book.totalCopies = 5;
      book.borrow();
      expect(book.borrowedCopies).toBe(1);
    });

    test("should return true on successful borrow", () => {
      book.totalCopies = 5;
      expect(book.borrow()).toBe(true);
    });

    test("should change status to unavailable when last copy borrowed", () => {
      book.totalCopies = 1;
      book.borrow();
      expect(book.status).toBe("unavailable");
    });

    test("should throw error when no copies available", () => {
      book.totalCopies = 1;
      book.borrowedCopies = 1;
      expect(() => book.borrow()).toThrow("No copies available");
    });

    test("should allow multiple borrows until exhausted", () => {
      book.totalCopies = 3;
      book.borrow();
      book.borrow();
      book.borrow();
      expect(book.availableCopies).toBe(0);
      expect(() => book.borrow()).toThrow();
    });
  });

  describe("return()", () => {
    test("should decrease borrowed copies", () => {
      book.totalCopies = 5;
      book.borrowedCopies = 2;
      book.return();
      expect(book.borrowedCopies).toBe(1);
    });

    test("should change status to available", () => {
      book.totalCopies = 1;
      book.borrowedCopies = 1;
      book.status = "unavailable";
      book.return();
      expect(book.status).toBe("available");
    });

    test("should throw error when no borrowed copies", () => {
      expect(() => book.return()).toThrow("No borrowed copies to return");
    });
  });

  describe("isAvailable()", () => {
    test("should return true when copies available", () => {
      book.totalCopies = 5;
      book.borrowedCopies = 3;
      expect(book.isAvailable()).toBe(true);
    });

    test("should return false when no copies available", () => {
      book.totalCopies = 2;
      book.borrowedCopies = 2;
      expect(book.isAvailable()).toBe(false);
    });
  });
});
```

---

## 6. Setup และ Teardown

### 6.1 Lifecycle Hooks

Jest มี 4 hooks หลักสำหรับจัดการ setup/teardown:

| Hook           | เมื่อไหร่ที่รัน                            |
| -------------- | ------------------------------------------ |
| `beforeAll()`  | รันครั้งเดียวก่อน tests ทั้งหมดใน describe |
| `beforeEach()` | รันก่อนทุก test                            |
| `afterEach()`  | รันหลังทุก test                            |
| `afterAll()`   | รันครั้งเดียวหลัง tests ทั้งหมดใน describe |

### 6.2 ตัวอย่างการใช้งาน

```javascript
describe("Database Operations", () => {
  let db;
  let testUser;

  // รันครั้งเดียวก่อนทุก tests - เหมาะสำหรับ setup ที่ใช้เวลานาน
  beforeAll(async () => {
    console.log("🔧 Connecting to test database...");
    db = await Database.connect("test_db");
  });

  // รันก่อนแต่ละ test - เหมาะสำหรับ reset state
  beforeEach(async () => {
    console.log("📝 Creating test user...");
    testUser = await db.users.create({
      name: "Test User",
      email: "test@example.com",
    });
  });

  // รันหลังแต่ละ test - เหมาะสำหรับ cleanup
  afterEach(async () => {
    console.log("🧹 Cleaning up test data...");
    await db.users.deleteMany({});
  });

  // รันครั้งเดียวหลังทุก tests - เหมาะสำหรับ cleanup resources
  afterAll(async () => {
    console.log("🔌 Disconnecting from database...");
    await db.disconnect();
  });

  test("should find user by email", async () => {
    const found = await db.users.findByEmail("test@example.com");
    expect(found.name).toBe("Test User");
  });

  test("should update user name", async () => {
    await db.users.update(testUser.id, { name: "Updated Name" });
    const updated = await db.users.findById(testUser.id);
    expect(updated.name).toBe("Updated Name");
  });
});
```

### 6.3 ลำดับการรัน (Execution Order)

```javascript
describe("Outer", () => {
  beforeAll(() => console.log("1. beforeAll Outer"));
  afterAll(() => console.log("7. afterAll Outer"));
  beforeEach(() => console.log("2. beforeEach Outer"));
  afterEach(() => console.log("4. afterEach Outer"));

  test("test in outer", () => {
    console.log("3. test in outer");
  });

  describe("Inner", () => {
    beforeAll(() => console.log("5. beforeAll Inner"));
    afterAll(() => console.log("10. afterAll Inner"));
    beforeEach(() => console.log("6. beforeEach Inner"));
    afterEach(() => console.log("8. afterEach Inner"));

    test("test in inner", () => {
      console.log("7. test in inner");
    });
  });
});

/*
Output:
1. beforeAll Outer
2. beforeEach Outer
3. test in outer
4. afterEach Outer
5. beforeAll Inner
2. beforeEach Outer   <- รันจาก outer ก่อน
6. beforeEach Inner
7. test in inner
8. afterEach Inner
4. afterEach Outer    <- รันจาก inner ก่อน
10. afterAll Inner
7. afterAll Outer
*/
```

### 6.4 Best Practices สำหรับ Setup/Teardown

```javascript
describe("UserService", () => {
  // ✅ Good: ใช้ beforeEach สำหรับ fresh state ในแต่ละ test
  let userService;
  let mockDb;

  beforeEach(() => {
    mockDb = {
      users: [],
      findById: jest.fn(),
      save: jest.fn(),
    };
    userService = new UserService(mockDb);
  });

  // ✅ Good: Clear mocks หลังแต่ละ test
  afterEach(() => {
    jest.clearAllMocks();
  });

  test("test 1", () => {
    // userService และ mockDb จะเป็น instance ใหม่ทุกครั้ง
  });

  test("test 2", () => {
    // ไม่มี side effects จาก test 1
  });
});

// ❌ Bad: ไม่ควรใช้ตัวแปรร่วมโดยไม่ reset
describe("Bad Example", () => {
  const items = []; // ❌ จะสะสม items จากทุก tests

  test("adds item", () => {
    items.push("a");
    expect(items).toHaveLength(1);
  });

  test("adds another item", () => {
    items.push("b");
    expect(items).toHaveLength(1); // ❌ FAIL! items มี 2 ตัวแล้ว
  });
});
```

---

## 7. การทดสอบ Asynchronous Code

### เหตุผลที่ต้องสอง Async Code

ในโลกจริง โปรแกรมส่วนใหญ่ต้องรอผลลัพธ์จาก:

- **API Calls** - เรียก API ต่างโลก เวลาตอบกลับไม่แน่นอน
- **Database Queries** - ค้นหาข้อมูลจากฐานข้อมูล ใช้เวลา
- **File I/O** - อ่าน/เขียนไฟล์ ซึ่งไม่เร็ว
- **Timers** - `setTimeout` ที่รอเวลาระบุ

ถ้า Test ของเราไม่รอให้ Task เหล่านี้เสร็จ ผลการทดสอบจะไม่ถูกต้องครับ

### 7.1 Callbacks (วิธีเก่า - ไม่แนะนำแล้ว)

```javascript
// ฟังก์ชันที่ใช้ callback
function fetchData(callback) {
  setTimeout(() => {
    callback("peanut butter");
  }, 100);
}

// วิธีทดสอบ - ใช้ done parameter
test("callback: fetchData returns peanut butter", (done) => {
  function callback(data) {
    try {
      expect(data).toBe("peanut butter");
      done(); // บอก Jest ว่า test เสร็จแล้ว (ต้องทำ!)
    } catch (error) {
      done(error); // ส่ง error ถ้า assertion fail
    }
  }

  fetchData(callback);
  // ⚠️ Jest จะรอให้มีการเรียก done() ก่อนจึงจะจบ test นี้
});

// ❌ ปัญหา: ถ้าลืมเรียก done() Jest จะรอ 5 วินาที แล้ว timeout
// ❌ ปัญหา: Code ยุ่งและยากจะอ่าน
```

### 7.2 Promises (วิธีปานกลาง)

**Promise** คือวัตถุ JavaScript ที่แทน "สัญญา" ของการทำงาน โดยมี 3 สถานะ:

- **Pending** - กำลังรอผลลัพธ์
- **Fulfilled (Resolved)** - ทำงานสำเร็จ ได้ผลลัพธ์ ใช้ `resolve(value)`
- **Rejected** - ทำงานล้มเหลว โยน Error ใช้ `reject(error)`

```javascript
// ฟังก์ชันที่ return Promise
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("peanut butter");
    }, 100);
  });
}

function fetchDataWithError() {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error("Network error"));
    }, 100);
  });
}

// วิธี 1: Return promise จาก test (Jest จะรอให้ Promise settle)
test("promise: fetchData returns peanut butter", () => {
  // ⚠️ ต้อง return Promise จาก test function
  return fetchData().then((data) => {
    expect(data).toBe("peanut butter");
  });
  // Jest จะรอให้ Promise resolve หรือ reject ก่อนจึงจบ test
});

// วิธี 2: ใช้ resolves/rejects matchers (สะอาดกว่า)
test("resolves: fetchData returns peanut butter", () => {
  // expect(...).resolves จะรอให้ Promise resolve และเช็คค่า
  return expect(fetchData()).resolves.toBe("peanut butter");
});

test("rejects: fetchDataWithError throws error", () => {
  // expect(...).rejects จะรอให้ Promise reject และเช็ค error
  return expect(fetchDataWithError()).rejects.toThrow("Network error");
});

// ❌ ปัญหา: ต้อง return Promise เสมอ ถ้าลืม Jest ก็ไม่รอ
// ❌ ปัญหา: ยังมีความซับซ้อนเล็กน้อย
```

### 7.3 Async/Await (⭐ วิธีแนะนำ)

**async/await** คือ Syntactic Sugar ของ Promise ที่ทำให้โค้ดดูเหมือนการสั่งการแบบ Synchronous

- `async` - บอก JavaScript ว่าฟังก์ชันนี้ return Promise
- `await` - หยุด Execution และรอให้ Promise settle ก่อนดำเนินการต่อ

```javascript
// ฟังก์ชัน async (ซึ่ง return Promise)
async function fetchUser(id) {
  // simulate API call
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) {
        resolve({ id: 1, name: "John" });
      } else {
        reject(new Error("User not found"));
      }
    }, 100);
  });
}

// ✅ วิธีแนะนำที่สุด: ใช้ async/await (สะอาดและง่ายเข้าใจ)
test("async/await: fetchUser returns user", async () => {
  // await รอให้ Promise resolve และเอาค่า resolve ออกมา
  const user = await fetchUser(1);

  expect(user).toEqual({ id: 1, name: "John" });
  expect(user.name).toBe("John");
});

test("async/await: fetchUser throws for invalid id", async () => {
  // await จะรอให้ Promise reject แล้ว throw error ออกมา
  await expect(fetchUser(999)).rejects.toThrow("User not found");
});

// ✅ หลาย assertions ในหนึ่ง test (ยืดหยุ่นมากขึ้น)
test("async/await: multiple assertions", async () => {
  const user = await fetchUser(1);

  expect(user).toBeDefined();
  expect(user.id).toBe(1);
  expect(user.name).toBe("John");
  expect(user).toHaveProperty("name", "John");
});
```

#### เปรียบเทียบทั้ง 3 วิธี

| วิธี        | ข้อดี                         | ข้อเสีย                  |
| ----------- | ----------------------------- | ------------------------ |
| Callbacks   | ใช้ได้ทั่วไป                  | Code ยุ่ง, Callback Hell |
| Promises    | ดีกว่า Callback               | ต้อง return, ยังซับซ้อน  |
| Async/Await | 🌟 Code เรียบร้อย, ง่ายเข้าใจ | ต้อง async function      |

#### ⚠️ สิ่งที่ต้องจำ

```javascript
// ❌ ผิด: ลืม async ก่อน test function
test("will fail", () => { // ❌ ไม่ใช่ async
  const user = await fetchUser(1); // ❌ SyntaxError!
});

// ✅ ถูก: ต้องเป็น async
test("correct", async () => { // ✅ async
  const user = await fetchUser(1); // ✅ OK
});

// ❌ ผิด: ลืมใส่ await ทำให้ test จบก่อนถึง assertion
test("will pass but wrong", async () => {
  const userPromise = fetchUser(1); // ❌ ลืม await
  expect(userPromise).toBeDefined(); // ✅ Promise เมื่อไร้ await ก็เป็น Promise object
  // Test จบก่อน Promise resolve แล้ว ❌
});

// ✅ ถูก: ใช้ await ด้วย
test("correct async", async () => {
  const user = await fetchUser(1); // ✅ มี await
  expect(user).toEqual({ id: 1, name: "John" }); // ✅ assertions รอจนกว่า Promise settle
});
```

### 7.4 ตัวอย่างจริง: ทดสอบ API Service

```javascript
// src/services/bookService.js
class BookService {
  constructor(apiClient) {
    this.api = apiClient;
  }

  async getBook(id) {
    const response = await this.api.get(`/books/${id}`);
    return response.data;
  }

  async createBook(bookData) {
    if (!bookData.title) {
      throw new Error("Title is required");
    }
    const response = await this.api.post("/books", bookData);
    return response.data;
  }

  async searchBooks(query) {
    const response = await this.api.get("/books", { params: { q: query } });
    return response.data;
  }
}

module.exports = BookService;
```

```javascript
// tests/unit/bookService.test.js
const BookService = require("../../src/services/bookService");

describe("BookService", () => {
  let bookService;
  let mockApiClient;

  beforeEach(() => {
    // สร้าง mock API client
    mockApiClient = {
      get: jest.fn(),
      post: jest.fn(),
    };
    bookService = new BookService(mockApiClient);
  });

  describe("getBook()", () => {
    test("should return book data", async () => {
      // Arrange
      const mockBook = { id: 1, title: "1984", author: "George Orwell" };
      mockApiClient.get.mockResolvedValue({ data: mockBook });

      // Act
      const result = await bookService.getBook(1);

      // Assert
      expect(result).toEqual(mockBook);
      expect(mockApiClient.get).toHaveBeenCalledWith("/books/1");
    });

    test("should throw error when API fails", async () => {
      // Arrange
      mockApiClient.get.mockRejectedValue(new Error("Network error"));

      // Act & Assert
      await expect(bookService.getBook(1)).rejects.toThrow("Network error");
    });
  });

  describe("createBook()", () => {
    test("should create and return new book", async () => {
      // Arrange
      const newBook = { title: "New Book", author: "Author" };
      const createdBook = { id: 1, ...newBook };
      mockApiClient.post.mockResolvedValue({ data: createdBook });

      // Act
      const result = await bookService.createBook(newBook);

      // Assert
      expect(result).toEqual(createdBook);
      expect(mockApiClient.post).toHaveBeenCalledWith("/books", newBook);
    });

    test("should throw error when title is missing", async () => {
      // Arrange
      const invalidBook = { author: "Author" };

      // Act & Assert
      await expect(bookService.createBook(invalidBook)).rejects.toThrow(
        "Title is required",
      );

      // API should not be called
      expect(mockApiClient.post).not.toHaveBeenCalled();
    });
  });

  describe("searchBooks()", () => {
    test("should return search results", async () => {
      // Arrange
      const mockResults = [
        { id: 1, title: "JavaScript Basics" },
        { id: 2, title: "Advanced JavaScript" },
      ];
      mockApiClient.get.mockResolvedValue({ data: mockResults });

      // Act
      const result = await bookService.searchBooks("javascript");

      // Assert
      expect(result).toHaveLength(2);
      expect(mockApiClient.get).toHaveBeenCalledWith("/books", {
        params: { q: "javascript" },
      });
    });
  });
});
```

### 7.5 Timeout สำหรับ Async Tests

```javascript
// กำหนด timeout สำหรับ test ที่ใช้เวลานาน
test("long running test", async () => {
  const result = await verySlowOperation();
  expect(result).toBeDefined();
}, 10000); // timeout 10 วินาที (default คือ 5 วินาที)

// หรือกำหนดใน jest.config.js
module.exports = {
  testTimeout: 10000,
};
```

---

## 8. Mocking (การจำลองข้อมูล)

### เหตุผลที่ต้อง Mock

Mocking คือเทคนิคการสร้างของจำลอง (Mock) แทนของจริง เพื่อ:

- **ควบคุม Behavior** - สามารถบอกให้ Mock ทำอะไรก็ได้ แล้วรันสหลักหมูกี่ครั้ง
- **ทดสอบแยกเป็นส่วน** - เทส Unit หนึ่งโดยไม่กังวลว่า Dependencies อื่นจะเสีย
- **เร็วขึ้น** - ไม่ต้องเรียก API จริง, ค้นหา Database จริง ประหยัดเวลามาก
- **ลดความพึ่งพา** - ไม่ต้องเก็บ Secret Key, Username/Password ในทดสอบ
- **ควบคุม Error** - บังคับให้ Mock throw error เพื่อทดสอบกรณี Failure

### ตัวอย่างเหตุการณ์จริง

```javascript
// ฟังก์ชันจริงที่ต้อง Mock
class PaymentService {
  async chargeCard(cardNumber, amount) {
    // เรียก Payment Gateway จริง (เช่น Stripe)
    // ถ้า Test นี้ run ทุกครั้ง จะถูกชำระ 100 บาท ทุกครั้ง!
    return await stripeAPI.charge(cardNumber, amount);
  }
}

// ❌ ถ้าไม่ Mock - Test จะเสี่ยง:
// 1. หากเครือข่าย disconnect ก็ fail ❌
// 2. ทุกการรัน test ก็มีค่าใช้จ่ายจริง 💸
// 3. Test ช้าลง (รอเครือข่าย) 🐢

// ✅ ถ้า Mock - จะปลอดภัยและเร็ว:
// 1. สามารถจำลองการ success ได้
// 2. สามารถจำลอง error ได้
// 3. ไม่มีค่าใช้จ่ายจริง
// 4. รวดเร็ว (ไม่รอเครือข่าย)
```

### 8.1 jest.fn() - Mock Functions

`jest.fn()` สร้าง "สปายเฟก" (Fake Function) ที่เราสามารถควบคุมได้:

- วิธีที่จะ return ค่า
- Error ที่จะ throw
- จำนวนครั้งที่ถูกเรียก
- Arguments ที่ถูกส่งไป

```javascript
// สร้าง mock function พื้นฐาน
const mockFn = jest.fn();

test("basic mock function", () => {
  mockFn(); // เรียกใช้ mock function
  mockFn("arg1");
  mockFn("arg2");

  // ตรวจสอบว่าถูกเรียกหรือไม่
  expect(mockFn).toHaveBeenCalled(); // ✅ ถูกเรียกอย่างน้อย 1 ครั้ง
  expect(mockFn).toHaveBeenCalledTimes(3); // ✅ ถูกเรียก 3 ครั้ง
  expect(mockFn).toHaveBeenCalledWith("arg1"); // ✅ ใช้ "arg1" ในครั้งที่สอง
});

// Mock function ที่มี Implementation
const mockAdd = jest.fn((a, b) => a + b);

test("mock function with implementation", () => {
  const result = mockAdd(2, 3);

  expect(result).toBe(5); // ✅ return ค่าจาก implementation
  expect(mockAdd).toHaveBeenCalledWith(2, 3); // ✅ ตรวจสอบ arguments
});

// ตั้ง Return Value (ไม่ต้องมี Implementation)
const mockGetUser = jest.fn();
mockGetUser.mockReturnValue({ id: 1, name: "John" }); // ✅ บอกให้ return object นี้

test("mockReturnValue - สำหรับ Synchronous", () => {
  const result = mockGetUser();
  expect(result).toEqual({ id: 1, name: "John" }); // ✅ ได้ค่าที่ตั้งไว้
  expect(mockGetUser).toHaveBeenCalled();
});

// ตั้ง Return Value สำหรับ Promise (สำหรับ Async Functions)
const mockFetchUser = jest.fn();
mockFetchUser.mockResolvedValue({ id: 1, name: "John" }); // ✅ resolve Promise

test("mockResolvedValue - สำหรับ Async", async () => {
  const user = await mockFetchUser();
  expect(user).toEqual({ id: 1, name: "John" }); // ✅ ได้ค่าหลังจาก resolve
});

// ตั้งให้ Mock throw error
const mockFailingFetch = jest.fn();
mockFailingFetch.mockRejectedValue(new Error("Network error")); // ✅ reject Promise

test("mockRejectedValue - สำหรับ Async Error", async () => {
  await expect(mockFailingFetch()).rejects.toThrow("Network error"); // ✅ ดักจับ error
});

// ตั้งค่าต่างกันสำหรับการเรียกแต่ละครั้ง
const mockIncrement = jest
  .fn()
  .mockReturnValueOnce(1) // ครั้งแรก return 1
  .mockReturnValueOnce(2) // ครั้งที่สอง return 2
  .mockReturnValueOnce(3); // ครั้งที่สาม return 3

test("mockReturnValueOnce - เปลี่ยนค่าแต่ละครั้ง", () => {
  expect(mockIncrement()).toBe(1); // ✅ ครั้งแรก 1
  expect(mockIncrement()).toBe(2); // ✅ ครั้งที่สอง 2
  expect(mockIncrement()).toBe(3); // ✅ ครั้งที่สาม 3
});
```

### 8.2 Mock Function Matchers (ตรวจสอบการใช้งาน)

Matchers เหล่านี้ช่วยให้เรา **สอบสวน** ว่า Mock function ถูกเรียกใช้อย่างไร:

```javascript
const mockFn = jest.fn();

// เรียกใช้ mock function หลายครั้ง
mockFn("arg1", "arg2");
mockFn("arg3");
mockFn({ id: 1 });

test("mock function matchers - ตรวจสอบการเรียกใช้", () => {
  // ✅ ถูกเรียกใช้หรือไม่
  expect(mockFn).toHaveBeenCalled(); // เรียกมาแล้ว ✅

  // ✅ เรียกมากี่ครั้ง
  expect(mockFn).toHaveBeenCalledTimes(3); // เรียก 3 ครั้ง ✅

  // ✅ ตรวจสอบ Arguments ของการเรียกใด ๆ
  expect(mockFn).toHaveBeenCalledWith("arg1", "arg2"); // ✅ เรียกด้วย arguments นี้มา
  expect(mockFn).toHaveBeenCalledWith("arg3"); // ✅ เรียกด้วย argument นี้มา
  expect(mockFn).toHaveBeenCalledWith({ id: 1 }); // ✅ เรียกด้วย object นี้มา

  // ✅ ตรวจสอบ Arguments ของการเรียกที่ n (nth call)
  expect(mockFn).toHaveBeenNthCalledWith(1, "arg1", "arg2"); // การเรียกครั้งที่ 1
  expect(mockFn).toHaveBeenNthCalledWith(2, "arg3"); // การเรียกครั้งที่ 2

  // ✅ ตรวจสอบการเรียกล่าสุด
  expect(mockFn).toHaveBeenLastCalledWith({ id: 1 }); // การเรียกสุดท้าย ✅

  // ✅ ดูทุก calls ที่เก็บไว้ (Advanced)
  expect(mockFn.mock.calls).toEqual([["arg1", "arg2"], ["arg3"], [{ id: 1 }]]);

  // ✅ ดูทุกค่า return ที่เก็บไว้ (Advanced)
  expect(mockFn.mock.results).toEqual([
    { type: "return", value: undefined },
    { type: "return", value: undefined },
    { type: "return", value: undefined },
  ]);
});
```

#### ตารางเปรียบเทียบ Matchers

| Matcher                               | ความหมาย                                | ตัวอย่าง                                         |
| ------------------------------------- | --------------------------------------- | ------------------------------------------------ |
| `toHaveBeenCalled()`                  | เรียกมาแล้วหรือไม่                      | `expect(mock).toHaveBeenCalled()`                |
| `toHaveBeenCalledTimes(n)`            | เรียกกี่ครั้ง                           | `expect(mock).toHaveBeenCalledTimes(2)`          |
| `toHaveBeenCalledWith(...args)`       | เรียกด้วย arguments นี้ไหม              | `expect(mock).toHaveBeenCalledWith(1, 2)`        |
| `toHaveBeenNthCalledWith(n, ...args)` | ครั้งที่ n เรียกด้วย arguments นี้ไหม   | `expect(mock).toHaveBeenNthCalledWith(1, "a")`   |
| `toHaveBeenLastCalledWith(...args)`   | ครั้งสุดท้าย เรียกด้วย arguments นี้ไหม | `expect(mock).toHaveBeenLastCalledWith("final")` |

### 8.3 jest.mock() - Module Mocking

```javascript
// src/services/emailService.js
const axios = require("axios");

async function sendEmail(to, subject, body) {
  const response = await axios.post("https://api.email.com/send", {
    to,
    subject,
    body,
  });
  return response.data;
}

module.exports = { sendEmail };
```

```javascript
// tests/emailService.test.js
const axios = require("axios");
const { sendEmail } = require("../src/services/emailService");

// Mock ทั้ง module
jest.mock("axios");

describe("emailService", () => {
  beforeEach(() => {
    // Reset mocks ก่อนแต่ละ test
    jest.clearAllMocks();
  });

  test("sendEmail should call axios.post", async () => {
    // Arrange
    axios.post.mockResolvedValue({
      data: { success: true, messageId: "123" },
    });

    // Act
    const result = await sendEmail(
      "test@example.com",
      "Test Subject",
      "Test Body",
    );

    // Assert
    expect(axios.post).toHaveBeenCalledWith("https://api.email.com/send", {
      to: "test@example.com",
      subject: "Test Subject",
      body: "Test Body",
    });
    expect(result).toEqual({ success: true, messageId: "123" });
  });

  test("sendEmail should throw on API error", async () => {
    // Arrange
    axios.post.mockRejectedValue(new Error("API Error"));

    // Act & Assert
    await expect(
      sendEmail("test@example.com", "Subject", "Body"),
    ).rejects.toThrow("API Error");
  });
});
```

### 8.4 jest.spyOn() - Spy on Methods

`spyOn` ใช้เมื่อต้องการ mock method ของ object โดยยังเก็บ implementation เดิมไว้ได้:

```javascript
const calculator = {
  add: (a, b) => a + b,
  multiply: (a, b) => a * b,
};

test("spyOn example", () => {
  // สร้าง spy
  const addSpy = jest.spyOn(calculator, "add");

  // เรียกใช้ method จริง
  const result = calculator.add(2, 3);

  // ตรวจสอบว่าถูกเรียก
  expect(addSpy).toHaveBeenCalledWith(2, 3);
  expect(result).toBe(5);

  // Restore method เดิม
  addSpy.mockRestore();
});

test("spyOn with mock implementation", () => {
  // สร้าง spy และ override implementation
  const multiplySpy = jest
    .spyOn(calculator, "multiply")
    .mockImplementation((a, b) => 0); // ให้ return 0 เสมอ

  expect(calculator.multiply(5, 5)).toBe(0);
  expect(multiplySpy).toHaveBeenCalledWith(5, 5);

  multiplySpy.mockRestore();
});
```

### 8.5 Manual Mocks (โฟลเดอร์ **mocks**)

สร้างโฟลเดอร์ `__mocks__` สำหรับ mock ที่ใช้หลายที่:

```
project/
├── src/
│   └── services/
│       └── database.js
├── __mocks__/
│   └── services/
│       └── database.js    ← Manual mock
└── tests/
    └── user.test.js
```

```javascript
// __mocks__/services/database.js
const mockDb = {
  connect: jest.fn().mockResolvedValue(true),
  query: jest.fn(),
  close: jest.fn(),
};

module.exports = mockDb;
```

```javascript
// tests/user.test.js
jest.mock("../src/services/database"); // ใช้ manual mock

const db = require("../src/services/database");

test("uses manual mock", async () => {
  db.query.mockResolvedValue([{ id: 1, name: "John" }]);

  const result = await db.query("SELECT * FROM users");
  expect(result).toHaveLength(1);
});
```

### 8.6 ตัวอย่างการ Mock Database Layer

```javascript
// src/repositories/bookRepository.js
class BookRepository {
  constructor(database) {
    this.db = database;
  }

  async findById(id) {
    return this.db.query("SELECT * FROM books WHERE id = ?", [id]);
  }

  async findAll() {
    return this.db.query("SELECT * FROM books");
  }

  async create(book) {
    const result = await this.db.query(
      "INSERT INTO books (title, author) VALUES (?, ?)",
      [book.title, book.author],
    );
    return { id: result.insertId, ...book };
  }

  async delete(id) {
    const result = await this.db.query("DELETE FROM books WHERE id = ?", [id]);
    return result.affectedRows > 0;
  }
}

module.exports = BookRepository;
```

```javascript
// tests/unit/bookRepository.test.js
const BookRepository = require("../../src/repositories/bookRepository");

describe("BookRepository", () => {
  let repository;
  let mockDatabase;

  beforeEach(() => {
    // สร้าง mock database
    mockDatabase = {
      query: jest.fn(),
    };
    repository = new BookRepository(mockDatabase);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe("findById()", () => {
    test("should query database with correct parameters", async () => {
      // Arrange
      const mockBook = { id: 1, title: "1984", author: "George Orwell" };
      mockDatabase.query.mockResolvedValue([mockBook]);

      // Act
      const result = await repository.findById(1);

      // Assert
      expect(mockDatabase.query).toHaveBeenCalledWith(
        "SELECT * FROM books WHERE id = ?",
        [1],
      );
      expect(result).toEqual([mockBook]);
    });

    test("should return empty array when book not found", async () => {
      mockDatabase.query.mockResolvedValue([]);

      const result = await repository.findById(999);

      expect(result).toEqual([]);
    });
  });

  describe("create()", () => {
    test("should insert book and return with id", async () => {
      // Arrange
      const newBook = { title: "New Book", author: "New Author" };
      mockDatabase.query.mockResolvedValue({ insertId: 5 });

      // Act
      const result = await repository.create(newBook);

      // Assert
      expect(mockDatabase.query).toHaveBeenCalledWith(
        "INSERT INTO books (title, author) VALUES (?, ?)",
        ["New Book", "New Author"],
      );
      expect(result).toEqual({
        id: 5,
        title: "New Book",
        author: "New Author",
      });
    });
  });

  describe("delete()", () => {
    test("should return true when book deleted", async () => {
      mockDatabase.query.mockResolvedValue({ affectedRows: 1 });

      const result = await repository.delete(1);

      expect(result).toBe(true);
    });

    test("should return false when book not found", async () => {
      mockDatabase.query.mockResolvedValue({ affectedRows: 0 });

      const result = await repository.delete(999);

      expect(result).toBe(false);
    });
  });
});
```

### 8.7 Clearing, Resetting, และ Restoring Mocks

```javascript
const mockFn = jest.fn().mockReturnValue("original");

test("mock management", () => {
  mockFn();
  mockFn();

  // clearAllMocks - ล้าง call history แต่เก็บ implementation
  jest.clearAllMocks();
  expect(mockFn.mock.calls).toHaveLength(0);
  expect(mockFn()).toBe("original"); // implementation ยังอยู่

  // resetAllMocks - ล้าง call history และ implementation
  jest.resetAllMocks();
  expect(mockFn()).toBeUndefined(); // implementation หายไป

  // restoreAllMocks - restore spies ให้กลับเป็น original implementation
  jest.restoreAllMocks();
});
```

---

## 9. Code Coverage

### 9.1 การรัน Coverage Report

```bash
# รัน tests พร้อม coverage
npm test -- --coverage

# หรือใช้ script ที่ตั้งไว้
npm run test:coverage
```

### 9.2 อ่าน Coverage Report

```
--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files           |   85.71 |    66.67 |     100 |   85.71 |
 Book.js            |   85.71 |    66.67 |     100 |   85.71 | 15-16
--------------------|---------|----------|---------|---------|-------------------
```

| Metric         | ความหมาย                                                 |
| -------------- | -------------------------------------------------------- |
| **Statements** | เปอร์เซ็นต์ของ statements ที่ถูก execute                 |
| **Branches**   | เปอร์เซ็นต์ของ branches (if/else, switch) ที่ถูก execute |
| **Functions**  | เปอร์เซ็นต์ของ functions ที่ถูกเรียก                     |
| **Lines**      | เปอร์เซ็นต์ของบรรทัดที่ถูก execute                       |

### 9.3 HTML Coverage Report

Jest สร้าง HTML report ใน `coverage/lcov-report/index.html`:

```bash
# รัน coverage
npm test -- --coverage

# เปิด report
open coverage/lcov-report/index.html  # macOS
start coverage/lcov-report/index.html  # Windows
```

### 9.4 กำหนด Coverage Threshold

```javascript
// jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
    // กำหนดเฉพาะบางไฟล์
    "./src/services/": {
      branches: 90,
      functions: 90,
    },
  },
};
```

### 9.5 เลือกไฟล์ที่จะวัด Coverage

```javascript
// jest.config.js
module.exports = {
  collectCoverageFrom: [
    "src/**/*.js", // รวมทุกไฟล์ใน src
    "!src/**/*.test.js", // ยกเว้นไฟล์ test
    "!src/**/*.spec.js",
    "!src/index.js", // ยกเว้นไฟล์ entry point
    "!**/node_modules/**",
  ],
};
```

### 9.6 ตัวอย่างการเพิ่ม Coverage

ถ้ามี code:

```javascript
function calculateGrade(score) {
  if (score >= 80) {
    return "A";
  } else if (score >= 70) {
    return "B";
  } else if (score >= 60) {
    return "C";
  } else if (score >= 50) {
    return "D";
  } else {
    return "F";
  }
}
```

Tests ที่ครอบคลุมทุก branch:

```javascript
describe("calculateGrade", () => {
  test.each([
    [85, "A"], // score >= 80
    [80, "A"], // boundary: exactly 80
    [75, "B"], // score >= 70 && score < 80
    [70, "B"], // boundary: exactly 70
    [65, "C"], // score >= 60 && score < 70
    [60, "C"], // boundary: exactly 60
    [55, "D"], // score >= 50 && score < 60
    [50, "D"], // boundary: exactly 50
    [45, "F"], // score < 50
    [0, "F"], // edge case: 0
    [100, "A"], // edge case: 100
  ])("should return %s for score %i", (score, expected) => {
    expect(calculateGrade(score)).toBe(expected);
  });
});
```

---

## 10. Best Practices และ Tips

## 10. Best Practices และ Tips

### 10.1 ตั้งชื่อ Test ให้ชัดเจน

**ทำไมจึงสำคัญ?** ชื่อที่ดี ช่วยให้ผู้อื่น (และตัวเอง 3 เดือนต่อมา) เข้าใจว่า Test นี้ทำอะไรได้ทันที

```javascript
// ❌ ชื่อไม่ชัดเจน - อ่านแล้วงง
test("test 1", () => {});
test("it works", () => {});
test("book test", () => {});
test("check function", () => {});

// ✅ ชื่อชัดเจน - บอกได้ว่าทดสอบอะไร
test("should return empty array when no books found", () => {});
test("should throw error when title is empty string", () => {});
test("should increase borrowed count by 1 when book is borrowed", () => {});
test("should prevent borrowing when all copies are unavailable", () => {});

// ✅ Pattern ที่ดี: "should [do what] when [condition]"
// - should = บอกว่าเป็น Assertion ทั่วไป
// - [do what] = ผลลัพธ์ที่คาดหวัง
// - when [condition] = เงื่อนไขการทดสอบ
```

### 10.2 AAA Pattern (Arrange-Act-Assert) - โครงสร้างมาตรฐาน

**ทำไมจึงสำคัญ?** Pattern นี้ทำให้ Test อ่านง่ายและเข้าใจโลจิก ผู้อื่นสามารถตามได้ทันที

```javascript
test("should calculate total price with discount", () => {
  // ========== 1. ARRANGE (เตรียมข้อมูล) ==========
  // ตั้งค่า Initial State และ Test Data
  const cart = new ShoppingCart();
  cart.addItem({ name: "Book", price: 100 });
  cart.addItem({ name: "Pen", price: 20 });
  const discount = 0.1; // 10% discount

  // ========== 2. ACT (ทำการกระทำ) ==========
  // เรียกใช้ Function ที่ต้องการทดสอบ
  const total = cart.calculateTotal(discount);

  // ========== 3. ASSERT (ตรวจสอบผลลัพธ์) ==========
  // ตรวจสอบว่าผลลัพธ์ตรงกับความคาดหวังหรือไม่
  expect(total).toBe(108); // (100 + 20) * 0.9
});

// ❌ ไม่ชัดเจน - ยุ่มเยอะ
test("test cart", () => {
  const cart = new ShoppingCart();
  cart.addItem({ name: "Book", price: 100 });
  const total = cart.calculateTotal(0.1);
  expect(total).toBe(90);
});

// ✅ ชัดเจน - มี Comment แบ่ง Sections
test("test cart with comments", () => {
  // Arrange
  const cart = new ShoppingCart();
  cart.addItem({ name: "Book", price: 100 });

  // Act
  const total = cart.calculateTotal(0.1);

  // Assert
  expect(total).toBe(90);
});

// ✅ ดีที่สุด - มี Blank Lines แบ่ง Sections (ไม่ต้องมี Comment)
test("test cart - clean structure", () => {
  const cart = new ShoppingCart();
  cart.addItem({ name: "Book", price: 100 });

  const total = cart.calculateTotal(0.1);

  expect(total).toBe(90);
});
```

### 10.3 One Assertion vs Multiple Assertions - เลือกอย่างชาญฉลาด

**ปัญหา:** ควรจะมี Assertion กี่อัน? มี 2 ทัศนะที่ต่างกัน

```javascript
// 🎯 ทัศนะที่ 1: "One Assertion Per Test"
// ข้อดี: ชัดเจนว่า Test ล้มเหลวตรงไหน
// ข้อเสีย: โค้ดเยอะ, Test ช้า

test("should return user with id", () => {
  const user = await userService.getUser(1);
  expect(user.id).toBe(1); // ✅ Assertion 1 อัน
});

test("should return user with correct name", () => {
  const user = await userService.getUser(1);
  expect(user.name).toBe("John"); // ✅ Assertion 1 อัน
});

test("should return user with correct email", () => {
  const user = await userService.getUser(1);
  expect(user.email).toBe("john@example.com"); // ✅ Assertion 1 อัน
});

// ❌ ปัญหา: ต้องเรียก getUser() 3 ครั้ง, Test 3 อัน ช้า!
```

```javascript
// 🎯 ทัศนะที่ 2: "Multiple Related Assertions"
// ข้อดี: ลดความซ้ำซ้อน, Test เร็ว, โค้ดเรียบร้อย
// ข้อเสีย: ถ้า Assertion แรก fail ก็ไม่รู้ว่า assertion อื่นจะ pass ไหม

// ✅ Good: Related Assertions (ที่เกี่ยวข้องกัน)
test("should return complete user object", () => {
  const user = await userService.getUser(1);

  expect(user).toBeDefined();
  expect(user.id).toBe(1);
  expect(user.name).toBe("John");
  expect(user.email).toBe("john@example.com");
  expect(user).toHaveProperty("role", "user");
});

// ✅ Good: แยก Test เมื่อทดสอบ Behavior ต่างกัน
test("should return user for valid id", async () => {
  const user = await userService.getUser(1);
  expect(user).toBeDefined();
  expect(user.name).toBe("John");
});

test("should throw error for invalid id", async () => {
  await expect(userService.getUser(-1)).rejects.toThrow();
});

test("should throw error for non-existent user", async () => {
  await expect(userService.getUser(999)).rejects.toThrow("User not found");
});
```

#### ✅ Guideline

1. **Assertions ที่เกี่ยวข้องกัน** (ทดสอบวัตถุเดียวกัน) → ใช้ Multiple Assertions ได้
2. **Assertions ที่แยกต่างหาก** (ทดสอบ Behavior ต่างกัน) → แยก Test ออก
3. **Success Case vs Error Case** → ต้องแยก Test!

### 10.4 Avoid Testing Implementation Details (ทดสอบ Behavior ไม่ใช่ Implementation)

**ปัญหา:** ถ้า Test ตรวจสอบรายละเอียด Implementation (เช่น Private Property) ก็จะเสี่ยง Break ได้ง่าย เมื่อเปลี่ยน Implementation

```javascript
// ❌ BAD: ทดสอบ Implementation Detail (Private Property)
class ShoppingCart {
  constructor() {
    this._items = []; // ❌ Private property (ต้องมี underscore)
  }

  addItem(item) {
    this._items.push(item);
  }

  getItemCount() {
    return this._items.length;
  }
}

test("should set internal _items array", () => {
  const cart = new ShoppingCart();
  cart.addItem({ name: "Book" });

  // ❌ ทดสอบ Private property โดยตรง - จะ Break เมื่อเปลี่ยนชื่อ
  expect(cart._items).toHaveLength(1);
  expect(cart._items[0].name).toBe("Book");
});

// ปัญหา: ถ้า Implementation เปลี่ยน (เช่น เปลี่ยน _items เป็น _products)
// Test นี้ก็จะ FAIL แม้ว่า Public Behavior ยังเหมือนเดิม

// ✅ GOOD: ทดสอบ Behavior ผ่าน Public Interface
test("should increase item count when item added", () => {
  const cart = new ShoppingCart();

  cart.addItem({ name: "Book" });

  // ✅ ทดสอบผ่าน Public Method (getItemCount)
  expect(cart.getItemCount()).toBe(1);
});

test("should maintain item details", () => {
  const cart = new ShoppingCart();
  const item = { name: "Book", price: 100 };

  cart.addItem(item);

  // ✅ ทดสอบผ่าน Public Method (getItems หรือ iterator)
  const items = cart.getItems();
  expect(items).toHaveLength(1);
  expect(items[0]).toEqual(item);
});
```

#### ทำไมจึงสำคัญ?

```javascript
// Example: Implementation เปลี่ยนแปลง

// ✅ ก่อนหน้า: ใช้ Array
class ShoppingCart {
  constructor() {
    this._items = [];
  }
}

// 🔄 ต่อมา: Refactor ให้ใช้ Set แทน (เพื่อ Performance)
class ShoppingCart {
  constructor() {
    this._itemsSet = new Set(); // ✅ ยังทำงานได้เหมือนเดิม
  }
}

// ❌ Test ที่ทดสอบ Implementation Detail จะ FAIL
// test("should set internal _items array") ← ❌ _items ไม่มีแล้ว!

// ✅ Test ที่ทดสอบ Behavior จะยังคงผ่าน
// test("should increase item count when item added") ← ✅ Public behavior ยังเหมือนเดิม
```

### 10.5 Test Isolation - Tests ต้องเป็นอิสระจากกัน

**ปัญหา:** ถ้า Test depends on each other (ขึ้นต่อกัน) ก็จะเสี่ยง:

- Test ล้มเหลวเพราะ Test ก่อนหน้า fail
- ไม่สามารถรัน Test เดียวได้ (ต้องรัน Setup Test ก่อน)
- ยากต่อการ Debug

```javascript
// ❌ BAD: Tests depend on each other
let userId;

test("should create user", async () => {
  const user = await userService.create({ name: "John" });
  userId = user.id; // ❌ Store in global variable
  expect(user).toBeDefined();
});

test("should get created user", async () => {
  // ❌ Depends on userId จากครั้งก่อน
  const user = await userService.getUser(userId);
  expect(user.name).toBe("John");
});

test("should update created user", async () => {
  // ❌ Depends on userId จากครั้งแรก
  await userService.update(userId, { name: "Jane" });
  expect(true).toBe(true);
});

// ปัญหา:
// - ถ้า "should create user" fail ก็ userId ไม่มี
// - การรัน "should get created user" เพียงลำพัง ก็ fail
// - ต้องรัน tests ตามลำดับ

// ✅ GOOD: Each test is independent (ใช้ beforeEach)
describe("UserService", () => {
  let testUser;

  // beforeEach ถูก run ก่อน test แต่ละอัน
  beforeEach(async () => {
    // Setup - สร้าง test data ใหม่ ทุกครั้ง
    testUser = await userService.create({ name: "John" });
  });

  afterEach(async () => {
    // Cleanup - ลบข้อมูล test หลังแต่ละ test
    await userService.delete(testUser.id);
  });

  test("should create user", () => {
    expect(testUser).toBeDefined();
    expect(testUser.name).toBe("John");
  });

  test("should get user", async () => {
    // ✅ ไม่ depends on previous test
    const user = await userService.getUser(testUser.id);
    expect(user.name).toBe("John");
  });

  test("should update user", async () => {
    // ✅ ไม่ depends on previous test
    await userService.update(testUser.id, { name: "Jane" });
    const updated = await userService.getUser(testUser.id);
    expect(updated.name).toBe("Jane");
  });

  // สามารถรัน Test เดียว ก็ได้ ✅
  // npm test -- -t "should get user"
});
```

#### ข้อดีของ Test Isolation

| ข้อดี              | รายละเอียด                                       |
| ------------------ | ------------------------------------------------ |
| **Independent**    | สามารถรัน Test ใดก็ได้ เพียงลำพัง                |
| **Maintainable**   | ลบ Test เดียวโดยไม่กระทบ Test อื่น               |
| **Debuggable**     | ถ้า Fail ก็รู้ว่าเป็นปัญหาของ Test นั้นเอง       |
| **Parallelizable** | Jest สามารถรัน Tests ขนาน (Parallel) ได้เร็วขึ้น |

### 10.6 Test Edge Cases (กรณีขอบเขต)

#### 🎯 Edge Case คืออะไร?

Edge Case คือ **กรณีที่อยู่ที่ขอบสุดของการใช้งาน** ซึ่งมักจะเป็นจุดที่มีความเสี่ยงเกิด Bug สูง:

- **Boundary Values** - ค่าทีอยู่ที่เส้นแบ่ง (เช่น 0, negative, max value)
- **Empty/Null/Undefined** - ค่าว่าง, null หรือ undefined
- **Invalid Input** - ข้อมูลที่ไม่ถูกต้องตามกฎ
- **Extreme Values** - ค่าที่มากจนเกินไปหรือน้อยจนเกินไป

#### ตัวอย่างจริง

```javascript
// ฟังก์ชันป้อนตัวเลข 0-100 แล้วให้เกรด
function calculateGrade(score) {
  if (score >= 80) return "A";
  else if (score >= 70) return "B";
  else if (score >= 60) return "C";
  else if (score >= 50) return "D";
  else return "F";
}

// ❌ Test ที่ไม่สมบูรณ์ (ทำให้ได้เกรด F)
test("calculates correct grade", () => {
  expect(calculateGrade(85)).toBe("A"); // เพียง 1 case!
}); // Coverage: 20% เท่านั้น

// ✅ Test ที่ครบถ้วน (Cover ทุก Edge Case)
describe("calculateGrade - Edge Cases", () => {
  test.each([
    // Normal Cases
    [85, "A"],
    [75, "B"],
    [65, "C"],
    [55, "D"],
    [45, "F"],

    // Boundary Cases (ขอบเขต) - สำคัญมาก!
    [80, "A"], // Boundary ของ A
    [79, "B"], // ขึ้นก่อนขึ้น A
    [70, "B"], // Boundary ของ B
    [69, "C"], // ขึ้นก่อนขึ้น B
    [60, "C"], // Boundary ของ C
    [59, "D"], // ขึ้นก่อนขึ้น C
    [50, "D"], // Boundary ของ D
    [49, "F"], // ขึ้นก่อนขึ้น D

    // Extreme Cases (ค่าสุดโต่ง)
    [0, "F"], // ต่ำสุด
    [100, "A"], // สูงสุด
  ])("should return %s for score %i", (score, expected) => {
    expect(calculateGrade(score)).toBe(expected);
  });
});
// Coverage: 100% ✅ ทั้ง statements, branches, lines
```

#### ตัวอย่างอื่น: ฟังก์ชัน Division

```javascript
function divideNumbers(a, b) {
  if (b === 0) throw new Error("Cannot divide by zero");
  return a / b;
}

describe("divideNumbers - Edge Cases", () => {
  // Normal cases
  test("divides positive numbers", () => {
    expect(divideNumbers(10, 2)).toBe(5);
  });

  // Edge Case 1: Division by Zero (Error)
  test("throws error when dividing by zero", () => {
    expect(() => divideNumbers(10, 0)).toThrow("Cannot divide by zero");
  });

  // Edge Case 2: Negative Numbers
  test("handles negative divisor", () => {
    expect(divideNumbers(10, -2)).toBe(-5);
  });

  test("handles negative dividend", () => {
    expect(divideNumbers(-10, 2)).toBe(-5);
  });

  test("handles both negative", () => {
    expect(divideNumbers(-10, -2)).toBe(5);
  });

  // Edge Case 3: Zero Dividend
  test("dividing zero returns zero", () => {
    expect(divideNumbers(0, 5)).toBe(0);
  });

  // Edge Case 4: Decimal Results
  test("handles decimal results", () => {
    expect(divideNumbers(10, 3)).toBeCloseTo(3.333, 2); // ใกล้เคียง 3.333
  });

  // Edge Case 5: Very Small/Large Numbers
  test("handles very large numbers", () => {
    expect(divideNumbers(1000000, 2)).toBe(500000);
  });

  test("handles very small numbers", () => {
    expect(divideNumbers(0.0001, 2)).toBeCloseTo(0.00005);
  });
});
```

#### ทำไมจึงต้องทดสอบ Edge Cases?

```javascript
// ❌ โค้ดนี้มี Bug ที่เกิดเฉพาะในกรณี Edge Case
function getDiscount(price) {
  // ขี้จัด!
  if (price > 100)
    return 0.1; // ต้องเปลี่ยน > เป็น >=
  else return 0;
}

// Test ที่ขาดหายไปได้ทำให้ Bug มี
test("incorrect boundary test", () => {
  expect(getDiscount(100)).toBe(0); // ❌ Fail! ควรได้ 0.1
  expect(getDiscount(101)).toBe(0.1); // ✅ Pass
});

// ผลมาจากการไม่ทดสอบ Boundary อย่างครบถ้วน

// ✅ ถ้าเขียน Test อย่างครบถ้วน ก็จะเจอ Bug ตั้งแต่เริ่ม
test("boundary test for getDiscount", () => {
  expect(getDiscount(99)).toBe(0); // Price < 100
  expect(getDiscount(100)).toBe(0.1); // ❌ Reveal Bug!
  expect(getDiscount(101)).toBe(0.1); // Price > 100
});
```

#### ✅ Checklist สำหรับ Edge Cases

เมื่อเขียน Test ให้ถามตัวเอง:

- [ ] ทดสอบค่า Boundary (ขอบเขต) แล้วหรือ?
- [ ] ทดสอบค่า Null/Undefined แล้วหรือ?
- [ ] ทดสอบค่า Empty String/Array แล้วหรือ?
- [ ] ทดสอบค่า Negative (ถ้าเกี่ยวข้อง) แล้วหรือ?
- [ ] ทดสอบค่า Zero แล้วหรือ?
- [ ] ทดสอบค่าสุดโต่ง (Max/Min) แล้วหรือ?
- [ ] ทดสอบ Error Cases (Exception) แล้วหรือ?
- [ ] ทดสอบ Invalid Input แล้วหรือ?

### 10.7 Keep Tests Fast (ให้ Test รวดเร็ว)

**ทำไมสำคัญ?** ถ้า Tests ช้า นักพัฒนาจะไม่อยากรันบ่อย → Bug เยอะ เพราะ Test ข้อมูลย้อยหลัง

```javascript
// ❌ BAD: Test ที่ช้าโดยไม่จำเป็น
test("should wait for timeout", async () => {
  // ❌ รอ 5 วินาที เพียงแค่เพื่อ pass test?
  await new Promise((r) => setTimeout(r, 5000));
  expect(true).toBe(true);
});

test("should call API and wait for response", async () => {
  // ❌ เรียก API จริง - ช้า, ไม่เสถียร, ต้องเครือข่าย
  const response = await fetch("https://api.example.com/users");
  expect(response.status).toBe(200);
});

// ✅ GOOD: ใช้ Fake Timers แทน setTimeout จริง
jest.useFakeTimers(); // ⚡ บอก Jest ให้ mock setTimeout/setInterval

test("should call callback after timeout", () => {
  const callback = jest.fn();

  setTimeout(callback, 5000);

  // ⚡ "ข้าม" เวลาจริง ไปเลย 5 วินาที (ไม่รอจริง!)
  jest.advanceTimersByTime(5000);

  expect(callback).toHaveBeenCalled();
  // Test เสร็จใน milliseconds แทนที่จะ 5 วินาที ✅
});

// ✅ GOOD: Mock API แทนเรียก API จริง
const mockApiClient = {
  getUsers: jest.fn().mockResolvedValue({ status: 200, data: [] }),
};

test("should get users from API", async () => {
  // ⚡ ใช้ Mock - ได้ผลทันที (ไม่ต้องรอ Network)
  const response = await mockApiClient.getUsers();
  expect(response.status).toBe(200);
  // Test เสร็จใน milliseconds ✅
});

// ✅ GOOD: ใช้ beforeEach แทน beforeAll (เมื่อจำเป็น)
// beforeEach รันแต่ละ test (สั้น ๆ setup)
// beforeAll รันครั้งเดียว (setup ที่นาน ๆ)

describe("Database Tests", () => {
  // ❌ ถ้าใช้ beforeEach สำหรับ heavy setup เก่า lag
  // beforeEach(async () => {
  //   // Seed 10,000 records - ช้า!
  //   db.seed(largeDataset);
  // });

  // ✅ ใช้ beforeAll สำหรับ heavy setup
  beforeAll(async () => {
    // Seed 10,000 records - ทำครั้งเดียว
    db.seed(largeDataset);
  });

  // ✅ ใช้ beforeEach สำหรับ lightweight setup เท่านั้น
  beforeEach(async () => {
    // Create single test record - เร็ว
    testRecord = await db.create({ name: "Test" });
  });

  test("test 1", () => {
    // ทุก test ใช้ shared setup ✅
  });
});
```

#### เทคนิคเพิ่มความเร็ว

| เทคนิค                  | ก่อน                     | หลัง                 |
| ----------------------- | ------------------------ | -------------------- |
| Mock External APIs      | 2-3 sec                  | 10-50 ms             |
| Fake Timers             | 5 sec (for timeout test) | <1 ms                |
| Test in-memory database | 1-2 sec (per test)       | 5-20 ms              |
| Parallel test execution | 100 sec (sequential)     | 25-30 sec (parallel) |

### 10.8 Common Jest Commands

```bash
# รัน tests ทั้งหมด
npm test

# รัน tests ใน watch mode
npm test -- --watch

# รันเฉพาะบาง tests
npm test -- --testNamePattern="should create"
npm test -- Book.test.js

# รัน tests พร้อม coverage
npm test -- --coverage

# รัน tests แบบ verbose
npm test -- --verbose

# รัน tests ตาม pattern
npm test -- --testPathPattern="unit"

# Update snapshots
npm test -- --updateSnapshot
```

---

## 11. แบบฝึกหัด

### แบบฝึกหัดที่ 1: Basic Testing

สร้าง `Calculator` class และเขียน tests ให้ครบ:

```javascript
// src/Calculator.js
class Calculator {
  add(a, b) {
    /* implement */
  }
  subtract(a, b) {
    /* implement */
  }
  multiply(a, b) {
    /* implement */
  }
  divide(a, b) {
    /* implement - throw error if b is 0 */
  }
}
```

**ข้อกำหนด:**

- ทดสอบทุก methods
- ทดสอบ edge cases (division by zero, negative numbers)
- Coverage ต้องได้ 100%

### แบบฝึกหัดที่ 2: User Class

สร้าง `User` class พร้อม tests:

```javascript
class User {
  constructor(name, email, age) {
    /* validate and set properties */
  }
  isAdult() {
    /* return true if age >= 18 */
  }
  updateEmail(newEmail) {
    /* validate and update email */
  }
  toJSON() {
    /* return user as plain object */
  }
}
```

**ข้อกำหนด:**

- Validation: name ต้องไม่ว่าง, email ต้องมี @, age ต้อง > 0
- จัดกลุ่ม tests ด้วย describe
- ใช้ beforeEach สร้าง test user

### แบบฝึกหัดที่ 3: Async Testing

สร้าง `UserService` class ที่ใช้ mock API:

```javascript
class UserService {
  constructor(apiClient) {
    this.api = apiClient;
  }

  async getUser(id) {
    /* call api.get('/users/:id') */
  }
  async createUser(data) {
    /* call api.post('/users', data) */
  }
  async updateUser(id, data) {
    /* call api.put('/users/:id', data) */
  }
  async deleteUser(id) {
    /* call api.delete('/users/:id') */
  }
}
```

**ข้อกำหนด:**

- Mock apiClient ด้วย jest.fn()
- ทดสอบ success cases
- ทดสอบ error cases (API failure)
- ใช้ async/await

### แบบฝึกหัดที่ 4: Book และ Library System

สร้าง `Library` class ที่จัดการ books:

```javascript
class Library {
  constructor() {
    this.books = [];
  }

  addBook(book) {
    /* add book to library */
  }
  removeBook(isbn) {
    /* remove book by isbn */
  }
  findBook(isbn) {
    /* find book by isbn */
  }
  searchBooks(query) {
    /* search by title or author */
  }
  borrowBook(isbn, memberId) {
    /* borrow a book */
  }
  returnBook(isbn, memberId) {
    /* return a book */
  }
  getAvailableBooks() {
    /* get all available books */
  }
  getBorrowedBooks(memberId) {
    /* get books borrowed by member */
  }
}
```

**ข้อกำหนด:**

- Coverage อย่างน้อย 80%
- ใช้ describe จัดกลุ่ม
- ทดสอบ edge cases ทั้งหมด
- ใช้ test.each สำหรับ searchBooks

---

## Quick Reference Card

### Test Structure

```javascript
describe("Component", () => {
  beforeEach(() => {
    /* setup */
  });
  afterEach(() => {
    /* cleanup */
  });

  test("should do something", () => {
    expect(actual).toBe(expected);
  });
});
```

### Common Matchers

```javascript
expect(x).toBe(y); // x === y
expect(x).toEqual(y); // deep equality
expect(x).toBeTruthy(); // x is truthy
expect(x).toBeFalsy(); // x is falsy
expect(x).toBeNull(); // x === null
expect(x).toBeDefined(); // x !== undefined
expect(x).toContain(y); // array/string contains y
expect(x).toHaveLength(n); // x.length === n
expect(x).toThrow(); // function throws
expect(x).toMatch(/regex/); // string matches regex
```

### Mocking

```javascript
const mockFn = jest.fn();
mockFn.mockReturnValue(x);
mockFn.mockResolvedValue(x);
mockFn.mockRejectedValue(x);
jest.mock("./module");
jest.spyOn(obj, "method");
```

### Async Testing

```javascript
test("async", async () => {
  const result = await asyncFn();
  expect(result).toBe(x);
});

test("rejects", async () => {
  await expect(asyncFn()).rejects.toThrow();
});
```

---

## เอกสารอ้างอิง

- [Jest Official Documentation](https://jestjs.io/docs/getting-started)
- [Jest API Reference](https://jestjs.io/docs/api)
- [Jest Expect API](https://jestjs.io/docs/expect)
- [Jest Mock Functions](https://jestjs.io/docs/mock-functions)

---

---

---

## เพิ่มเติม

## 1. การทำ Mocking ที่ซับซ้อน (Complex Mocking)

ในการทำงานจริง ระบบมักจะมี Dependency ที่ซ้อนกันหลายชั้น การ Mock แค่ฟังก์ชันเดียวอาจไม่พอครับ

### การทำ Partial Mocking

บางครั้งเราต้องการ Mock แค่บางฟังก์ชันใน Module แต่ยังอยากใช้ฟังก์ชันจริงส่วนที่เหลืออยู่ เราสามารถใช้ `jest.requireActual` ได้ครับ

```javascript
jest.mock("../src/utils/validators", () => {
  const originalModule = jest.requireActual("../src/utils/validators");
  return {
    ...originalModule,
    isEmail: jest.fn().mockReturnValue(true), // Mock แค่ตัวนี้
    // ส่วน isStrongPassword จะยังทำงานจริงตามปกติ
  };
});
```

### การ Mock Third-party Libraries (Deep Mocking)

ตัวอย่างเช่นการ Mock `axios` ที่มีการเรียกใช้งานซ้อนกัน เช่น `axios.create().get()`

```javascript
// ตัวอย่างการ Mock chain function
const mockGet = jest.fn();
jest.mock("axios", () => ({
  create: () => ({
    get: mockGet,
  }),
}));
```

---

## 2. การทำ Code Coverage ให้ได้ 100%

การได้เลข 100% ไม่ได้หมายความว่าโปรแกรมไม่มี Bug แต่หมายความว่า **"ทุกบรรทัดและทุกเงื่อนไขถูกรันผ่าน Test อย่างน้อย 1 ครั้ง"**

### กลยุทธ์การเก็บ Coverage ให้ครบ

- **Branch Coverage (เงื่อนไข):** ต้องทดสอบทั้งกรณี `if` เป็นจริง และ `else` เป็นเท็จ รวมถึงการใช้ Ternary Operator
- **Edge Case Analysis:** ทดสอบค่าขอบเขต (Boundary Value) เช่น หากฟังก์ชันรับตัวเลข 1-100 ต้องทดสอบ 0, 1, 100, และ 101
- **Error Handling:** อย่าลืมเขียน Test เพื่อให้ Code เข้าไปที่ส่วน `catch` block โดยการ Mock ให้ Dependency ตัวนั้น `reject` หรือ `throw error` ออกมา

### ตารางตรวจสอบ Coverage

| ส่วนที่ต้องตรวจ        | วิธีการทดสอบ                                                                  |
| ---------------------- | ----------------------------------------------------------------------------- |
| **Logic Branches**     | ใช้ `test.each()` เพื่อส่ง Parameter หลายๆ ชุดเข้าเงื่อนไขที่ต่างกัน          |
| **Optional Chaining**  | ตรวจสอบกรณีที่ Object เป็น `null` หรือ `undefined` ในจุดที่มีเครื่องหมาย `?.` |
| **Default Parameters** | ทดสอบการเรียกฟังก์ชันโดยไม่ส่ง Argument เพื่อดูว่า Default Value ทำงานไหม     |

---

## 3. แนะนำแนวคิด TDD (Test-Driven Development)

TDD คือการเปลี่ยน Workflow จากการ "เขียน Code แล้วค่อยเขียน Test" เป็นการ **"เขียน Test ก่อนเขียน Code"**

### วงจร Red-Green-Refactor

1. **🔴 RED (Fail):** เขียน Test สำหรับ Feature ใหม่ (รันแล้วต้องพัง เพราะยังไม่ได้เขียน Code)
2. **🟢 GREEN (Pass):** เขียน Code ให้เรียบง่ายที่สุดเพื่อให้ Test นั้นผ่าน
3. **🔵 REFACTOR (Clean):** ปรับปรุง Code ให้สะอาด (Clean Code) โดยที่ Test ยังต้องผ่านเหมือนเดิม

### ทำไมถึงควรใช้ TDD?

- **Design Quality:** ช่วยให้เราคิดถึง Interface ของฟังก์ชันก่อนที่จะลงมือเขียนจริง (คิดแบบ User-centric)
- **Confidence:** เมื่อเราแก้ไข Code เก่า เราจะรู้ทันทีว่ากระทบส่วนอื่นไหมเพราะมี Test คุมไว้ตลอด
- **Less Debugging:** ลดเวลาที่ต้องมานั่งงมหา Bug ในภายหลัง เพราะเราดักไว้ตั้งแต่ตอนเขียนแล้ว

---

---

---

## TDD

เพื่อให้เห็นภาพการทำงานแบบ **TDD (Test-Driven Development)** ในแบบฝึกหัดที่ 4 (Library System) ผมจะจำลองขั้นตอนการสร้างระบบโดยเริ่มจาก "ศูนย์" ตามวงจร **Red-Green-Refactor** ครับ

---

### ขั้นตอนที่ 1: 🔴 RED (เขียน Test ให้พังก่อน)

ตามหลัก TDD เราจะเขียนเฉพาะ Test สำหรับความต้องการแรก คือ **"การเพิ่มหนังสือเข้าห้องสมุด"**

**ไฟล์: `tests/Library.test.js**`

```javascript
const Library = require("../src/Library");

describe("Library System (TDD Example)", () => {
  let library;

  beforeEach(() => {
    library = new Library(); // จัดเตรียมข้อมูล (Arrange)
  });

  test("should add a book to the library", () => {
    const book = { isbn: "123", title: "Test Book", author: "Author" };

    library.addBook(book); // ลงมือทำ (Act)

    // ตรวจสอบผล (Assert)
    expect(library.books).toContainEqual(book);
    expect(library.books).toHaveLength(1);
  });
});
```

_เมื่อคุณรัน `npm test` ตอนนี้จะ **FAIL** เพราะเรายังไม่ได้สร้างไฟล์ `Library.js` เลยครับ_

---

### ขั้นตอนที่ 2: 🟢 GREEN (เขียน Code ให้ Test ผ่าน)

เราจะเขียน Code ที่เรียบง่ายที่สุดเพื่อให้ Test เมื่อครู่รันผ่าน

**ไฟล์: `src/Library.js**`

```javascript
class Library {
  constructor() {
    this.books = []; // กำหนดค่าเริ่มต้นตามคู่มือ
  }

  addBook(book) {
    this.books.push(book); // เขียนแค่พอให้ Test ผ่าน
  }
}

module.exports = Library;
```

_เมื่อรัน `npm test` อีกครั้ง ผลจะเป็น **PASS** 🎉_

---

### ขั้นตอนที่ 3: 🔵 REFACTOR (ปรับปรุง Code)

หาก Code ดูดีแล้ว เราจะขยับไปทำ Feature ถัดไป เช่น **"การยืมหนังสือ"** โดยเริ่มจากวงจร **RED** อีกรอบครับ

**ไฟล์: `tests/Library.test.js` (เพิ่ม Case ใหม่)**

```javascript
describe("borrowBook()", () => {
  test("should allow a member to borrow an available book", () => {
    // Arrange
    const book = { isbn: "123", title: "JS Testing", status: "available" };
    library.addBook(book);

    // Act
    const result = library.borrowBook("123", "member-01");

    // Assert
    expect(result).toBe(true);
    expect(library.findBook("123").status).toBe("unavailable");
  });

  // ทดสอบ Edge Case: ยืมหนังสือที่ไม่มีอยู่จริง
  test("should throw error if book is not found", () => {
    expect(() => library.borrowBook("999", "member-01")).toThrow(
      "Book not found",
    );
  });
});
```

---

### ขั้นตอนที่ 4: 🟢 GREEN (อัปเดต Code ให้รองรับ Feature ใหม่)

**ไฟล์: `src/Library.js` (เพิ่มเติม)**

```javascript
  findBook(isbn) {
    return this.books.find(b => b.isbn === isbn);
  }

  borrowBook(isbn, memberId) {
    const book = this.findBook(isbn);
    if (!book) throw new Error('Book not found'); // จัดการ Edge Case

    if (book.status === 'available') {
      book.status = 'unavailable';
      book.borrower = memberId;
      return true;
    }
    return false;
  }

```

---

### 📊 ผลลัพธ์จากการทำ TDD ในตัวอย่างนี้:

1. **มั่นใจ 100%:** ทุกบรรทัดที่เขียนมี Test รองรับเสมอ
2. **Code สะอาด:** เราเขียนเฉพาะสิ่งที่จำเป็นต้องใช้จริงๆ
3. **ครอบคลุม Edge Cases:** เราคิดถึงกรณี "หนังสือไม่มี" ก่อนจะเขียน Logic การยืมด้วยซ้ำ

---

---

## การเขียน Test ฟังก์ชัน `searchBooks` โดยใช้ `test.each`

ตามที่คู่มือแนะนำในแบบฝึกหัดที่ 4 จะช่วยให้เราสามารถทดสอบ Case ต่างๆ (เช่น ค้นหาจากชื่อเรื่อง, ชื่อผู้แต่ง หรือกรณีไม่พบข้อมูล) ได้ในครั้งเดียวโดยไม่ต้องเขียน `test()` ซ้ำๆ หลายรอบครับ

นี่คือตัวอย่างการทำ TDD สำหรับฟังก์ชันนี้ครับ:

### 1. 🔴 ขั้นตอน RED: เขียน Test ด้วย `test.each`

เราจะกำหนดชุดข้อมูลทดสอบ (Data Table) ไว้ล่วงหน้า ซึ่งประกอบด้วย `query` ที่ใช้ค้นหา และ `expectedCount` ที่คาดหวังว่าจะเจอครับ

**ไฟล์: `tests/Library.test.js**`

```javascript
describe("Library.searchBooks() - TDD with test.each", () => {
  let library;

  beforeEach(() => {
    library = new Library();
    // Arrange: เตรียมข้อมูลตั้งต้น
    library.addBook({
      isbn: "111",
      title: "Node.js Guide",
      author: "John Doe",
    });
    library.addBook({
      isbn: "222",
      title: "React Basics",
      author: "Jane Smith",
    });
    library.addBook({
      isbn: "333",
      title: "Advanced Node.js",
      author: "John Doe",
    });
  });

  // ใช้ test.each เพื่อรันชุดข้อมูลทดสอบหลายๆ กรณี
  test.each([
    ["Node.js", 2, "should find books by title pattern"],
    ["Jane", 1, "should find books by author name"],
    ["Python", 0, "should return empty array if no matches"],
    ["", 3, "should return all books if query is empty"],
  ])("%s: %s", (query, expectedCount, description) => {
    // Act: เรียกใช้ฟังก์ชันค้นหา
    const results = library.searchBooks(query);

    // Assert: ตรวจสอบจำนวนผลลัพธ์ที่ได้
    expect(results).toHaveLength(expectedCount);
  });
});
```

---

### 2. 🟢 ขั้นตอน GREEN: เขียน Logic การค้นหา

เราจะเขียน Code ใน `Library.js` ให้สามารถค้นหาได้ทั้งจาก `title` และ `author` ตามที่โจทย์กำหนดครับ

**ไฟล์: `src/Library.js**`

```javascript
class Library {
  constructor() {
    this.books = [];
  }

  addBook(book) {
    this.books.push(book);
  }

  searchBooks(query) {
    // ถ้า query ว่าง ให้คืนค่าหนังสือทั้งหมด
    if (!query) return this.books;

    const lowerQuery = query.toLowerCase();

    // ค้นหาว่า query ตรงกับชื่อเรื่องหรือชื่อผู้แต่งหรือไม่
    return this.books.filter(
      (book) =>
        book.title.toLowerCase().includes(lowerQuery) ||
        book.author.toLowerCase().includes(lowerQuery),
    );
  }
}
```

---

### 3. 🔵 ขั้นตอน REFACTOR และเช็ค Coverage

หลังจาก Test ผ่านแล้ว เราสามารถปรับปรุง Code ให้กระชับขึ้นได้ และอย่าลืมรัน Coverage Report เพื่อดูว่าเราทดสอบครอบคลุมทุกบรรทัดหรือยัง (เป้าหมายคืออย่างน้อย 80% ตามโจทย์)

```bash
npm test -- --coverage

```

### 💡 วิเคราะห์สิ่งที่ได้จากตัวอย่างนี้:

- **ความกระชับ:** `test.each` ช่วยให้เราเพิ่ม Case ใหม่ๆ (เช่น ค้นหาแบบ Case-insensitive) ได้ง่ายๆ เพียงแค่เพิ่มบรรทัดใน Array เท่านั้น
- **การจัดกลุ่ม:** การใช้ `describe` ครอบ `test.each` ทำให้ Report ที่ได้อ่านง่ายและเป็นระเบียบ
- **ครอบคลุม Edge Cases:** เราได้ทดสอบกรณี Query เป็นค่าว่าง หรือค้นหาแล้วไม่เจอ ซึ่งเป็นจุดที่มักจะเกิด Bug ได้บ่อย

**หากคุณต้องการให้ผมช่วยเขียน Test สำหรับส่วนที่ต้องใช้ Mocking เช่น `borrowBook` ที่ต้องไปตัดยอดจากระบบอื่น แจ้งได้เลยนะครับ!**
