# Unit Testing with Jest - คู่มือ

## 📚 สารบัญ

1. [บทนำ](#บทนำ)
2. [Jest คืออะไร](#jest-คืออะไร)
3. [ติดตั้ง Jest](#ติดตั้ง-jest)
4. [Watch Mode และ Automation](#watch-mode-และ-automation)
5. [โครงสร้างการทดสอบ](#โครงสร้างการทดสอบ)
6. [Matchers และ Assertions](#matchers-และ-assertions)
7. [ตัวอย่างการทดสอบ](#ตัวอย่างการทดสอบ)
8. [การทดสอบ Async Code](#การทดสอบ-async-code)
9. [Mocking](#mocking)
10. [Best Practices](#best-practices)
11. [แบบฝึกหัด](#แบบฝึกหัด)

---

## บทนำ

### เป้าหมายการเรียน

- เข้าใจความสำคัญของ Unit Testing
- สามารถเขียน Test Case ด้วย Jest ได้
- รู้วิธี Test ฟังก์ชัน, Async Code และ Mocking
- ปฏิบัติทดสอบโค้ดอย่างมืออาชีพ

### ทำไมต้อง Unit Testing?

```
✅ ความมั่นใจ: รู้ว่าโค้ดทำงานได้ถูกต้อง
✅ จับบั๊ก: หาบั๊ก ตอนเร็วก่อนไปถึง Production
✅ ปรับปรุงโค้ด: Refactoring อย่างปลอดภัย
✅ เอกสาร: Test Case เป็นตัวอย่างวิธีใช้โค้ด
✅ ออกแบบ: เขียน Test ก่อน → โค้ดดีขึ้น
```

---

## Jest คืออะไร

### นิยาม

Jest คือ Testing Framework ของ JavaScript/TypeScript ที่:

- **ใช้ง่าย**: Config ง่าย, ข้อความข้อผิดพลาดชัดเจน
- **เร็ว**: รัน Test ที่เปลี่ยนแปลงเท่านั้น
- **ครบครัน**: มี Assertion, Mock, Snapshot ในตัว
- **ทั่วไป**: ใช้กับ React, Vue, Node.js ฯลฯ

### เทียบกับเครื่องมืออื่น

```
Jest      vs  Mocha        vs  Vitest
─────────────────────────────────────
All-in-one    Lightweight   Lightning fast
Easy config   Setup needed  Modern ESM
Popular       Flexible      Vite native
```

---

## ติดตั้ง Jest

### ขั้นตอนที่ 1: สร้าง Project

```bash
mkdir my-jest-project
cd my-jest-project
npm init -y
```

### ขั้นตอนที่ 2: ติดตั้ง Jest

```bash
npm install --save-dev jest
# หรือ
npm install -D jest
```

### ขั้นตอนที่ 3: ตั้งค่า package.json

```json
{
  "name": "my-jest-project",
  "version": "1.0.0",
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "devDependencies": {
    "jest": "^29.0.0"
  }
}
```

### ขั้นตอนที่ 4: สร้าง jest.config.js (Optional)

```javascript
module.exports = {
  testEnvironment: "node",
  coverageDirectory: "coverage",
  testMatch: ["**/__tests__/**/*.js", "**/?(*.)+(spec|test).js"],
};
```

### ขั้นตอนที่ 5: รัน Test

```bash
npm test
```

---

## Watch Mode และ Automation

### Watch Mode (รัน Test อัตโนมัติ)

**Watch Mode** ช่วยให้ Test ทำงานอัตโนมัติเมื่อมีการเปลี่ยนแปลงไฟล์

```bash
npm run test:watch
```

**ตัวอักษรคำสั่ง ใน Watch Mode:**

```
 ▶ Press a to run all tests.
 ▶ Press f to run only failed tests.
 ▶ Press p to filter by a filename regex pattern.
 ▶ Press t to filter by a test name regex pattern.
 ▶ Press q to quit watch mode.
```

### ตัวอย่าง Workflow

```bash
# หน้าต่าง 1: เปิด Watch Mode
npm run test:watch

# หน้าต่าง 2: แก้ไขโค้ด
# เมื่อบันทึกไฟล์ → Test จะรันอัตโนมัติ
# ✅ PASS หรือ ❌ FAIL จะปรากฏในหน้าต่างแรก
```

### Optional: Pre-commit Hooks (รัน Test ก่อน Commit)

ติดตั้ง `husky` เพื่อรัน Test อัตโนมัติก่อน Commit:

```bash
npm install -D husky lint-staged

# ตั้งค่า husky
npx husky init

# สร้าง pre-commit hook
npx husky add .husky/pre-commit "npm test"
```

**เพิ่ม lint-staged (ทดสอบเฉพาะไฟล์ที่เปลี่ยน):** ใน package.json

```json
{
  "lint-staged": {
    "src/**/*.js": "jest --bail --findRelatedTests"
  }
}
```

### IDE Integration (VS Code)

**ติดตั้ง Extension:**

1. เปิด Extension Marketplace
2. ค้นหา "Jest"
3. ติดตั้ง "Jest Runner" หรือ "Jest"

**ตั้งค่า .vscode/settings.json:**

```json
{
  "jest.runMode": "on-demand",
  "jest.coverageColors": {
    "covered": "rgba(0, 255, 0, 0.3)",
    "uncovered": "rgba(255, 0, 0, 0.3)"
  }
}
```

**ประโยชน์:**

- ✅ เห็น Test ผ่าน/ตกได้ทันที
- ✅ Coverage highlighting ในโค้ด
- ✅ Click เพื่อรัน Test เดี่ยว
- ✅ Toggle Debug Mode

### Optional: CI/CD (Continuous Integration)

**ตัวอย่าง GitHub Actions:**

สร้างไฟล์ `.github/workflows/test.yml`:

```yaml
name: Jest Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"

      - name: Install dependencies
        run: npm install

      - name: Run tests
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3
```

### ตัวเลือก Jest CLI

```bash
# รันและรักษาไฟล์ที่เปลี่ยน
jest --watch

# รันแค่ test ที่ล้มเหลว
jest --onlyChanged

# รัน test จับคู่
jest --testNamePattern="should add"

# หยุดหลัง test ล้มเหลวไฟล์สแรก
jest --bail

# ประเมิน coverage
jest --coverage

# อัปเดต snapshot
jest --updateSnapshot

# ดีบัก test
node --inspect-brk node_modules/.bin/jest --runInBand
```

---

## โครงสร้างการทดสอบ

### โครงสร้างพื้นฐาน

```javascript
// 1. describe() - จัดกลุ่ม Test ที่เกี่ยวข้อง
describe("Calculator", () => {
  // 2. it() หรือ test() - เขียน Test Case เดี่ยว
  it("should add two numbers correctly", () => {
    // 3. arrange - เตรียมข้อมูล
    const a = 2;
    const b = 3;

    // 4. act - ทำการทดสอบ
    const result = a + b;

    // 5. assert - ตรวจสอบผลลัพธ์
    expect(result).toBe(5);
  });
});
```

### Arrange-Act-Assert (AAA) Pattern

```javascript
describe("User Registration", () => {
  it("should create a new user", () => {
    // ARRANGE: เตรียมข้อมูลนำเข้า
    const userData = { name: "John", email: "john@example.com" };

    // ACT: ดำเนินการที่ต้องการทดสอบ
    const user = createUser(userData);

    // ASSERT: ตรวจสอบผลลัพธ์ที่คาดหวัง
    expect(user.name).toBe("John");
    expect(user.id).toBeDefined();
  });
});
```

### Hooks (Setup และ Teardown)

```javascript
describe("Database Operations", () => {
  // ทำความสะอาดข้อมูลก่อน Test ทั้งหมด
  beforeAll(() => {
    console.log("เชื่อมต่อ Database...");
  });

  // ทำความสะอาดก่อน Test แต่ละครั้ง
  beforeEach(() => {
    console.log("เตรียมข้อมูลทดสอบ");
  });

  // ลบข้อมูลหลัง Test แต่ละครั้ง
  afterEach(() => {
    console.log("ล้างข้อมูลทดสอบ");
  });

  // ล้างข้อมูล หลัง Test ทั้งหมด
  afterAll(() => {
    console.log("ปิดการเชื่อมต่อ Database");
  });

  it("should insert data", () => {
    // Test case
  });
});
```

---

## Matchers และ Assertions

### Equality (เทียบค่า)

```javascript
const value = 5;

expect(value).toBe(5); // === (exact match)
expect(value).toEqual(5); // == (value comparison)
expect(value).not.toBe(4); // not equal
expect({ a: 1 }).toEqual({ a: 1 }); // deep equality
```

### Truthiness

```javascript
const user = { name: "John" };

expect(user).toBeTruthy(); // ค่าที่ true
expect(null).toBeFalsy(); // ค่าที่ false
expect(user).toBeDefined(); // กำหนดค่า
expect(user.age).toBeUndefined(); // ไม่กำหนดค่า
expect(user).not.toBeNull(); // ไม่ใช่ null
```

### Numbers

```javascript
const value = 4;

expect(value).toBeGreaterThan(3); // > 3
expect(value).toBeGreaterThanOrEqual(4); // >= 4
expect(value).toBeLessThan(5); // < 5
expect(value).toBeLessThanOrEqual(4); // <= 4
expect(0.1 + 0.2).toBeCloseTo(0.3); // ประมาณเท่ากัน (float)
```

### Strings

```javascript
const message = "Hello Jest";

expect(message).toMatch(/Jest/); // regex match
expect(message).toMatch("Jest"); // string match
expect(message).toHaveLength(11); // ความยาว
expect(message).toContain("Jest"); // มีข้อความ
```

### Arrays

```javascript
const fruits = ["apple", "banana", "orange"];

expect(fruits).toContain("apple"); // มีใน array
expect(fruits).toHaveLength(3); // จำนวนสมาชิก
expect(fruits).toEqual(expect.arrayContaining(["apple"]));
```

### Objects

```javascript
const user = { name: "John", age: 30 };

expect(user).toHaveProperty("name"); // มี property
expect(user).toHaveProperty("name", "John"); // property มีค่า
expect(user).toMatchObject({ name: "John" }); // คล้ายกับ object
```

### Exceptions

```javascript
function divide(a, b) {
  if (b === 0) throw new Error("Cannot divide by zero");
  return a / b;
}

expect(() => divide(10, 0)).toThrow(); // โยน error
expect(() => divide(10, 0)).toThrow("Cannot divide"); // error message
expect(() => divide(10, 0)).toThrow(Error); // error type
```

---

## ตัวอย่างการทดสอบ

### ตัวอย่างที่ 1: ฟังก์ชันธรรมชาติ

```javascript
// math.js
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

module.exports = { add, multiply };
```

```javascript
// math.test.js
const { add, multiply } = require("./math");

describe("Math Functions", () => {
  describe("add()", () => {
    it("should add two positive numbers", () => {
      expect(add(2, 3)).toBe(5);
    });

    it("should add negative numbers", () => {
      expect(add(-2, -3)).toBe(-5);
    });

    it("should return 0 when both inputs are 0", () => {
      expect(add(0, 0)).toBe(0);
    });
  });

  describe("multiply()", () => {
    it("should multiply two numbers", () => {
      expect(multiply(3, 4)).toBe(12);
    });

    it("should return 0 when one input is 0", () => {
      expect(multiply(5, 0)).toBe(0);
    });
  });
});
```

### ตัวอย่างที่ 2: ฟังก์ชัน String

```javascript
// stringUtils.js
function capitalize(str) {
  if (typeof str !== "string") throw new Error("Input must be string");
  return str.charAt(0).toUpperCase() + str.slice(1);
}

function isPalindrome(str) {
  const cleaned = str.toLowerCase().replace(/\s/g, "");
  return cleaned === cleaned.split("").reverse().join("");
}

module.exports = { capitalize, isPalindrome };
```

```javascript
// stringUtils.test.js
const { capitalize, isPalindrome } = require("./stringUtils");

describe("String Utilities", () => {
  describe("capitalize()", () => {
    it("should capitalize first letter", () => {
      expect(capitalize("hello")).toBe("Hello");
    });

    it("should handle already capitalized strings", () => {
      expect(capitalize("Hello")).toBe("Hello");
    });

    it("should throw error for non-string input", () => {
      expect(() => capitalize(123)).toThrow("Input must be string");
    });
  });

  describe("isPalindrome()", () => {
    it("should return true for palindromes", () => {
      expect(isPalindrome("racecar")).toBe(true);
      expect(isPalindrome("A man a plan a canal Panama")).toBe(true);
    });

    it("should return false for non-palindromes", () => {
      expect(isPalindrome("hello")).toBe(false);
    });
  });
});
```

### ตัวอย่างที่ 3: Object/Class

```javascript
// User.js
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
    this.posts = [];
  }

  addPost(post) {
    this.posts.push(post);
    return this.posts.length;
  }

  getPostCount() {
    return this.posts.length;
  }
}

module.exports = User;
```

```javascript
// User.test.js
const User = require("./User");

describe("User Class", () => {
  let user;

  beforeEach(() => {
    // สร้าง instance ใหม่ก่อน test แต่ละครั้ง
    user = new User("John Doe", "john@example.com");
  });

  it("should create user with correct properties", () => {
    expect(user.name).toBe("John Doe");
    expect(user.email).toBe("john@example.com");
    expect(user.posts).toEqual([]);
  });

  it("should add post to user", () => {
    const count = user.addPost("First post");

    expect(count).toBe(1);
    expect(user.posts).toHaveLength(1);
    expect(user.posts[0]).toBe("First post");
  });

  it("should return correct post count", () => {
    user.addPost("Post 1");
    user.addPost("Post 2");

    expect(user.getPostCount()).toBe(2);
  });
});
```

---

## การทดสอบ Async Code

### ตัวอย่างที่ 4: Promise

```javascript
// api.js
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: "John", email: "john@example.com" });
      } else {
        reject(new Error("Invalid user ID"));
      }
    }, 100);
  });
}

module.exports = { fetchUser };
```

```javascript
// api.test.js
const { fetchUser } = require("./api");

describe("API Functions", () => {
  // วิธีที่ 1: return Promise
  it("should fetch user with promise", () => {
    return fetchUser(1).then((user) => {
      expect(user.id).toBe(1);
      expect(user.name).toBe("John");
    });
  });

  // วิธีที่ 2: async/await (แนะนำ)
  it("should fetch user with async/await", async () => {
    const user = await fetchUser(1);
    expect(user.id).toBe(1);
    expect(user.name).toBe("John");
  });

  // วิธีที่ 3: .resolves matcher
  it("should fetch user with .resolves", () => {
    return expect(fetchUser(1)).resolves.toMatchObject({
      id: 1,
      name: "John",
    });
  });

  // ทดสอบ Rejection
  it("should reject with invalid ID", async () => {
    try {
      await fetchUser(-1);
      fail("Should have thrown error");
    } catch (error) {
      expect(error.message).toMatch("Invalid user ID");
    }
  });

  // ใช้ .rejects matcher
  it("should reject with invalid ID (rejects matcher)", () => {
    return expect(fetchUser(-1)).rejects.toThrow("Invalid user ID");
  });
});
```

### ตัวอย่างที่ 5: Async/Await Functions

```javascript
// userService.js
async function getUserWithPosts(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(userId);
  return { ...user, posts };
}

async function fetchUser(id) {
  // simulate API call
  return Promise.resolve({ id, name: "John" });
}

async function fetchPosts(userId) {
  return Promise.resolve([
    { id: 1, title: "First Post" },
    { id: 2, title: "Second Post" },
  ]);
}

module.exports = { getUserWithPosts };
```

```javascript
// userService.test.js
const { getUserWithPosts } = require("./userService");

describe("User Service", () => {
  it("should get user with posts", async () => {
    const result = await getUserWithPosts(1);

    expect(result).toHaveProperty("id");
    expect(result).toHaveProperty("name");
    expect(result.posts).toHaveLength(2);
    expect(result.posts[0].title).toBe("First Post");
  });
});
```

---

## Mocking

### ตัวอย่างที่ 6: Mock Functions

```javascript
// notification.js
function sendNotification(userId, callback) {
  // ส่งข้อมูลไปยัง API
  const result = { success: true, userId };
  callback(result);
}

module.exports = { sendNotification };
```

```javascript
// notification.test.js
const { sendNotification } = require("./notification");

describe("Notification", () => {
  it("should call callback with result", () => {
    // สร้าง mock function
    const mockCallback = jest.fn();

    sendNotification(123, mockCallback);

    // ตรวจสอบว่า callback ถูกเรียก
    expect(mockCallback).toHaveBeenCalled();
    expect(mockCallback).toHaveBeenCalledTimes(1);
    expect(mockCallback).toHaveBeenCalledWith({
      success: true,
      userId: 123,
    });
  });
});
```

### ตัวอย่างที่ 7: Mock Modules

```javascript
// database.js
const db = {
  getUser: (id) => {
    // โอ้ มันเรียกใช้ database จริง
    console.log(`SELECT * FROM users WHERE id = ${id}`);
    return { id, name: "Real User" };
  },
};

module.exports = db;
```

```javascript
// userRepository.js
const db = require("./database");

function getUserById(id) {
  return db.getUser(id);
}

module.exports = { getUserById };
```

```javascript
// userRepository.test.js
jest.mock("./database");
const db = require("./database");
const { getUserById } = require("./userRepository");

describe("User Repository", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("should get user from database", () => {
    // Mock database response
    db.getUser.mockReturnValue({ id: 1, name: "Mocked User" });

    const user = getUserById(1);

    expect(db.getUser).toHaveBeenCalledWith(1);
    expect(user.name).toBe("Mocked User");
  });
});
```

### ตัวอย่างที่ 8: Mock Fetch API

```javascript
// weatherService.js
async function getWeather(city) {
  const response = await fetch(`https://api.weather.com/city/${city}`);
  const data = await response.json();
  return data;
}

module.exports = { getWeather };
```

```javascript
// weatherService.test.js
const { getWeather } = require("./weatherService");

describe("Weather Service", () => {
  beforeEach(() => {
    global.fetch = jest.fn();
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  it("should fetch weather data", async () => {
    // Mock fetch response
    const mockData = {
      city: "Bangkok",
      temperature: 32,
      condition: "Sunny",
    };

    global.fetch.mockResolvedValueOnce({
      json: jest.fn().mockResolvedValueOnce(mockData),
    });

    const result = await getWeather("Bangkok");

    expect(global.fetch).toHaveBeenCalledWith(
      "https://api.weather.com/city/Bangkok",
    );
    expect(result.city).toBe("Bangkok");
  });
});
```

---

## Best Practices

### ✅ DO's

```javascript
// 1. ใช้ descriptive names
it("should add two positive numbers correctly", () => {
  expect(add(2, 3)).toBe(5);
});

// 2. Test one thing per test case
it("should validate email format", () => {
  expect(isValidEmail("user@example.com")).toBe(true);
});

it("should reject empty email", () => {
  expect(isValidEmail("")).toBe(false);
});

// 3. ใช้ AAA pattern
it("should create user", () => {
  // Arrange
  const userData = { name: "John", email: "john@test.com" };

  // Act
  const user = createUser(userData);

  // Assert
  expect(user.id).toBeDefined();
});

// 4. Test edge cases
it("should handle empty array", () => {
  expect(getMax([])).toBeUndefined();
});

// 5. ล้างข้อมูลหลังทดสอบ
describe("Database", () => {
  afterEach(() => {
    cleanupDatabase();
  });
});
```

### ❌ DON'Ts

```javascript
// 1. หลีกเลี่ยง test หลายสิ่ง
it("should create user and send email", () => {
  // ทำสองอย่าง ไม่ดี!
});

// 2. หลีกเลี่ยง vague descriptions
it("should work", () => {
  // ??? ทำงานทำไม?
});

// 3. หลีกเลี่ยง test dependencies
it("should create user", () => {
  createUser("John"); // อาศัย test อื่น?
});

// 4. หลีกเลี่ยง setTimeout
setTimeout(() => {
  expect(result).toBe(5);
}, 1000); // ใช้ jest.useFakeTimers แทน
```

### การทดสอบ Coverage

```bash
# ดู coverage report
npm run test:coverage

# สร้าง HTML report
npx jest --coverage --coverageReporters=html
```

**Coverage Goals:**

- **Statements**: >80% - เขียน test ครอบคำสั่งทั้งหมด
- **Branches**: >75% - test ทิศทางทั้งหมด (if/else)
- **Functions**: >80% - ทดสอบฟังก์ชันทั้งหมด
- **Lines**: >80% - ครอบทุกบรรทัด

---

## แบบฝึกหัด

### ลำดับความยาก: ⭐ (ง่าย) → ⭐⭐⭐ (ยาก)

### แบบฝึกหัดที่ 1: ⭐ Basic Calculations

**โจทย์:**
เขียน test case สำหรับฟังก์ชันต่อไปนี้:

```javascript
// calculator.js
function subtract(a, b) {
  return a - b;
}
function divide(a, b) {
  return a / b;
}
function isEven(n) {
  return n % 2 === 0;
}
```

**ต้องทดสอบ:**

- subtract ทั่วไป, negative numbers
- divide เลขปกติ, ด้วย 0
- isEven ตัวเลขคู่, คี่

---

### แบบฝึกหัดที่ 2: ⭐⭐ String Validation

**โจทย์:**
เขียนฟังก์ชันและ test case สำหรับ:

```javascript
// validators.js
function isValidPassword(password) {
  // ต้องมี: ความยาว >= 8, มีตัวเลข, ตัวอักษรใหญ่
  return (
    password.length >= 8 && /[0-9]/.test(password) && /[A-Z]/.test(password)
  );
}
```

**ต้องทดสอบ:** Password ที่ valid/invalid ต่าง ๆ

---

### แบบฝึกหัดที่ 3: ⭐⭐ Array Operations

**โจทย์:**
เขียน test สำหรับ:

```javascript
function getMax(arr) {
  if (arr.length === 0) return undefined;
  return Math.max(...arr);
}

function removeDuplicates(arr) {
  return [...new Set(arr)];
}
```

**ต้องพิจารณา:**

- Array ว่าง
- Single element
- Duplicates
- Negative numbers

---

### แบบฝึกหัดที่ 4: ⭐⭐⭐ API Testing

**โจทย์:**
เขียน test สำหรับ:

```javascript
async function getUserProfile(userId) {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) throw new Error("User not found");
  return response.json();
}
```

**ต้องทดสอบ:**

- Mock successful response
- Mock error response
- ตรวจสอบ fetch ถูกเรียกถูกต้อง

---

### แบบฝึกหัดที่ 5: ⭐⭐⭐ Class Testing

**โจทย์:**
เขียน test สำหรับคลาส:

```javascript
class ShoppingCart {
  constructor() {
    this.items = [];
  }

  addItem(item, price) {
    this.items.push({ item, price });
  }

  getTotal() {
    return this.items.reduce((sum, i) => sum + i.price, 0);
  }

  clear() {
    this.items = [];
  }
}
```

**ต้องทดสอบ:**

- Add items
- Calculate total
- Clear cart
- Empty cart scenarios

---

## เรียนรู้เพิ่มเติม

### ทรัพยากร

- [Jest Official Documentation](https://jestjs.io)
- [Jest Matchers Reference](https://jestjs.io/docs/expect)
- [Testing Best Practices](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)

### วิดีโอสอน

- Jest Basics - Unit Testing in JavaScript
- Advanced Jest Patterns
- Testing Real-World Applications

---

## สรุป

### Key Takeaways

1. **Unit Testing** คือการทดสอบส่วนเล็กของโค้ด
2. **Jest** เป็น Framework ที่ครบครัน, ใช้ง่าย
3. **AAA Pattern** ช่วยเขียน test ที่ชัดเจน
4. **Mocking** ช่วยทดสอบ dependencies
5. **Coverage** ช่วยวัดคุณภาพ test

### Checklist เมื่อสำเร็จหลักสูตร

- ✅ สามารถเขียน test case ธรรมชาติ
- ✅ เข้าใจ async/await testing ทั้งหมด
- ✅ รู้วิธี Mock functions และ modules
- ✅ เข้าใจ Coverage metrics
- ✅ สามารถ maintain test suites ได้

---

**Happy Testing! 🎉**
