# 🧪 Jest: Learning by Example
## จากศูนย์สู่ Pro ด้วยตัวอย่างจริง

---

# 📚 สารบัญ

| Level | หัวข้อ | ความยาก |
|-------|--------|---------|
| 1 | [Test แรกของคุณ](#level-1-test-แรกของคุณ) | ⭐ |
| 2 | [Matchers พื้นฐาน](#level-2-matchers-พื้นฐาน) | ⭐ |
| 3 | [Matchers ขั้นกลาง](#level-3-matchers-ขั้นกลาง) | ⭐⭐ |
| 4 | [จัดกลุ่ม Test ด้วย describe](#level-4-จัดกลุ่ม-test-ด้วย-describe) | ⭐⭐ |
| 5 | [Setup และ Teardown](#level-5-setup-และ-teardown) | ⭐⭐ |
| 6 | [ทดสอบ Function จริง](#level-6-ทดสอบ-function-จริง) | ⭐⭐⭐ |
| 7 | [ทดสอบ Class และ Object](#level-7-ทดสอบ-class-และ-object) | ⭐⭐⭐ |
| 8 | [ทดสอบ Error และ Exception](#level-8-ทดสอบ-error-และ-exception) | ⭐⭐⭐ |
| 9 | [Async/Await Testing](#level-9-asyncawait-testing) | ⭐⭐⭐⭐ |
| 10 | [Mocking พื้นฐาน](#level-10-mocking-พื้นฐาน) | ⭐⭐⭐⭐ |
| 11 | [Mocking ขั้นสูง](#level-11-mocking-ขั้นสูง) | ⭐⭐⭐⭐⭐ |
| 12 | [Test Driven Development](#level-12-test-driven-development) | ⭐⭐⭐⭐⭐ |
| 13 | [โปรเจกต์จริง: Library System](#level-13-โปรเจกต์จริง-library-system) | ⭐⭐⭐⭐⭐ |

---

# 🚀 เริ่มต้น: ติดตั้ง Jest

```bash
# สร้างโฟลเดอร์โปรเจกต์
mkdir jest-tutorial
cd jest-tutorial

# สร้าง package.json
npm init -y

# ติดตั้ง Jest
npm install --save-dev jest

# แก้ไข package.json เพิ่ม test script
```

แก้ไข `package.json`:
```json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}
```

**โครงสร้างโฟลเดอร์:**
```
jest-tutorial/
├── src/
│   └── (โค้ดของเรา)
├── tests/
│   └── (ไฟล์ test)
├── package.json
└── jest.config.js (optional)
```

---

# Level 1: Test แรกของคุณ
> ⭐ ความยาก: ง่ายมาก

## 🎯 เป้าหมาย
เขียน test แรกให้ผ่าน (และไม่ผ่าน)

## 📝 ตัวอย่าง 1.1: Hello Jest

สร้างไฟล์ `tests/first.test.js`:

```javascript
// Test ที่ง่ายที่สุดในโลก
test('สองบวกสองเท่ากับสี่', () => {
  expect(2 + 2).toBe(4);
});
```

**รัน:**
```bash
npm test
```

**ผลลัพธ์:**
```
 PASS  tests/first.test.js
  ✓ สองบวกสองเท่ากับสี่ (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
```

## 📝 ตัวอย่าง 1.2: Test ที่ไม่ผ่าน

```javascript
test('ทดสอบที่จะไม่ผ่าน', () => {
  expect(2 + 2).toBe(5);  // ❌ ผิด!
});
```

**ผลลัพธ์:**
```
 FAIL  tests/first.test.js
  ✕ ทดสอบที่จะไม่ผ่าน (3 ms)

  Expected: 5
  Received: 4
```

## 📝 ตัวอย่าง 1.3: หลาย Test ในไฟล์เดียว

```javascript
test('บวกเลข', () => {
  expect(1 + 1).toBe(2);
});

test('ลบเลข', () => {
  expect(5 - 3).toBe(2);
});

test('คูณเลข', () => {
  expect(3 * 4).toBe(12);
});

test('หารเลข', () => {
  expect(10 / 2).toBe(5);
});
```

## 💡 สิ่งที่เรียนรู้

| คำสั่ง | ความหมาย |
|--------|----------|
| `test('ชื่อ', () => {})` | สร้าง test case ใหม่ |
| `expect(ค่าจริง)` | ค่าที่ต้องการทดสอบ |
| `.toBe(ค่าที่คาดหวัง)` | เปรียบเทียบว่าเท่ากันไหม |

---

# Level 2: Matchers พื้นฐาน
> ⭐ ความยาก: ง่าย

## 🎯 เป้าหมาย
เรียนรู้ Matchers ที่ใช้บ่อยที่สุด

## 📝 ตัวอย่าง 2.1: toBe vs toEqual

```javascript
// toBe ใช้กับ primitive (number, string, boolean)
test('toBe สำหรับ primitive', () => {
  const a = 5;
  expect(a).toBe(5);
  
  const name = 'Jest';
  expect(name).toBe('Jest');
  
  const isActive = true;
  expect(isActive).toBe(true);
});

// toEqual ใช้กับ object และ array
test('toEqual สำหรับ object', () => {
  const user = { name: 'John', age: 25 };
  expect(user).toEqual({ name: 'John', age: 25 });
});

test('toEqual สำหรับ array', () => {
  const numbers = [1, 2, 3];
  expect(numbers).toEqual([1, 2, 3]);
});
```

## 📝 ตัวอย่าง 2.2: ทำไม toBe ใช้กับ Object ไม่ได้

```javascript
test('toBe กับ object - จะ FAIL!', () => {
  const obj1 = { a: 1 };
  const obj2 = { a: 1 };
  
  // ❌ FAIL - เพราะเป็นคนละ reference
  // expect(obj1).toBe(obj2);
  
  // ✅ PASS - ใช้ toEqual แทน
  expect(obj1).toEqual(obj2);
});

test('toBe กับ object เดียวกัน - PASS', () => {
  const obj1 = { a: 1 };
  const obj2 = obj1;  // reference เดียวกัน
  
  expect(obj1).toBe(obj2);  // ✅ PASS
});
```

## 📝 ตัวอย่าง 2.3: Truthiness Matchers

```javascript
test('ทดสอบค่า null', () => {
  const value = null;
  expect(value).toBeNull();
  expect(value).toBeDefined();
  expect(value).not.toBeUndefined();
});

test('ทดสอบค่า undefined', () => {
  let value;
  expect(value).toBeUndefined();
  expect(value).not.toBeDefined();
});

test('ทดสอบ truthy/falsy', () => {
  // Truthy values
  expect(1).toBeTruthy();
  expect('hello').toBeTruthy();
  expect([]).toBeTruthy();
  expect({}).toBeTruthy();
  
  // Falsy values
  expect(0).toBeFalsy();
  expect('').toBeFalsy();
  expect(null).toBeFalsy();
  expect(undefined).toBeFalsy();
});
```

## 📝 ตัวอย่าง 2.4: not - กลับความหมาย

```javascript
test('ใช้ not กลับความหมาย', () => {
  expect(5).not.toBe(3);
  expect('hello').not.toBe('world');
  expect([1, 2]).not.toEqual([1, 2, 3]);
  expect(null).not.toBeUndefined();
});
```

## 💡 สรุป Matchers พื้นฐาน

| Matcher | ใช้ทำอะไร | ตัวอย่าง |
|---------|----------|----------|
| `toBe(x)` | เท่ากับ x (primitive) | `expect(5).toBe(5)` |
| `toEqual(x)` | เท่ากับ x (object/array) | `expect({a:1}).toEqual({a:1})` |
| `toBeNull()` | เป็น null | `expect(null).toBeNull()` |
| `toBeUndefined()` | เป็น undefined | `expect(undefined).toBeUndefined()` |
| `toBeDefined()` | ไม่ใช่ undefined | `expect(5).toBeDefined()` |
| `toBeTruthy()` | เป็นค่า truthy | `expect(1).toBeTruthy()` |
| `toBeFalsy()` | เป็นค่า falsy | `expect(0).toBeFalsy()` |
| `not` | กลับความหมาย | `expect(5).not.toBe(3)` |

---

# Level 3: Matchers ขั้นกลาง
> ⭐⭐ ความยาก: ปานกลาง

## 📝 ตัวอย่าง 3.1: Number Matchers

```javascript
test('เปรียบเทียบตัวเลข', () => {
  const value = 10;
  
  expect(value).toBeGreaterThan(5);        // > 5
  expect(value).toBeGreaterThanOrEqual(10); // >= 10
  expect(value).toBeLessThan(20);          // < 20
  expect(value).toBeLessThanOrEqual(10);   // <= 10
});

test('ทศนิยม - ใช้ toBeCloseTo', () => {
  // ❌ อย่าใช้ toBe กับทศนิยม!
  // expect(0.1 + 0.2).toBe(0.3); // FAIL เพราะ floating point error
  
  // ✅ ใช้ toBeCloseTo แทน
  expect(0.1 + 0.2).toBeCloseTo(0.3);
  expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // 5 ทศนิยม
});
```

## 📝 ตัวอย่าง 3.2: String Matchers

```javascript
test('ทดสอบ string', () => {
  const message = 'Hello, World!';
  
  // ตรงกับ regex
  expect(message).toMatch(/Hello/);
  expect(message).toMatch(/world/i);  // i = case insensitive
  
  // มีคำว่า
  expect(message).toMatch('World');
  
  // ไม่มีคำว่า
  expect(message).not.toMatch('Goodbye');
});

test('ทดสอบ email format', () => {
  const email = 'test@example.com';
  expect(email).toMatch(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/);
});
```

## 📝 ตัวอย่าง 3.3: Array Matchers

```javascript
test('ทดสอบ array', () => {
  const fruits = ['apple', 'banana', 'orange'];
  
  // มี item อยู่ใน array
  expect(fruits).toContain('banana');
  expect(fruits).not.toContain('grape');
  
  // ความยาว array
  expect(fruits).toHaveLength(3);
});

test('array ของ object', () => {
  const users = [
    { name: 'John', age: 25 },
    { name: 'Jane', age: 30 },
  ];
  
  // ใช้ toContainEqual สำหรับ object
  expect(users).toContainEqual({ name: 'John', age: 25 });
});
```

## 📝 ตัวอย่าง 3.4: Object Matchers

```javascript
test('ทดสอบ object', () => {
  const user = {
    name: 'John',
    age: 25,
    email: 'john@example.com',
    address: {
      city: 'Bangkok',
      country: 'Thailand'
    }
  };
  
  // มี property
  expect(user).toHaveProperty('name');
  expect(user).toHaveProperty('age', 25);  // มี property และค่าตรง
  
  // nested property
  expect(user).toHaveProperty('address.city');
  expect(user).toHaveProperty('address.city', 'Bangkok');
  
  // ตรวจสอบบางส่วนของ object
  expect(user).toMatchObject({
    name: 'John',
    age: 25
  });
});
```

## 📝 ตัวอย่าง 3.5: รวมทุก Matcher

```javascript
test('ตัวอย่างรวม: ข้อมูลหนังสือ', () => {
  const book = {
    title: 'JavaScript Testing',
    author: 'John Doe',
    pages: 350,
    price: 29.99,
    tags: ['javascript', 'testing', 'jest'],
    publisher: {
      name: 'Tech Books',
      year: 2024
    }
  };
  
  // String
  expect(book.title).toMatch(/JavaScript/);
  
  // Number
  expect(book.pages).toBeGreaterThan(300);
  expect(book.price).toBeCloseTo(30, 0);
  
  // Array
  expect(book.tags).toContain('jest');
  expect(book.tags).toHaveLength(3);
  
  // Object
  expect(book).toHaveProperty('publisher.year', 2024);
  expect(book).toMatchObject({
    author: 'John Doe',
    pages: 350
  });
});
```

## 💡 สรุป Matchers ขั้นกลาง

| Matcher | ใช้กับ | ตัวอย่าง |
|---------|--------|----------|
| `toBeGreaterThan(n)` | Number | `expect(10).toBeGreaterThan(5)` |
| `toBeLessThan(n)` | Number | `expect(5).toBeLessThan(10)` |
| `toBeCloseTo(n, d)` | Float | `expect(0.3).toBeCloseTo(0.1+0.2)` |
| `toMatch(regex)` | String | `expect('hi').toMatch(/h/)` |
| `toContain(item)` | Array | `expect([1,2]).toContain(1)` |
| `toContainEqual(obj)` | Array of Object | `expect([{a:1}]).toContainEqual({a:1})` |
| `toHaveLength(n)` | Array/String | `expect([1,2,3]).toHaveLength(3)` |
| `toHaveProperty(key)` | Object | `expect({a:1}).toHaveProperty('a')` |
| `toMatchObject(obj)` | Object | `expect({a:1,b:2}).toMatchObject({a:1})` |

---

# Level 4: จัดกลุ่ม Test ด้วย describe
> ⭐⭐ ความยาก: ปานกลาง

## 🎯 เป้าหมาย
จัดระเบียบ test ให้อ่านง่ายและดูแลง่าย

## 📝 ตัวอย่าง 4.1: describe พื้นฐาน

```javascript
describe('Calculator', () => {
  test('บวกเลขได้ถูกต้อง', () => {
    expect(1 + 2).toBe(3);
  });

  test('ลบเลขได้ถูกต้อง', () => {
    expect(5 - 3).toBe(2);
  });

  test('คูณเลขได้ถูกต้อง', () => {
    expect(4 * 5).toBe(20);
  });

  test('หารเลขได้ถูกต้อง', () => {
    expect(10 / 2).toBe(5);
  });
});
```

**ผลลัพธ์:**
```
 PASS  tests/calculator.test.js
  Calculator
    ✓ บวกเลขได้ถูกต้อง (1 ms)
    ✓ ลบเลขได้ถูกต้อง
    ✓ คูณเลขได้ถูกต้อง (1 ms)
    ✓ หารเลขได้ถูกต้อง
```

## 📝 ตัวอย่าง 4.2: describe ซ้อนกัน (Nested)

```javascript
describe('User Module', () => {
  
  describe('Registration', () => {
    test('สมัครสมาชิกสำเร็จ', () => {
      expect(true).toBe(true);
    });

    test('แจ้งเตือนเมื่อ email ซ้ำ', () => {
      expect(true).toBe(true);
    });
  });

  describe('Login', () => {
    test('login สำเร็จ', () => {
      expect(true).toBe(true);
    });

    test('แจ้งเตือนเมื่อ password ผิด', () => {
      expect(true).toBe(true);
    });
  });

  describe('Profile', () => {
    test('ดูข้อมูลส่วนตัวได้', () => {
      expect(true).toBe(true);
    });

    test('แก้ไขข้อมูลได้', () => {
      expect(true).toBe(true);
    });
  });
});
```

**ผลลัพธ์:**
```
 PASS  tests/user.test.js
  User Module
    Registration
      ✓ สมัครสมาชิกสำเร็จ
      ✓ แจ้งเตือนเมื่อ email ซ้ำ
    Login
      ✓ login สำเร็จ
      ✓ แจ้งเตือนเมื่อ password ผิด
    Profile
      ✓ ดูข้อมูลส่วนตัวได้
      ✓ แก้ไขข้อมูลได้
```

## 📝 ตัวอย่าง 4.3: ใช้ it แทน test

```javascript
// test และ it ทำงานเหมือนกัน
// it อ่านเป็นภาษาอังกฤษได้ลื่นกว่า

describe('Array', () => {
  it('should start empty', () => {
    const arr = [];
    expect(arr).toHaveLength(0);
  });

  it('should add items', () => {
    const arr = [];
    arr.push(1);
    expect(arr).toContain(1);
  });

  it('should remove items', () => {
    const arr = [1, 2, 3];
    arr.pop();
    expect(arr).toEqual([1, 2]);
  });
});
```

## 📝 ตัวอย่าง 4.4: Skip และ Only

```javascript
describe('Feature Tests', () => {
  
  // ข้าม test นี้ไปก่อน
  test.skip('ฟีเจอร์ที่ยังไม่เสร็จ', () => {
    expect(true).toBe(false);
  });

  // รันเฉพาะ test นี้
  // test.only('รันแค่อันนี้', () => {
  //   expect(true).toBe(true);
  // });

  test('ทดสอบปกติ', () => {
    expect(1 + 1).toBe(2);
  });
});

// ข้ามทั้ง describe
describe.skip('Module ที่ยังไม่พร้อม', () => {
  test('test 1', () => {});
  test('test 2', () => {});
});
```

## 📝 ตัวอย่าง 4.5: โครงสร้างแนะนำ

```javascript
// tests/book.test.js

describe('Book', () => {
  
  describe('constructor', () => {
    it('should create a book with title and author', () => {
      // ...
    });

    it('should throw error if title is empty', () => {
      // ...
    });
  });

  describe('methods', () => {
    
    describe('borrow()', () => {
      it('should decrease available copies', () => {
        // ...
      });

      it('should throw error if no copies available', () => {
        // ...
      });
    });

    describe('return()', () => {
      it('should increase available copies', () => {
        // ...
      });
    });
  });
});
```

---

# Level 5: Setup และ Teardown
> ⭐⭐ ความยาก: ปานกลาง

## 🎯 เป้าหมาย
เตรียมข้อมูลก่อน test และทำความสะอาดหลัง test

## 📝 ตัวอย่าง 5.1: beforeEach และ afterEach

```javascript
describe('Shopping Cart', () => {
  let cart;

  // รันก่อนทุก test
  beforeEach(() => {
    cart = [];
    console.log('🛒 สร้างตะกร้าใหม่');
  });

  // รันหลังทุก test
  afterEach(() => {
    cart = null;
    console.log('🗑️ ล้างตะกร้า');
  });

  test('เพิ่มสินค้าลงตะกร้า', () => {
    cart.push({ name: 'iPhone', price: 30000 });
    expect(cart).toHaveLength(1);
  });

  test('ตะกร้าว่างเปล่าเสมอตอนเริ่มต้น', () => {
    // cart ถูก reset แล้วใน beforeEach
    expect(cart).toHaveLength(0);
  });

  test('เพิ่มหลายสินค้า', () => {
    cart.push({ name: 'iPhone', price: 30000 });
    cart.push({ name: 'AirPods', price: 5000 });
    expect(cart).toHaveLength(2);
  });
});
```

## 📝 ตัวอย่าง 5.2: beforeAll และ afterAll

```javascript
describe('Database Tests', () => {
  let db;

  // รันครั้งเดียวก่อน test แรก
  beforeAll(() => {
    console.log('🔌 เชื่อมต่อ Database');
    db = { connected: true };
  });

  // รันครั้งเดียวหลัง test สุดท้าย
  afterAll(() => {
    console.log('🔌 ปิดการเชื่อมต่อ Database');
    db = null;
  });

  beforeEach(() => {
    console.log('  📝 เริ่ม test ใหม่');
  });

  afterEach(() => {
    console.log('  ✅ จบ test');
  });

  test('test 1', () => {
    expect(db.connected).toBe(true);
  });

  test('test 2', () => {
    expect(db.connected).toBe(true);
  });
});
```

**ลำดับการรัน:**
```
🔌 เชื่อมต่อ Database
  📝 เริ่ม test ใหม่
  ✅ จบ test
  📝 เริ่ม test ใหม่
  ✅ จบ test
🔌 ปิดการเชื่อมต่อ Database
```

## 📝 ตัวอย่าง 5.3: Scope ของ Setup/Teardown

```javascript
describe('Outer', () => {
  beforeAll(() => console.log('1 - beforeAll Outer'));
  afterAll(() => console.log('1 - afterAll Outer'));
  beforeEach(() => console.log('1 - beforeEach Outer'));
  afterEach(() => console.log('1 - afterEach Outer'));

  test('Outer test', () => console.log('1 - test Outer'));

  describe('Inner', () => {
    beforeAll(() => console.log('2 - beforeAll Inner'));
    afterAll(() => console.log('2 - afterAll Inner'));
    beforeEach(() => console.log('2 - beforeEach Inner'));
    afterEach(() => console.log('2 - afterEach Inner'));

    test('Inner test', () => console.log('2 - test Inner'));
  });
});
```

**ลำดับการรัน:**
```
1 - beforeAll Outer
1 - beforeEach Outer
1 - test Outer
1 - afterEach Outer
2 - beforeAll Inner
1 - beforeEach Outer    ← outer beforeEach รันก่อน
2 - beforeEach Inner
2 - test Inner
2 - afterEach Inner
1 - afterEach Outer     ← outer afterEach รันทีหลัง
2 - afterAll Inner
1 - afterAll Outer
```

## 📝 ตัวอย่าง 5.4: ตัวอย่างจริง - Library System

```javascript
describe('Library System', () => {
  let library;
  let testBooks;

  beforeAll(() => {
    // โหลด test data ครั้งเดียว
    testBooks = [
      { id: 1, title: 'JavaScript Basics', author: 'John Doe' },
      { id: 2, title: 'Python Guide', author: 'Jane Smith' },
      { id: 3, title: 'React Handbook', author: 'Bob Wilson' },
    ];
  });

  beforeEach(() => {
    // Reset library ก่อนแต่ละ test
    library = {
      books: [...testBooks],  // copy array
      borrowedBooks: []
    };
  });

  describe('Search', () => {
    test('ค้นหาด้วยชื่อหนังสือ', () => {
      const results = library.books.filter(b => 
        b.title.includes('JavaScript')
      );
      expect(results).toHaveLength(1);
      expect(results[0].title).toBe('JavaScript Basics');
    });

    test('ค้นหาด้วยชื่อผู้แต่ง', () => {
      const results = library.books.filter(b => 
        b.author.includes('Jane')
      );
      expect(results).toHaveLength(1);
    });
  });

  describe('Borrow', () => {
    test('ยืมหนังสือสำเร็จ', () => {
      const book = library.books.shift();  // นำหนังสือออก
      library.borrowedBooks.push(book);
      
      expect(library.books).toHaveLength(2);
      expect(library.borrowedBooks).toHaveLength(1);
    });

    test('หนังสือถูก reset ก่อน test นี้', () => {
      // beforeEach ทำให้ library มี 3 เล่มอีกครั้ง
      expect(library.books).toHaveLength(3);
      expect(library.borrowedBooks).toHaveLength(0);
    });
  });
});
```

## 💡 สรุป Setup/Teardown

| Hook | เมื่อไหร่รัน | ใช้ทำอะไร |
|------|-------------|-----------|
| `beforeAll` | ครั้งเดียว ก่อน test แรก | เชื่อมต่อ DB, โหลด data |
| `afterAll` | ครั้งเดียว หลัง test สุดท้าย | ปิด connection, cleanup |
| `beforeEach` | ก่อนทุก test | Reset state, สร้าง instance ใหม่ |
| `afterEach` | หลังทุก test | Clear data, reset mock |

---

# Level 6: ทดสอบ Function จริง
> ⭐⭐⭐ ความยาก: ปานกลาง-ยาก

## 🎯 เป้าหมาย
เขียน test สำหรับ function จริงๆ ที่แยกไฟล์

## 📝 ตัวอย่าง 6.1: Function พื้นฐาน

สร้างไฟล์ `src/math.js`:
```javascript
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

module.exports = { add, subtract, multiply, divide };
```

สร้างไฟล์ `tests/math.test.js`:
```javascript
const { add, subtract, multiply, divide } = require('../src/math');

describe('Math Functions', () => {
  
  describe('add()', () => {
    test('บวกเลขบวก', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('บวกเลขลบ', () => {
      expect(add(-2, -3)).toBe(-5);
    });

    test('บวกเลขบวกกับลบ', () => {
      expect(add(5, -3)).toBe(2);
    });

    test('บวกกับ 0', () => {
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('divide()', () => {
    test('หารเลข', () => {
      expect(divide(10, 2)).toBe(5);
    });

    test('หารด้วย 0 ต้อง throw error', () => {
      expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
    });
  });
});
```

## 📝 ตัวอย่าง 6.2: String Functions

สร้างไฟล์ `src/string-utils.js`:
```javascript
function capitalize(str) {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

function slugify(str) {
  return str
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, '')
    .replace(/[\s_-]+/g, '-')
    .replace(/^-+|-+$/g, '');
}

function truncate(str, maxLength) {
  if (str.length <= maxLength) return str;
  return str.slice(0, maxLength - 3) + '...';
}

function countWords(str) {
  return str.trim().split(/\s+/).filter(word => word.length > 0).length;
}

module.exports = { capitalize, slugify, truncate, countWords };
```

สร้างไฟล์ `tests/string-utils.test.js`:
```javascript
const { capitalize, slugify, truncate, countWords } = require('../src/string-utils');

describe('String Utilities', () => {
  
  describe('capitalize()', () => {
    test('ทำให้อักษรตัวแรกเป็นตัวใหญ่', () => {
      expect(capitalize('hello')).toBe('Hello');
    });

    test('จัดการกับ UPPERCASE', () => {
      expect(capitalize('HELLO')).toBe('Hello');
    });

    test('จัดการกับ string ว่าง', () => {
      expect(capitalize('')).toBe('');
    });

    test('จัดการกับ null/undefined', () => {
      expect(capitalize(null)).toBe('');
      expect(capitalize(undefined)).toBe('');
    });
  });

  describe('slugify()', () => {
    test('แปลง space เป็น dash', () => {
      expect(slugify('Hello World')).toBe('hello-world');
    });

    test('ลบอักขระพิเศษ', () => {
      expect(slugify('Hello, World!')).toBe('hello-world');
    });

    test('จัดการ multiple spaces', () => {
      expect(slugify('Hello    World')).toBe('hello-world');
    });
  });

  describe('truncate()', () => {
    test('ไม่ตัดถ้าสั้นกว่า maxLength', () => {
      expect(truncate('Hello', 10)).toBe('Hello');
    });

    test('ตัดและเพิ่ม ... ถ้ายาวเกิน', () => {
      expect(truncate('Hello World', 8)).toBe('Hello...');
    });
  });

  describe('countWords()', () => {
    test('นับคำ', () => {
      expect(countWords('Hello World')).toBe(2);
    });

    test('จัดการ multiple spaces', () => {
      expect(countWords('Hello    World   Test')).toBe(3);
    });

    test('string ว่าง', () => {
      expect(countWords('')).toBe(0);
    });
  });
});
```

## 📝 ตัวอย่าง 6.3: Array Functions

สร้างไฟล์ `src/array-utils.js`:
```javascript
function unique(arr) {
  return [...new Set(arr)];
}

function chunk(arr, size) {
  const result = [];
  for (let i = 0; i < arr.length; i += size) {
    result.push(arr.slice(i, i + size));
  }
  return result;
}

function flatten(arr) {
  return arr.flat(Infinity);
}

function groupBy(arr, key) {
  return arr.reduce((acc, item) => {
    const group = item[key];
    if (!acc[group]) acc[group] = [];
    acc[group].push(item);
    return acc;
  }, {});
}

module.exports = { unique, chunk, flatten, groupBy };
```

สร้างไฟล์ `tests/array-utils.test.js`:
```javascript
const { unique, chunk, flatten, groupBy } = require('../src/array-utils');

describe('Array Utilities', () => {
  
  describe('unique()', () => {
    test('ลบค่าซ้ำออก', () => {
      expect(unique([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);
    });

    test('จัดการ string', () => {
      expect(unique(['a', 'b', 'a'])).toEqual(['a', 'b']);
    });

    test('array ว่าง', () => {
      expect(unique([])).toEqual([]);
    });
  });

  describe('chunk()', () => {
    test('แบ่ง array เป็น chunks', () => {
      expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
    });

    test('chunk ใหญ่กว่า array', () => {
      expect(chunk([1, 2], 5)).toEqual([[1, 2]]);
    });
  });

  describe('flatten()', () => {
    test('flatten nested array', () => {
      expect(flatten([1, [2, [3, [4]]]])).toEqual([1, 2, 3, 4]);
    });
  });

  describe('groupBy()', () => {
    test('จัดกลุ่มตาม key', () => {
      const users = [
        { name: 'John', role: 'admin' },
        { name: 'Jane', role: 'user' },
        { name: 'Bob', role: 'admin' },
      ];
      
      expect(groupBy(users, 'role')).toEqual({
        admin: [
          { name: 'John', role: 'admin' },
          { name: 'Bob', role: 'admin' },
        ],
        user: [
          { name: 'Jane', role: 'user' },
        ],
      });
    });
  });
});
```

## 📝 ตัวอย่าง 6.4: test.each - ทดสอบหลาย input

```javascript
const { add } = require('../src/math');

describe('add() with test.each', () => {
  
  // แบบ Array
  test.each([
    [1, 2, 3],
    [5, 5, 10],
    [-1, 1, 0],
    [0, 0, 0],
  ])('add(%i, %i) = %i', (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
  });

  // แบบ Object (อ่านง่ายกว่า)
  test.each([
    { a: 1, b: 2, expected: 3 },
    { a: 5, b: 5, expected: 10 },
    { a: -1, b: 1, expected: 0 },
  ])('add($a, $b) = $expected', ({ a, b, expected }) => {
    expect(add(a, b)).toBe(expected);
  });
});
```

---

# Level 7: ทดสอบ Class และ Object
> ⭐⭐⭐ ความยาก: ปานกลาง-ยาก

## 📝 ตัวอย่าง 7.1: Class พื้นฐาน

สร้างไฟล์ `src/Book.js`:
```javascript
class Book {
  constructor(title, author, isbn) {
    if (!title || !author) {
      throw new Error('Title and author are required');
    }
    
    this.title = title;
    this.author = author;
    this.isbn = isbn;
    this.totalCopies = 1;
    this.borrowedCopies = 0;
  }

  get availableCopies() {
    return this.totalCopies - this.borrowedCopies;
  }

  get isAvailable() {
    return this.availableCopies > 0;
  }

  addCopies(count) {
    if (count <= 0) {
      throw new Error('Count must be positive');
    }
    this.totalCopies += count;
  }

  borrow() {
    if (!this.isAvailable) {
      throw new Error('No copies available');
    }
    this.borrowedCopies++;
    return true;
  }

  return() {
    if (this.borrowedCopies === 0) {
      throw new Error('No borrowed copies to return');
    }
    this.borrowedCopies--;
    return true;
  }
}

module.exports = Book;
```

สร้างไฟล์ `tests/Book.test.js`:
```javascript
const Book = require('../src/Book');

describe('Book', () => {
  
  describe('constructor', () => {
    test('สร้าง book ได้สำเร็จ', () => {
      const book = new Book('1984', 'George Orwell', '978-0451524935');
      
      expect(book.title).toBe('1984');
      expect(book.author).toBe('George Orwell');
      expect(book.isbn).toBe('978-0451524935');
      expect(book.totalCopies).toBe(1);
      expect(book.borrowedCopies).toBe(0);
    });

    test('throw error เมื่อไม่มี title', () => {
      expect(() => new Book('', 'Author')).toThrow('Title and author are required');
    });

    test('throw error เมื่อไม่มี author', () => {
      expect(() => new Book('Title', '')).toThrow('Title and author are required');
    });
  });

  describe('availableCopies', () => {
    let book;

    beforeEach(() => {
      book = new Book('Test', 'Author');
      book.totalCopies = 5;
    });

    test('คำนวณจำนวนที่เหลือถูกต้อง', () => {
      book.borrowedCopies = 2;
      expect(book.availableCopies).toBe(3);
    });

    test('เหลือ 0 เมื่อยืมหมด', () => {
      book.borrowedCopies = 5;
      expect(book.availableCopies).toBe(0);
    });
  });

  describe('isAvailable', () => {
    test('true เมื่อมีหนังสือเหลือ', () => {
      const book = new Book('Test', 'Author');
      expect(book.isAvailable).toBe(true);
    });

    test('false เมื่อหนังสือหมด', () => {
      const book = new Book('Test', 'Author');
      book.borrowedCopies = 1;
      expect(book.isAvailable).toBe(false);
    });
  });

  describe('addCopies()', () => {
    test('เพิ่มจำนวนหนังสือได้', () => {
      const book = new Book('Test', 'Author');
      book.addCopies(5);
      expect(book.totalCopies).toBe(6);
    });

    test('throw error เมื่อเพิ่มจำนวน <= 0', () => {
      const book = new Book('Test', 'Author');
      expect(() => book.addCopies(0)).toThrow('Count must be positive');
      expect(() => book.addCopies(-1)).toThrow('Count must be positive');
    });
  });

  describe('borrow()', () => {
    test('ยืมหนังสือสำเร็จ', () => {
      const book = new Book('Test', 'Author');
      book.totalCopies = 3;
      
      const result = book.borrow();
      
      expect(result).toBe(true);
      expect(book.borrowedCopies).toBe(1);
      expect(book.availableCopies).toBe(2);
    });

    test('throw error เมื่อหนังสือหมด', () => {
      const book = new Book('Test', 'Author');
      book.borrowedCopies = 1;  // ยืมไปหมดแล้ว
      
      expect(() => book.borrow()).toThrow('No copies available');
    });
  });

  describe('return()', () => {
    test('คืนหนังสือสำเร็จ', () => {
      const book = new Book('Test', 'Author');
      book.borrowedCopies = 1;
      
      const result = book.return();
      
      expect(result).toBe(true);
      expect(book.borrowedCopies).toBe(0);
    });

    test('throw error เมื่อไม่มีหนังสือที่ยืมไป', () => {
      const book = new Book('Test', 'Author');
      
      expect(() => book.return()).toThrow('No borrowed copies to return');
    });
  });
});
```

## 📝 ตัวอย่าง 7.2: Class ที่มี Dependency

สร้างไฟล์ `src/Library.js`:
```javascript
class Library {
  constructor(name) {
    this.name = name;
    this.books = [];
    this.members = [];
  }

  addBook(book) {
    this.books.push(book);
  }

  findBookByTitle(title) {
    return this.books.find(book => 
      book.title.toLowerCase().includes(title.toLowerCase())
    );
  }

  findBooksByAuthor(author) {
    return this.books.filter(book =>
      book.author.toLowerCase().includes(author.toLowerCase())
    );
  }

  getAvailableBooks() {
    return this.books.filter(book => book.isAvailable);
  }

  getTotalBooks() {
    return this.books.reduce((sum, book) => sum + book.totalCopies, 0);
  }

  getStatistics() {
    const totalBooks = this.getTotalBooks();
    const availableBooks = this.getAvailableBooks().length;
    const borrowedBooks = this.books.reduce((sum, book) => sum + book.borrowedCopies, 0);
    
    return {
      totalBooks,
      availableBooks,
      borrowedBooks,
      uniqueTitles: this.books.length
    };
  }
}

module.exports = Library;
```

สร้างไฟล์ `tests/Library.test.js`:
```javascript
const Library = require('../src/Library');
const Book = require('../src/Book');

describe('Library', () => {
  let library;

  beforeEach(() => {
    library = new Library('Central Library');
  });

  describe('constructor', () => {
    test('สร้าง library ได้', () => {
      expect(library.name).toBe('Central Library');
      expect(library.books).toEqual([]);
      expect(library.members).toEqual([]);
    });
  });

  describe('addBook()', () => {
    test('เพิ่มหนังสือได้', () => {
      const book = new Book('1984', 'George Orwell');
      library.addBook(book);
      
      expect(library.books).toHaveLength(1);
      expect(library.books[0].title).toBe('1984');
    });
  });

  describe('findBookByTitle()', () => {
    beforeEach(() => {
      library.addBook(new Book('JavaScript Basics', 'John Doe'));
      library.addBook(new Book('Python Guide', 'Jane Smith'));
      library.addBook(new Book('Advanced JavaScript', 'Bob Wilson'));
    });

    test('หาหนังสือเจอ', () => {
      const book = library.findBookByTitle('JavaScript');
      expect(book.title).toBe('JavaScript Basics');
    });

    test('ไม่เจอ return undefined', () => {
      const book = library.findBookByTitle('Ruby');
      expect(book).toBeUndefined();
    });

    test('case insensitive', () => {
      const book = library.findBookByTitle('PYTHON');
      expect(book.title).toBe('Python Guide');
    });
  });

  describe('findBooksByAuthor()', () => {
    beforeEach(() => {
      library.addBook(new Book('Book 1', 'John Doe'));
      library.addBook(new Book('Book 2', 'Jane Smith'));
      library.addBook(new Book('Book 3', 'John Doe'));
    });

    test('หาหนังสือตามผู้แต่ง', () => {
      const books = library.findBooksByAuthor('John');
      expect(books).toHaveLength(2);
    });

    test('ไม่เจอ return empty array', () => {
      const books = library.findBooksByAuthor('Unknown');
      expect(books).toEqual([]);
    });
  });

  describe('getAvailableBooks()', () => {
    test('แสดงเฉพาะหนังสือที่ว่าง', () => {
      const book1 = new Book('Book 1', 'Author');
      const book2 = new Book('Book 2', 'Author');
      
      library.addBook(book1);
      library.addBook(book2);
      
      book1.borrow();  // ยืมไป
      
      const available = library.getAvailableBooks();
      expect(available).toHaveLength(1);
      expect(available[0].title).toBe('Book 2');
    });
  });

  describe('getStatistics()', () => {
    test('แสดงสถิติถูกต้อง', () => {
      const book1 = new Book('Book 1', 'Author');
      book1.totalCopies = 5;
      book1.borrowedCopies = 2;
      
      const book2 = new Book('Book 2', 'Author');
      book2.totalCopies = 3;
      book2.borrowedCopies = 3;
      
      library.addBook(book1);
      library.addBook(book2);
      
      const stats = library.getStatistics();
      
      expect(stats).toEqual({
        totalBooks: 8,       // 5 + 3
        availableBooks: 1,    // book1 ยังว่าง, book2 หมด
        borrowedBooks: 5,     // 2 + 3
        uniqueTitles: 2
      });
    });
  });
});
```

---

# Level 8: ทดสอบ Error และ Exception
> ⭐⭐⭐ ความยาก: ปานกลาง-ยาก

## 📝 ตัวอย่าง 8.1: toThrow พื้นฐาน

```javascript
function validateAge(age) {
  if (typeof age !== 'number') {
    throw new TypeError('Age must be a number');
  }
  if (age < 0) {
    throw new RangeError('Age cannot be negative');
  }
  if (age > 150) {
    throw new RangeError('Age cannot exceed 150');
  }
  return true;
}

describe('validateAge()', () => {
  
  test('ยอมรับ age ที่ถูกต้อง', () => {
    expect(validateAge(25)).toBe(true);
    expect(validateAge(0)).toBe(true);
    expect(validateAge(150)).toBe(true);
  });

  // ⚠️ ต้องห่อด้วย arrow function!
  test('throw TypeError เมื่อไม่ใช่ตัวเลข', () => {
    expect(() => validateAge('25')).toThrow(TypeError);
    expect(() => validateAge(null)).toThrow(TypeError);
  });

  test('throw RangeError เมื่อ age ติดลบ', () => {
    expect(() => validateAge(-1)).toThrow(RangeError);
  });

  test('throw RangeError เมื่อ age เกิน 150', () => {
    expect(() => validateAge(200)).toThrow(RangeError);
  });

  // ตรวจสอบ error message
  test('throw error พร้อม message ที่ถูกต้อง', () => {
    expect(() => validateAge(-5)).toThrow('Age cannot be negative');
  });

  // ตรวจสอบด้วย regex
  test('throw error ที่ match regex', () => {
    expect(() => validateAge('abc')).toThrow(/must be a number/);
  });
});
```

## 📝 ตัวอย่าง 8.2: Custom Error Class

```javascript
// src/errors.js
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

class NotFoundError extends Error {
  constructor(resource, id) {
    super(`${resource} with id ${id} not found`);
    this.name = 'NotFoundError';
    this.resource = resource;
    this.id = id;
  }
}

module.exports = { ValidationError, NotFoundError };
```

```javascript
// src/userService.js
const { ValidationError, NotFoundError } = require('./errors');

const users = [
  { id: 1, name: 'John', email: 'john@test.com' },
  { id: 2, name: 'Jane', email: 'jane@test.com' },
];

function createUser(userData) {
  if (!userData.name) {
    throw new ValidationError('Name is required', 'name');
  }
  if (!userData.email) {
    throw new ValidationError('Email is required', 'email');
  }
  if (!/\S+@\S+\.\S+/.test(userData.email)) {
    throw new ValidationError('Invalid email format', 'email');
  }
  
  const newUser = { id: users.length + 1, ...userData };
  users.push(newUser);
  return newUser;
}

function getUserById(id) {
  const user = users.find(u => u.id === id);
  if (!user) {
    throw new NotFoundError('User', id);
  }
  return user;
}

module.exports = { createUser, getUserById };
```

```javascript
// tests/userService.test.js
const { createUser, getUserById } = require('../src/userService');
const { ValidationError, NotFoundError } = require('../src/errors');

describe('User Service', () => {

  describe('createUser()', () => {
    
    test('สร้าง user สำเร็จ', () => {
      const user = createUser({ name: 'Bob', email: 'bob@test.com' });
      expect(user).toHaveProperty('id');
      expect(user.name).toBe('Bob');
    });

    test('throw ValidationError เมื่อไม่มี name', () => {
      expect(() => createUser({ email: 'test@test.com' }))
        .toThrow(ValidationError);
    });

    test('throw ValidationError เมื่อไม่มี email', () => {
      expect(() => createUser({ name: 'Test' }))
        .toThrow(ValidationError);
    });

    test('ตรวจสอบ ValidationError properties', () => {
      try {
        createUser({ name: 'Test' });
        fail('Should have thrown');  // ถ้ามาถึงบรรทัดนี้ = test fail
      } catch (error) {
        expect(error).toBeInstanceOf(ValidationError);
        expect(error.field).toBe('email');
        expect(error.message).toBe('Email is required');
      }
    });

    test('throw ValidationError เมื่อ email format ไม่ถูก', () => {
      expect(() => createUser({ name: 'Test', email: 'invalid' }))
        .toThrow('Invalid email format');
    });
  });

  describe('getUserById()', () => {
    
    test('หา user เจอ', () => {
      const user = getUserById(1);
      expect(user.name).toBe('John');
    });

    test('throw NotFoundError เมื่อไม่เจอ', () => {
      expect(() => getUserById(999)).toThrow(NotFoundError);
    });

    test('ตรวจสอบ NotFoundError properties', () => {
      try {
        getUserById(999);
      } catch (error) {
        expect(error).toBeInstanceOf(NotFoundError);
        expect(error.resource).toBe('User');
        expect(error.id).toBe(999);
        expect(error.message).toBe('User with id 999 not found');
      }
    });
  });
});
```

## 📝 ตัวอย่าง 8.3: ทดสอบ Error ด้วย rejects (Async)

```javascript
// ฟังก์ชัน async ที่ throw error
async function fetchUser(id) {
  if (id <= 0) {
    throw new Error('Invalid user ID');
  }
  // จำลอง API call
  return { id, name: 'User ' + id };
}

describe('fetchUser()', () => {
  
  test('throw error เมื่อ id <= 0', async () => {
    await expect(fetchUser(0)).rejects.toThrow('Invalid user ID');
    await expect(fetchUser(-1)).rejects.toThrow(Error);
  });

  test('return user เมื่อ id ถูกต้อง', async () => {
    const user = await fetchUser(1);
    expect(user).toEqual({ id: 1, name: 'User 1' });
  });
});
```

---

# Level 9: Async/Await Testing
> ⭐⭐⭐⭐ ความยาก: ยาก

## 📝 ตัวอย่าง 9.1: Async Function พื้นฐาน

```javascript
// src/api.js

// จำลอง delay
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function fetchData(id) {
  await delay(100);  // จำลอง network delay
  
  if (id === 999) {
    throw new Error('Not found');
  }
  
  return { id, data: `Data for ${id}` };
}

async function fetchMultiple(ids) {
  const results = await Promise.all(ids.map(id => fetchData(id)));
  return results;
}

module.exports = { fetchData, fetchMultiple, delay };
```

```javascript
// tests/api.test.js
const { fetchData, fetchMultiple } = require('../src/api');

describe('API Functions', () => {
  
  describe('fetchData()', () => {
    
    // ✅ วิธีที่ 1: async/await
    test('fetch data สำเร็จ (async/await)', async () => {
      const result = await fetchData(1);
      
      expect(result).toEqual({ id: 1, data: 'Data for 1' });
    });

    // ✅ วิธีที่ 2: return Promise
    test('fetch data สำเร็จ (return Promise)', () => {
      return fetchData(2).then(result => {
        expect(result).toEqual({ id: 2, data: 'Data for 2' });
      });
    });

    // ✅ วิธีที่ 3: resolves
    test('fetch data สำเร็จ (resolves)', async () => {
      await expect(fetchData(3)).resolves.toEqual({
        id: 3,
        data: 'Data for 3'
      });
    });

    // ทดสอบ error
    test('throw error เมื่อ id = 999', async () => {
      await expect(fetchData(999)).rejects.toThrow('Not found');
    });
  });

  describe('fetchMultiple()', () => {
    
    test('fetch หลาย items พร้อมกัน', async () => {
      const results = await fetchMultiple([1, 2, 3]);
      
      expect(results).toHaveLength(3);
      expect(results[0]).toEqual({ id: 1, data: 'Data for 1' });
      expect(results[2]).toEqual({ id: 3, data: 'Data for 3' });
    });
  });
});
```

## 📝 ตัวอย่าง 9.2: ทดสอบ Callback (เก่า)

```javascript
// src/legacy.js
function fetchWithCallback(id, callback) {
  setTimeout(() => {
    if (id === 999) {
      callback(new Error('Not found'), null);
    } else {
      callback(null, { id, data: `Data for ${id}` });
    }
  }, 100);
}

module.exports = { fetchWithCallback };
```

```javascript
// tests/legacy.test.js
const { fetchWithCallback } = require('../src/legacy');

describe('Callback Functions', () => {
  
  // ใช้ done parameter
  test('fetch สำเร็จ (callback)', (done) => {
    fetchWithCallback(1, (error, result) => {
      expect(error).toBeNull();
      expect(result).toEqual({ id: 1, data: 'Data for 1' });
      done();  // ⚠️ ต้องเรียก done() เพื่อบอกว่า test จบ
    });
  });

  test('fetch error (callback)', (done) => {
    fetchWithCallback(999, (error, result) => {
      expect(error).toBeInstanceOf(Error);
      expect(error.message).toBe('Not found');
      expect(result).toBeNull();
      done();
    });
  });
});
```

## 📝 ตัวอย่าง 9.3: ทดสอบ Timer

```javascript
// src/timer.js
function debounce(fn, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delay);
  };
}

function scheduleTask(task, delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      task();
      resolve('done');
    }, delay);
  });
}

module.exports = { debounce, scheduleTask };
```

```javascript
// tests/timer.test.js
const { debounce, scheduleTask } = require('../src/timer');

// ใช้ fake timers
jest.useFakeTimers();

describe('Timer Functions', () => {
  
  afterEach(() => {
    jest.clearAllTimers();
  });

  describe('debounce()', () => {
    test('เรียก function หลังจาก delay', () => {
      const mockFn = jest.fn();
      const debouncedFn = debounce(mockFn, 1000);

      debouncedFn();
      expect(mockFn).not.toBeCalled();  // ยังไม่ถูกเรียก

      jest.advanceTimersByTime(500);
      expect(mockFn).not.toBeCalled();  // ยังไม่ครบ 1000ms

      jest.advanceTimersByTime(500);
      expect(mockFn).toBeCalledTimes(1);  // ครบ 1000ms แล้ว
    });

    test('reset timer เมื่อเรียกซ้ำ', () => {
      const mockFn = jest.fn();
      const debouncedFn = debounce(mockFn, 1000);

      debouncedFn();
      jest.advanceTimersByTime(500);
      
      debouncedFn();  // reset timer
      jest.advanceTimersByTime(500);
      expect(mockFn).not.toBeCalled();  // ยังไม่ครบ

      jest.advanceTimersByTime(500);
      expect(mockFn).toBeCalledTimes(1);
    });
  });

  describe('scheduleTask()', () => {
    test('รัน task หลังจาก delay', async () => {
      const mockTask = jest.fn();
      const promise = scheduleTask(mockTask, 2000);

      expect(mockTask).not.toBeCalled();

      jest.advanceTimersByTime(2000);
      
      await promise;  // รอ promise resolve
      expect(mockTask).toBeCalledTimes(1);
    });
  });
});
```

---

# Level 10: Mocking พื้นฐาน
> ⭐⭐⭐⭐ ความยาก: ยาก

## 🎯 ทำไมต้อง Mock?

- **แยก test** ไม่ให้ขึ้นกับ dependency
- **เร็วขึ้น** ไม่ต้องรอ network/database
- **ควบคุมได้** จำลอง error, edge case
- **ไม่เปลี่ยนแปลงข้อมูลจริง**

## 📝 ตัวอย่าง 10.1: jest.fn() พื้นฐาน

```javascript
describe('jest.fn() Basics', () => {
  
  test('สร้าง mock function', () => {
    const mockFn = jest.fn();
    
    mockFn();
    mockFn('hello');
    mockFn(1, 2, 3);

    // ตรวจว่าถูกเรียกกี่ครั้ง
    expect(mockFn).toHaveBeenCalled();
    expect(mockFn).toHaveBeenCalledTimes(3);
    
    // ตรวจ arguments
    expect(mockFn).toHaveBeenCalledWith('hello');
    expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
  });

  test('กำหนด return value', () => {
    const mockFn = jest.fn();
    
    // return ค่าเดียวตลอด
    mockFn.mockReturnValue(42);
    expect(mockFn()).toBe(42);
    expect(mockFn()).toBe(42);
    
    // return ค่าต่างกันทุกครั้ง
    mockFn
      .mockReturnValueOnce(1)
      .mockReturnValueOnce(2)
      .mockReturnValueOnce(3);
    
    expect(mockFn()).toBe(1);
    expect(mockFn()).toBe(2);
    expect(mockFn()).toBe(3);
    expect(mockFn()).toBe(42);  // กลับไปใช้ค่าเดิม
  });

  test('mock implementation', () => {
    const mockFn = jest.fn((a, b) => a + b);
    
    expect(mockFn(2, 3)).toBe(5);
    expect(mockFn(10, 20)).toBe(30);
  });
});
```

## 📝 ตัวอย่าง 10.2: Mock Callback

```javascript
// src/processor.js
function processItems(items, callback) {
  return items.map(item => callback(item));
}

function processAsync(items, callback) {
  return Promise.all(items.map(item => callback(item)));
}

module.exports = { processItems, processAsync };
```

```javascript
// tests/processor.test.js
const { processItems, processAsync } = require('../src/processor');

describe('Processor', () => {
  
  describe('processItems()', () => {
    test('เรียก callback สำหรับทุก item', () => {
      const mockCallback = jest.fn(x => x * 2);
      const items = [1, 2, 3];
      
      const result = processItems(items, mockCallback);
      
      expect(mockCallback).toHaveBeenCalledTimes(3);
      expect(mockCallback).toHaveBeenNthCalledWith(1, 1);
      expect(mockCallback).toHaveBeenNthCalledWith(2, 2);
      expect(mockCallback).toHaveBeenNthCalledWith(3, 3);
      expect(result).toEqual([2, 4, 6]);
    });
  });

  describe('processAsync()', () => {
    test('เรียก async callback', async () => {
      const mockCallback = jest.fn(async x => x * 2);
      const items = [1, 2, 3];
      
      const result = await processAsync(items, mockCallback);
      
      expect(mockCallback).toHaveBeenCalledTimes(3);
      expect(result).toEqual([2, 4, 6]);
    });
  });
});
```

## 📝 ตัวอย่าง 10.3: Mock Method ของ Object

```javascript
// src/userManager.js
class UserManager {
  constructor(database) {
    this.database = database;
  }

  async createUser(userData) {
    const user = { id: Date.now(), ...userData };
    await this.database.save('users', user);
    return user;
  }

  async getUser(id) {
    return await this.database.findById('users', id);
  }

  async deleteUser(id) {
    const user = await this.getUser(id);
    if (!user) {
      throw new Error('User not found');
    }
    await this.database.delete('users', id);
    return true;
  }
}

module.exports = UserManager;
```

```javascript
// tests/userManager.test.js
const UserManager = require('../src/userManager');

describe('UserManager', () => {
  let userManager;
  let mockDatabase;

  beforeEach(() => {
    // สร้าง mock database
    mockDatabase = {
      save: jest.fn(),
      findById: jest.fn(),
      delete: jest.fn(),
    };
    
    userManager = new UserManager(mockDatabase);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('createUser()', () => {
    test('บันทึก user ลง database', async () => {
      const userData = { name: 'John', email: 'john@test.com' };
      
      const result = await userManager.createUser(userData);
      
      expect(mockDatabase.save).toHaveBeenCalledTimes(1);
      expect(mockDatabase.save).toHaveBeenCalledWith('users', expect.objectContaining({
        name: 'John',
        email: 'john@test.com'
      }));
      expect(result).toHaveProperty('id');
      expect(result.name).toBe('John');
    });
  });

  describe('getUser()', () => {
    test('ดึงข้อมูล user จาก database', async () => {
      const mockUser = { id: 1, name: 'John' };
      mockDatabase.findById.mockResolvedValue(mockUser);
      
      const result = await userManager.getUser(1);
      
      expect(mockDatabase.findById).toHaveBeenCalledWith('users', 1);
      expect(result).toEqual(mockUser);
    });

    test('return null เมื่อไม่เจอ', async () => {
      mockDatabase.findById.mockResolvedValue(null);
      
      const result = await userManager.getUser(999);
      
      expect(result).toBeNull();
    });
  });

  describe('deleteUser()', () => {
    test('ลบ user สำเร็จ', async () => {
      mockDatabase.findById.mockResolvedValue({ id: 1, name: 'John' });
      mockDatabase.delete.mockResolvedValue(true);
      
      const result = await userManager.deleteUser(1);
      
      expect(mockDatabase.delete).toHaveBeenCalledWith('users', 1);
      expect(result).toBe(true);
    });

    test('throw error เมื่อไม่เจอ user', async () => {
      mockDatabase.findById.mockResolvedValue(null);
      
      await expect(userManager.deleteUser(999)).rejects.toThrow('User not found');
      expect(mockDatabase.delete).not.toHaveBeenCalled();
    });
  });
});
```

## 💡 สรุป Mock Matchers

| Matcher | ใช้ตรวจอะไร |
|---------|-------------|
| `toHaveBeenCalled()` | ถูกเรียกอย่างน้อย 1 ครั้ง |
| `toHaveBeenCalledTimes(n)` | ถูกเรียก n ครั้ง |
| `toHaveBeenCalledWith(args)` | ถูกเรียกด้วย arguments นี้ |
| `toHaveBeenNthCalledWith(n, args)` | ครั้งที่ n ถูกเรียกด้วย args นี้ |
| `toHaveBeenLastCalledWith(args)` | ครั้งสุดท้ายถูกเรียกด้วย args นี้ |
| `toHaveReturned()` | มีการ return ค่า |
| `toHaveReturnedWith(value)` | return ค่านี้ |

---

# Level 11: Mocking ขั้นสูง
> ⭐⭐⭐⭐⭐ ความยาก: ยากมาก

## 📝 ตัวอย่าง 11.1: jest.mock() - Mock ทั้ง Module

```javascript
// src/emailService.js
const nodemailer = require('nodemailer');

async function sendEmail(to, subject, body) {
  const transporter = nodemailer.createTransport({
    host: 'smtp.gmail.com',
    port: 587,
  });
  
  const result = await transporter.sendMail({
    from: 'noreply@library.com',
    to,
    subject,
    text: body,
  });
  
  return result.messageId;
}

module.exports = { sendEmail };
```

```javascript
// tests/emailService.test.js

// Mock nodemailer ก่อน import
jest.mock('nodemailer');

const nodemailer = require('nodemailer');
const { sendEmail } = require('../src/emailService');

describe('Email Service', () => {
  let mockSendMail;

  beforeEach(() => {
    mockSendMail = jest.fn().mockResolvedValue({
      messageId: 'test-message-id-123'
    });
    
    nodemailer.createTransport.mockReturnValue({
      sendMail: mockSendMail
    });
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  test('ส่ง email สำเร็จ', async () => {
    const result = await sendEmail(
      'user@test.com',
      'Test Subject',
      'Test Body'
    );

    expect(nodemailer.createTransport).toHaveBeenCalled();
    expect(mockSendMail).toHaveBeenCalledWith({
      from: 'noreply@library.com',
      to: 'user@test.com',
      subject: 'Test Subject',
      text: 'Test Body',
    });
    expect(result).toBe('test-message-id-123');
  });

  test('จัดการ error', async () => {
    mockSendMail.mockRejectedValue(new Error('SMTP Error'));
    
    await expect(sendEmail('test@test.com', 'Hi', 'Body'))
      .rejects.toThrow('SMTP Error');
  });
});
```

## 📝 ตัวอย่าง 11.2: jest.spyOn() - สอดแนม Method

```javascript
// src/calculator.js
const calculator = {
  add(a, b) {
    return a + b;
  },
  multiply(a, b) {
    return a * b;
  },
  calculate(a, b, operation) {
    if (operation === 'add') {
      return this.add(a, b);
    } else if (operation === 'multiply') {
      return this.multiply(a, b);
    }
    throw new Error('Unknown operation');
  }
};

module.exports = calculator;
```

```javascript
// tests/calculator.test.js
const calculator = require('../src/calculator');

describe('Calculator with spyOn', () => {
  
  afterEach(() => {
    jest.restoreAllMocks();  // คืนค่า original
  });

  test('spyOn - ตรวจว่า method ถูกเรียก', () => {
    const addSpy = jest.spyOn(calculator, 'add');
    
    const result = calculator.calculate(2, 3, 'add');
    
    expect(addSpy).toHaveBeenCalledWith(2, 3);
    expect(result).toBe(5);  // ยังทำงานจริง
  });

  test('spyOn + mockImplementation - เปลี่ยนพฤติกรรม', () => {
    const addSpy = jest.spyOn(calculator, 'add')
      .mockImplementation((a, b) => a * 10 + b);
    
    const result = calculator.calculate(2, 3, 'add');
    
    expect(result).toBe(23);  // 2*10 + 3
  });

  test('spyOn + mockReturnValue', () => {
    jest.spyOn(calculator, 'add').mockReturnValue(999);
    
    expect(calculator.add(1, 2)).toBe(999);
  });
});
```

## 📝 ตัวอย่าง 11.3: Mock File System

```javascript
// src/fileManager.js
const fs = require('fs').promises;
const path = require('path');

async function readConfig(filename) {
  const filepath = path.join(__dirname, 'config', filename);
  const content = await fs.readFile(filepath, 'utf-8');
  return JSON.parse(content);
}

async function writeConfig(filename, data) {
  const filepath = path.join(__dirname, 'config', filename);
  await fs.writeFile(filepath, JSON.stringify(data, null, 2));
  return true;
}

async function listConfigs() {
  const configDir = path.join(__dirname, 'config');
  const files = await fs.readdir(configDir);
  return files.filter(f => f.endsWith('.json'));
}

module.exports = { readConfig, writeConfig, listConfigs };
```

```javascript
// tests/fileManager.test.js
const fs = require('fs').promises;
const { readConfig, writeConfig, listConfigs } = require('../src/fileManager');

jest.mock('fs', () => ({
  promises: {
    readFile: jest.fn(),
    writeFile: jest.fn(),
    readdir: jest.fn(),
  }
}));

describe('File Manager', () => {
  
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('readConfig()', () => {
    test('อ่าน config file', async () => {
      const mockConfig = { database: 'localhost', port: 3000 };
      fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
      
      const result = await readConfig('app.json');
      
      expect(fs.readFile).toHaveBeenCalled();
      expect(result).toEqual(mockConfig);
    });

    test('throw error เมื่อไฟล์ไม่มี', async () => {
      fs.readFile.mockRejectedValue(new Error('ENOENT: no such file'));
      
      await expect(readConfig('missing.json'))
        .rejects.toThrow('ENOENT');
    });
  });

  describe('writeConfig()', () => {
    test('เขียน config file', async () => {
      fs.writeFile.mockResolvedValue();
      
      const result = await writeConfig('app.json', { port: 4000 });
      
      expect(fs.writeFile).toHaveBeenCalled();
      expect(result).toBe(true);
    });
  });

  describe('listConfigs()', () => {
    test('แสดงรายการ config files', async () => {
      fs.readdir.mockResolvedValue([
        'app.json',
        'db.json',
        'readme.txt',
        'test.json'
      ]);
      
      const result = await listConfigs();
      
      expect(result).toEqual(['app.json', 'db.json', 'test.json']);
    });
  });
});
```

## 📝 ตัวอย่าง 11.4: Mock Axios / HTTP Requests

```javascript
// src/apiClient.js
const axios = require('axios');

const API_BASE = 'https://api.example.com';

async function getUsers() {
  const response = await axios.get(`${API_BASE}/users`);
  return response.data;
}

async function createUser(userData) {
  const response = await axios.post(`${API_BASE}/users`, userData);
  return response.data;
}

async function updateUser(id, userData) {
  const response = await axios.put(`${API_BASE}/users/${id}`, userData);
  return response.data;
}

module.exports = { getUsers, createUser, updateUser };
```

```javascript
// tests/apiClient.test.js
const axios = require('axios');
const { getUsers, createUser, updateUser } = require('../src/apiClient');

jest.mock('axios');

describe('API Client', () => {
  
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('getUsers()', () => {
    test('ดึงข้อมูล users สำเร็จ', async () => {
      const mockUsers = [
        { id: 1, name: 'John' },
        { id: 2, name: 'Jane' },
      ];
      axios.get.mockResolvedValue({ data: mockUsers });
      
      const result = await getUsers();
      
      expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users');
      expect(result).toEqual(mockUsers);
    });

    test('จัดการ error', async () => {
      axios.get.mockRejectedValue(new Error('Network Error'));
      
      await expect(getUsers()).rejects.toThrow('Network Error');
    });
  });

  describe('createUser()', () => {
    test('สร้าง user สำเร็จ', async () => {
      const userData = { name: 'Bob', email: 'bob@test.com' };
      const createdUser = { id: 3, ...userData };
      axios.post.mockResolvedValue({ data: createdUser });
      
      const result = await createUser(userData);
      
      expect(axios.post).toHaveBeenCalledWith(
        'https://api.example.com/users',
        userData
      );
      expect(result).toEqual(createdUser);
    });
  });

  describe('updateUser()', () => {
    test('update user สำเร็จ', async () => {
      const userData = { name: 'John Updated' };
      const updatedUser = { id: 1, ...userData };
      axios.put.mockResolvedValue({ data: updatedUser });
      
      const result = await updateUser(1, userData);
      
      expect(axios.put).toHaveBeenCalledWith(
        'https://api.example.com/users/1',
        userData
      );
      expect(result).toEqual(updatedUser);
    });
  });
});
```

---

# Level 12: Test Driven Development
> ⭐⭐⭐⭐⭐ ความยาก: ยากมาก

## 🔴🟢🔵 TDD Cycle: Red → Green → Refactor

1. **🔴 Red**: เขียน test ก่อน (จะ fail)
2. **🟢 Green**: เขียน code ให้ test ผ่าน (minimal)
3. **🔵 Refactor**: ปรับปรุง code (test ยังผ่าน)

## 📝 ตัวอย่าง: สร้าง Password Validator ด้วย TDD

### Step 1: 🔴 Red - เขียน Test แรก

```javascript
// tests/passwordValidator.test.js

const { validatePassword } = require('../src/passwordValidator');

describe('Password Validator', () => {
  
  test('ยอมรับ password ที่ถูกต้อง', () => {
    expect(validatePassword('Abcd1234!')).toBe(true);
  });
});
```

**รัน test → FAIL** (ยังไม่มี function)

### Step 2: 🟢 Green - เขียน Code ให้ผ่าน

```javascript
// src/passwordValidator.js

function validatePassword(password) {
  return true;  // แค่ให้ผ่านก่อน
}

module.exports = { validatePassword };
```

**รัน test → PASS**

### Step 3: 🔴 Red - เพิ่ม Test: ต้องมีอย่างน้อย 8 ตัว

```javascript
test('reject password สั้นกว่า 8 ตัว', () => {
  expect(validatePassword('Ab1!')).toBe(false);
});
```

**รัน test → FAIL**

### Step 4: 🟢 Green - แก้ Code

```javascript
function validatePassword(password) {
  if (password.length < 8) return false;
  return true;
}
```

**รัน test → PASS**

### Step 5: 🔴 Red - เพิ่ม Test: ต้องมีตัวพิมพ์ใหญ่

```javascript
test('reject password ไม่มีตัวพิมพ์ใหญ่', () => {
  expect(validatePassword('abcd1234!')).toBe(false);
});
```

### Step 6: 🟢 Green - แก้ Code

```javascript
function validatePassword(password) {
  if (password.length < 8) return false;
  if (!/[A-Z]/.test(password)) return false;
  return true;
}
```

### ... ทำซ้ำ จนครบทุก requirement ...

### Final Code

```javascript
// src/passwordValidator.js

function validatePassword(password) {
  const errors = [];
  
  if (password.length < 8) {
    errors.push('ต้องมีอย่างน้อย 8 ตัวอักษร');
  }
  
  if (!/[A-Z]/.test(password)) {
    errors.push('ต้องมีตัวพิมพ์ใหญ่อย่างน้อย 1 ตัว');
  }
  
  if (!/[a-z]/.test(password)) {
    errors.push('ต้องมีตัวพิมพ์เล็กอย่างน้อย 1 ตัว');
  }
  
  if (!/[0-9]/.test(password)) {
    errors.push('ต้องมีตัวเลขอย่างน้อย 1 ตัว');
  }
  
  if (!/[!@#$%^&*]/.test(password)) {
    errors.push('ต้องมีอักขระพิเศษอย่างน้อย 1 ตัว');
  }
  
  return errors.length === 0;
}

function getPasswordErrors(password) {
  const errors = [];
  
  if (password.length < 8) {
    errors.push('ต้องมีอย่างน้อย 8 ตัวอักษร');
  }
  if (!/[A-Z]/.test(password)) {
    errors.push('ต้องมีตัวพิมพ์ใหญ่อย่างน้อย 1 ตัว');
  }
  if (!/[a-z]/.test(password)) {
    errors.push('ต้องมีตัวพิมพ์เล็กอย่างน้อย 1 ตัว');
  }
  if (!/[0-9]/.test(password)) {
    errors.push('ต้องมีตัวเลขอย่างน้อย 1 ตัว');
  }
  if (!/[!@#$%^&*]/.test(password)) {
    errors.push('ต้องมีอักขระพิเศษอย่างน้อย 1 ตัว');
  }
  
  return errors;
}

module.exports = { validatePassword, getPasswordErrors };
```

### Final Tests

```javascript
// tests/passwordValidator.test.js

const { validatePassword, getPasswordErrors } = require('../src/passwordValidator');

describe('Password Validator', () => {
  
  describe('validatePassword()', () => {
    
    describe('Valid passwords', () => {
      test.each([
        'Abcd1234!',
        'MyP@ssw0rd',
        'Str0ng!Pass',
        'Test123!@#',
      ])('"%s" should be valid', (password) => {
        expect(validatePassword(password)).toBe(true);
      });
    });

    describe('Invalid passwords', () => {
      test('reject สั้นกว่า 8 ตัว', () => {
        expect(validatePassword('Ab1!')).toBe(false);
      });

      test('reject ไม่มีตัวพิมพ์ใหญ่', () => {
        expect(validatePassword('abcd1234!')).toBe(false);
      });

      test('reject ไม่มีตัวพิมพ์เล็ก', () => {
        expect(validatePassword('ABCD1234!')).toBe(false);
      });

      test('reject ไม่มีตัวเลข', () => {
        expect(validatePassword('Abcdefgh!')).toBe(false);
      });

      test('reject ไม่มีอักขระพิเศษ', () => {
        expect(validatePassword('Abcd12345')).toBe(false);
      });
    });
  });

  describe('getPasswordErrors()', () => {
    
    test('return empty array สำหรับ valid password', () => {
      expect(getPasswordErrors('Abcd1234!')).toEqual([]);
    });

    test('return all errors สำหรับ empty string', () => {
      const errors = getPasswordErrors('');
      expect(errors).toHaveLength(5);
    });

    test('return specific error', () => {
      const errors = getPasswordErrors('abcd1234!');
      expect(errors).toContain('ต้องมีตัวพิมพ์ใหญ่อย่างน้อย 1 ตัว');
      expect(errors).toHaveLength(1);
    });
  });
});
```

---

# Level 13: โปรเจกต์จริง: Library System
> ⭐⭐⭐⭐⭐ ความยาก: ยากมาก

## 📁 โครงสร้างโปรเจกต์

```
library-system/
├── src/
│   ├── models/
│   │   ├── Book.js
│   │   ├── Member.js
│   │   └── Loan.js
│   ├── services/
│   │   ├── BookService.js
│   │   ├── MemberService.js
│   │   └── LoanService.js
│   ├── repositories/
│   │   └── Database.js
│   └── utils/
│       └── validators.js
├── tests/
│   ├── models/
│   │   ├── Book.test.js
│   │   ├── Member.test.js
│   │   └── Loan.test.js
│   ├── services/
│   │   ├── BookService.test.js
│   │   ├── MemberService.test.js
│   │   └── LoanService.test.js
│   └── utils/
│       └── validators.test.js
├── package.json
└── jest.config.js
```

## 📝 ตัวอย่าง: Book Model + Tests

```javascript
// src/models/Book.js

class Book {
  constructor({ isbn, title, author, publishYear, copies = 1 }) {
    this.validateInput({ isbn, title, author, publishYear });
    
    this.isbn = isbn;
    this.title = title;
    this.author = author;
    this.publishYear = publishYear;
    this.totalCopies = copies;
    this.borrowedCopies = 0;
    this.createdAt = new Date();
  }

  validateInput({ isbn, title, author, publishYear }) {
    if (!isbn || !/^[\d-]+$/.test(isbn)) {
      throw new Error('Invalid ISBN format');
    }
    if (!title || title.trim().length === 0) {
      throw new Error('Title is required');
    }
    if (!author || author.trim().length === 0) {
      throw new Error('Author is required');
    }
    if (publishYear && (publishYear < 1000 || publishYear > new Date().getFullYear())) {
      throw new Error('Invalid publish year');
    }
  }

  get availableCopies() {
    return this.totalCopies - this.borrowedCopies;
  }

  get isAvailable() {
    return this.availableCopies > 0;
  }

  borrow() {
    if (!this.isAvailable) {
      throw new Error('No copies available for borrowing');
    }
    this.borrowedCopies++;
    return {
      isbn: this.isbn,
      borrowedAt: new Date(),
      dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14 days
    };
  }

  return() {
    if (this.borrowedCopies === 0) {
      throw new Error('No borrowed copies to return');
    }
    this.borrowedCopies--;
    return true;
  }

  addCopies(count) {
    if (count <= 0) {
      throw new Error('Count must be a positive number');
    }
    this.totalCopies += count;
  }

  toJSON() {
    return {
      isbn: this.isbn,
      title: this.title,
      author: this.author,
      publishYear: this.publishYear,
      totalCopies: this.totalCopies,
      availableCopies: this.availableCopies,
      isAvailable: this.isAvailable,
    };
  }
}

module.exports = Book;
```

```javascript
// tests/models/Book.test.js

const Book = require('../../src/models/Book');

describe('Book Model', () => {
  
  // Helper function
  const createValidBook = (overrides = {}) => {
    return new Book({
      isbn: '978-1234567890',
      title: 'Test Book',
      author: 'Test Author',
      publishYear: 2024,
      ...overrides,
    });
  };

  describe('constructor', () => {
    
    test('สร้าง book ด้วยข้อมูลที่ถูกต้อง', () => {
      const book = createValidBook();
      
      expect(book.isbn).toBe('978-1234567890');
      expect(book.title).toBe('Test Book');
      expect(book.author).toBe('Test Author');
      expect(book.publishYear).toBe(2024);
      expect(book.totalCopies).toBe(1);
      expect(book.borrowedCopies).toBe(0);
      expect(book.createdAt).toBeInstanceOf(Date);
    });

    test('กำหนดจำนวน copies ได้', () => {
      const book = createValidBook({ copies: 5 });
      expect(book.totalCopies).toBe(5);
    });

    describe('validation errors', () => {
      
      test.each([
        [{ isbn: '' }, 'Invalid ISBN format'],
        [{ isbn: 'abc' }, 'Invalid ISBN format'],
        [{ title: '' }, 'Title is required'],
        [{ title: '   ' }, 'Title is required'],
        [{ author: '' }, 'Author is required'],
        [{ publishYear: 500 }, 'Invalid publish year'],
        [{ publishYear: 3000 }, 'Invalid publish year'],
      ])('throw error: %o → %s', (override, expectedError) => {
        expect(() => createValidBook(override)).toThrow(expectedError);
      });
    });
  });

  describe('availableCopies', () => {
    
    test('คำนวณถูกต้อง', () => {
      const book = createValidBook({ copies: 5 });
      book.borrowedCopies = 2;
      
      expect(book.availableCopies).toBe(3);
    });

    test('เป็น 0 เมื่อยืมหมด', () => {
      const book = createValidBook({ copies: 2 });
      book.borrowedCopies = 2;
      
      expect(book.availableCopies).toBe(0);
    });
  });

  describe('isAvailable', () => {
    
    test('true เมื่อมีหนังสือเหลือ', () => {
      const book = createValidBook();
      expect(book.isAvailable).toBe(true);
    });

    test('false เมื่อหนังสือหมด', () => {
      const book = createValidBook();
      book.borrowedCopies = 1;
      expect(book.isAvailable).toBe(false);
    });
  });

  describe('borrow()', () => {
    
    test('ยืมสำเร็จ - เพิ่ม borrowedCopies', () => {
      const book = createValidBook({ copies: 3 });
      
      const result = book.borrow();
      
      expect(book.borrowedCopies).toBe(1);
      expect(book.availableCopies).toBe(2);
      expect(result).toHaveProperty('isbn', '978-1234567890');
      expect(result).toHaveProperty('borrowedAt');
      expect(result).toHaveProperty('dueDate');
    });

    test('dueDate เป็น 14 วันหลังยืม', () => {
      const book = createValidBook();
      const beforeBorrow = Date.now();
      
      const result = book.borrow();
      
      const expectedDue = beforeBorrow + 14 * 24 * 60 * 60 * 1000;
      expect(result.dueDate.getTime()).toBeCloseTo(expectedDue, -3); // +-1 second
    });

    test('throw error เมื่อหนังสือหมด', () => {
      const book = createValidBook();
      book.borrow();  // ยืมไป 1 เล่ม (หมดแล้ว)
      
      expect(() => book.borrow()).toThrow('No copies available for borrowing');
    });

    test('ยืมได้จนหมด', () => {
      const book = createValidBook({ copies: 3 });
      
      book.borrow();
      book.borrow();
      book.borrow();
      
      expect(book.isAvailable).toBe(false);
      expect(() => book.borrow()).toThrow();
    });
  });

  describe('return()', () => {
    
    test('คืนสำเร็จ - ลด borrowedCopies', () => {
      const book = createValidBook({ copies: 2 });
      book.borrow();
      book.borrow();
      
      const result = book.return();
      
      expect(result).toBe(true);
      expect(book.borrowedCopies).toBe(1);
      expect(book.availableCopies).toBe(1);
    });

    test('throw error เมื่อไม่มีหนังสือที่ยืมไป', () => {
      const book = createValidBook();
      
      expect(() => book.return()).toThrow('No borrowed copies to return');
    });
  });

  describe('addCopies()', () => {
    
    test('เพิ่มจำนวนสำเร็จ', () => {
      const book = createValidBook({ copies: 2 });
      
      book.addCopies(3);
      
      expect(book.totalCopies).toBe(5);
    });

    test.each([0, -1, -10])('throw error เมื่อ count = %i', (count) => {
      const book = createValidBook();
      expect(() => book.addCopies(count)).toThrow('Count must be a positive number');
    });
  });

  describe('toJSON()', () => {
    
    test('return object ที่ถูกต้อง', () => {
      const book = createValidBook({ copies: 5 });
      book.borrowedCopies = 2;
      
      const json = book.toJSON();
      
      expect(json).toEqual({
        isbn: '978-1234567890',
        title: 'Test Book',
        author: 'Test Author',
        publishYear: 2024,
        totalCopies: 5,
        availableCopies: 3,
        isAvailable: true,
      });
    });

    test('ไม่รวม internal properties', () => {
      const book = createValidBook();
      const json = book.toJSON();
      
      expect(json).not.toHaveProperty('borrowedCopies');
      expect(json).not.toHaveProperty('createdAt');
    });
  });
});
```

## 📝 ตัวอย่าง: BookService + Mock Database

```javascript
// src/services/BookService.js

class BookService {
  constructor(database) {
    this.db = database;
  }

  async getAllBooks() {
    return await this.db.findAll('books');
  }

  async getBookByIsbn(isbn) {
    const book = await this.db.findOne('books', { isbn });
    if (!book) {
      throw new Error(`Book with ISBN ${isbn} not found`);
    }
    return book;
  }

  async searchBooks(query) {
    const allBooks = await this.db.findAll('books');
    const lowerQuery = query.toLowerCase();
    
    return allBooks.filter(book =>
      book.title.toLowerCase().includes(lowerQuery) ||
      book.author.toLowerCase().includes(lowerQuery)
    );
  }

  async addBook(bookData) {
    // Check duplicate
    const existing = await this.db.findOne('books', { isbn: bookData.isbn });
    if (existing) {
      throw new Error(`Book with ISBN ${bookData.isbn} already exists`);
    }
    
    return await this.db.insert('books', bookData);
  }

  async updateBook(isbn, updateData) {
    const book = await this.getBookByIsbn(isbn);
    const updated = { ...book, ...updateData };
    return await this.db.update('books', { isbn }, updated);
  }

  async deleteBook(isbn) {
    await this.getBookByIsbn(isbn);  // Check exists
    return await this.db.delete('books', { isbn });
  }

  async getAvailableBooks() {
    const allBooks = await this.db.findAll('books');
    return allBooks.filter(book => book.availableCopies > 0);
  }

  async borrowBook(isbn, memberId) {
    const book = await this.getBookByIsbn(isbn);
    
    if (book.availableCopies === 0) {
      throw new Error('No copies available');
    }
    
    // Update book
    const updatedBook = {
      ...book,
      borrowedCopies: book.borrowedCopies + 1,
    };
    await this.db.update('books', { isbn }, updatedBook);
    
    // Create loan record
    const loan = {
      isbn,
      memberId,
      borrowedAt: new Date(),
      dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000),
      status: 'active',
    };
    await this.db.insert('loans', loan);
    
    return loan;
  }
}

module.exports = BookService;
```

```javascript
// tests/services/BookService.test.js

const BookService = require('../../src/services/BookService');

describe('BookService', () => {
  let bookService;
  let mockDb;

  // Test data
  const testBooks = [
    {
      isbn: '978-1234567890',
      title: 'JavaScript Basics',
      author: 'John Doe',
      totalCopies: 5,
      borrowedCopies: 2,
      availableCopies: 3,
    },
    {
      isbn: '978-0987654321',
      title: 'Python Guide',
      author: 'Jane Smith',
      totalCopies: 3,
      borrowedCopies: 3,
      availableCopies: 0,
    },
    {
      isbn: '978-1111111111',
      title: 'Advanced JavaScript',
      author: 'Bob Wilson',
      totalCopies: 2,
      borrowedCopies: 0,
      availableCopies: 2,
    },
  ];

  beforeEach(() => {
    // Create mock database
    mockDb = {
      findAll: jest.fn(),
      findOne: jest.fn(),
      insert: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
    };
    
    bookService = new BookService(mockDb);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('getAllBooks()', () => {
    
    test('ดึงหนังสือทั้งหมด', async () => {
      mockDb.findAll.mockResolvedValue(testBooks);
      
      const result = await bookService.getAllBooks();
      
      expect(mockDb.findAll).toHaveBeenCalledWith('books');
      expect(result).toEqual(testBooks);
      expect(result).toHaveLength(3);
    });

    test('return empty array เมื่อไม่มีหนังสือ', async () => {
      mockDb.findAll.mockResolvedValue([]);
      
      const result = await bookService.getAllBooks();
      
      expect(result).toEqual([]);
    });
  });

  describe('getBookByIsbn()', () => {
    
    test('หาหนังสือเจอ', async () => {
      mockDb.findOne.mockResolvedValue(testBooks[0]);
      
      const result = await bookService.getBookByIsbn('978-1234567890');
      
      expect(mockDb.findOne).toHaveBeenCalledWith('books', { isbn: '978-1234567890' });
      expect(result.title).toBe('JavaScript Basics');
    });

    test('throw error เมื่อไม่เจอ', async () => {
      mockDb.findOne.mockResolvedValue(null);
      
      await expect(bookService.getBookByIsbn('999-9999999999'))
        .rejects.toThrow('Book with ISBN 999-9999999999 not found');
    });
  });

  describe('searchBooks()', () => {
    
    beforeEach(() => {
      mockDb.findAll.mockResolvedValue(testBooks);
    });

    test('ค้นหาด้วย title', async () => {
      const result = await bookService.searchBooks('JavaScript');
      
      expect(result).toHaveLength(2);
      expect(result[0].title).toBe('JavaScript Basics');
      expect(result[1].title).toBe('Advanced JavaScript');
    });

    test('ค้นหาด้วย author', async () => {
      const result = await bookService.searchBooks('Jane');
      
      expect(result).toHaveLength(1);
      expect(result[0].author).toBe('Jane Smith');
    });

    test('case insensitive', async () => {
      const result = await bookService.searchBooks('JAVASCRIPT');
      
      expect(result).toHaveLength(2);
    });

    test('return empty เมื่อไม่เจอ', async () => {
      const result = await bookService.searchBooks('Ruby');
      
      expect(result).toEqual([]);
    });
  });

  describe('addBook()', () => {
    
    test('เพิ่มหนังสือสำเร็จ', async () => {
      const newBook = {
        isbn: '978-2222222222',
        title: 'New Book',
        author: 'New Author',
      };
      mockDb.findOne.mockResolvedValue(null);  // ไม่มี duplicate
      mockDb.insert.mockResolvedValue({ id: 1, ...newBook });
      
      const result = await bookService.addBook(newBook);
      
      expect(mockDb.findOne).toHaveBeenCalledWith('books', { isbn: '978-2222222222' });
      expect(mockDb.insert).toHaveBeenCalledWith('books', newBook);
      expect(result).toHaveProperty('id', 1);
    });

    test('throw error เมื่อ ISBN ซ้ำ', async () => {
      mockDb.findOne.mockResolvedValue(testBooks[0]);  // มี duplicate
      
      await expect(bookService.addBook({ isbn: '978-1234567890', title: 'Test' }))
        .rejects.toThrow('Book with ISBN 978-1234567890 already exists');
      
      expect(mockDb.insert).not.toHaveBeenCalled();
    });
  });

  describe('getAvailableBooks()', () => {
    
    test('แสดงเฉพาะหนังสือที่ว่าง', async () => {
      mockDb.findAll.mockResolvedValue(testBooks);
      
      const result = await bookService.getAvailableBooks();
      
      expect(result).toHaveLength(2);
      expect(result.every(b => b.availableCopies > 0)).toBe(true);
    });
  });

  describe('borrowBook()', () => {
    
    test('ยืมหนังสือสำเร็จ', async () => {
      const book = { ...testBooks[0] };  // มี availableCopies: 3
      mockDb.findOne.mockResolvedValue(book);
      mockDb.update.mockResolvedValue(true);
      mockDb.insert.mockResolvedValue({ id: 1 });
      
      const result = await bookService.borrowBook('978-1234567890', 'member-1');
      
      // ตรวจว่า update book
      expect(mockDb.update).toHaveBeenCalledWith(
        'books',
        { isbn: '978-1234567890' },
        expect.objectContaining({ borrowedCopies: 3 })  // เดิม 2, เพิ่มเป็น 3
      );
      
      // ตรวจว่า create loan
      expect(mockDb.insert).toHaveBeenCalledWith(
        'loans',
        expect.objectContaining({
          isbn: '978-1234567890',
          memberId: 'member-1',
          status: 'active',
        })
      );
      
      expect(result).toHaveProperty('dueDate');
    });

    test('throw error เมื่อหนังสือหมด', async () => {
      mockDb.findOne.mockResolvedValue(testBooks[1]);  // availableCopies: 0
      
      await expect(bookService.borrowBook('978-0987654321', 'member-1'))
        .rejects.toThrow('No copies available');
      
      expect(mockDb.update).not.toHaveBeenCalled();
      expect(mockDb.insert).not.toHaveBeenCalled();
    });
  });
});
```

---

# 🏆 สรุป: Jest Cheat Sheet

## Test Structure

```javascript
describe('Component', () => {
  beforeAll(() => { /* ครั้งเดียวก่อนทั้งหมด */ });
  afterAll(() => { /* ครั้งเดียวหลังทั้งหมด */ });
  beforeEach(() => { /* ก่อนแต่ละ test */ });
  afterEach(() => { /* หลังแต่ละ test */ });

  test('should do something', () => {
    expect(actual).toBe(expected);
  });

  test.skip('skip this test', () => {});
  test.only('run only this', () => {});
});
```

## Common Matchers

```javascript
// Equality
expect(x).toBe(y)              // x === y (primitive)
expect(x).toEqual(y)           // deep equality (object)
expect(x).not.toBe(y)          // negate

// Truthiness
expect(x).toBeTruthy()
expect(x).toBeFalsy()
expect(x).toBeNull()
expect(x).toBeUndefined()
expect(x).toBeDefined()

// Numbers
expect(x).toBeGreaterThan(y)
expect(x).toBeLessThan(y)
expect(x).toBeCloseTo(y, decimals)

// Strings
expect(str).toMatch(/regex/)
expect(str).toContain('substring')

// Arrays
expect(arr).toContain(item)
expect(arr).toContainEqual(obj)
expect(arr).toHaveLength(n)

// Objects
expect(obj).toHaveProperty('key')
expect(obj).toMatchObject({ partial })

// Errors
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('message')
expect(() => fn()).toThrow(ErrorClass)
```

## Async Testing

```javascript
// async/await
test('async', async () => {
  const result = await asyncFn();
  expect(result).toBe(x);
});

// resolves/rejects
await expect(promise).resolves.toBe(x);
await expect(promise).rejects.toThrow();
```

## Mocking

```javascript
// Mock function
const mockFn = jest.fn();
mockFn.mockReturnValue(x);
mockFn.mockResolvedValue(x);
mockFn.mockRejectedValue(error);
mockFn.mockImplementation(fn);

// Mock assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(n);
expect(mockFn).toHaveBeenCalledWith(args);

// Mock module
jest.mock('./module');

// Spy
jest.spyOn(obj, 'method');

// Reset
jest.clearAllMocks();
jest.resetAllMocks();
jest.restoreAllMocks();
```

## CLI Commands

```bash
npm test                    # รัน tests ทั้งหมด
npm test -- --watch         # watch mode
npm test -- --coverage      # coverage report
npm test -- --verbose       # แสดงรายละเอียด
npm test -- <pattern>       # รันเฉพาะไฟล์ที่ match
npm test -- -t "test name"  # รันเฉพาะ test ที่ชื่อ match
```

---

**🎉 ยินดีด้วย! คุณพร้อมเขียน Jest Tests แบบ Pro แล้ว!**

---

*เอกสารนี้จัดทำสำหรับวิชา Software Testing and Evaluation*  
*ปรับปรุงล่าสุด: มกราคม 2026*