# สัปดาห์ที่ 9: Integration Testing

## 📋 วัตถุประสงค์การเรียนรู้

เมื่อจบบทเรียนนี้ นิสิตจะสามารถ:

- อธิบายแนวคิดและวัตถุประสงค์ของ Integration Testing
- เข้าใจ Integration Strategies ต่างๆ และการเลือกใช้
- ทำ Integration Testing กับ Database และ External APIs
- ทำ API Contract Testing ด้วย JSON Schema
- ใช้ Mocking จัดการ External Dependencies
- เขียน Integration Tests ด้วย supertest และ msw

---

## 📚 PART 1: Integration Testing Fundamentals

### 1.1 Integration Testing คืออะไร?

**Integration Testing** คือการทดสอบการทำงานร่วมกันของ **modules หลายๆ ตัว** ที่ถูกรวมเข้าด้วยกัน เพื่อตรวจสอบว่าส่วนต่างๆ ทำงานร่วมกันได้ถูกต้องหรือไม่

```
Unit Testing:        [Module A]  ← ทดสอบแยกส่วน ✓
Integration Testing: [Module A] ↔ [Module B] ← ทดสอบการเชื่อมต่อ ✓
System Testing:      [Module A ↔ Module B ↔ Module C] ← ทดสอบทั้งระบบ ✓
```

### 1.2 ทำไมต้องทำ Integration Testing?

**ปัญหาที่พบบ่อย:**

- Unit tests ผ่านหมดแล้ว แต่พอรวมกันแล้วพัง
- Interface ระหว่าง modules ไม่ตรงกัน (parameter type, data format)
- Database operations ล้มเหลว (schema mismatch, constraint violations)
- External API calls ใช้งานไม่ได้ (wrong endpoint, auth failures)
- Data flow ผิดพลาด (data loss, data corruption)

**ตัวอย่างจริง:**

```javascript
// Unit Test ผ่าน ✓
class UserService {
  createUser(userData) {
    if (!userData.email) throw new Error("Email required");
    return { id: 1, ...userData };
  }
}

// Database Layer ทำงานได้ ✓
class Database {
  async save(table, data) {
    // บันทึกลง DB
    return { id: 1 };
  }
}

// แต่พอ integrate แล้วเกิดปัญหา ✗
// ปัญหา 1: UserService returns sync, Database.save returns async Promise
// ปัญหา 2: Database ต้องการ 'users' table name แต่ไม่ได้ส่งไป
// ปัญหา 3: Email format validation ใน DB strict กว่าใน UserService
```

### 1.3 Integration Testing vs Unit Testing

| Aspect           | Unit Testing           | Integration Testing       |
| ---------------- | ---------------------- | ------------------------- |
| **Scope**        | Single module/function | Multiple modules combined |
| **Dependencies** | Mocked/Isolated        | Real/Partially real       |
| **Speed**        | Very fast (ms)         | Slower (100ms - seconds)  |
| **Focus**        | Internal logic         | Interface & interaction   |
| **When**         | During development     | After unit tests pass     |
| **Who**          | Developers             | Developers/QA             |

### 1.4 Integration Testing Objectives

🎯 **Main Objectives:**

1. **Verify Interfaces** - ตรวจสอบว่า interface ระหว่าง modules ทำงานถูกต้อง
2. **Test Data Flow** - ข้อมูลไหลผ่าน modules ต่างๆ ถูกต้อง
3. **Catch Integration Bugs** - จับ bugs ที่เกิดจากการรวม modules
4. **Validate External Interactions** - ทดสอบการเชื่อมต่อกับระบบภายนอก
5. **Ensure Data Integrity** - ข้อมูลถูกต้องตลอด lifecycle

---

## 🏗️ PART 2: Integration Strategies

### 2.1 Integration Strategies Overview

มี 4 strategies หลักๆ แต่ในโลกจริง **strategy ขึ้นกับ architecture ของระบบ**

#### **1. Big Bang Integration**

รวม modules ทั้งหมดพร้อมกัน แล้วทดสอบทีเดียว

```
[Module A] ─┐
[Module B] ─┼─→ [Test All Together]
[Module C] ─┘
```

**ข้อดี:**

- ทำได้เร็ว ไม่ต้องทำทีละ module

**ข้อเสีย:**

- หา bugs ยาก (ไม่รู้ว่า bug อยู่ตรงไหน)
- Debug ลำบาก
- ทดสอบได้เมื่อ modules สำเร็จหมดแล้วเท่านั้น

**เมื่อไหร่ใช้:**

- ระบบเล็กมาก (2-3 modules)
- Time-constrained projects
- **ไม่แนะนำในทางปฏิบัติ**

---

#### **2. Top-Down Integration**

เริ่มจาก high-level modules ลงมา low-level modules (ใช้ **stubs** แทน modules ที่ยังไม่เสร็จ)

📌 **Stub คืออะไร?** → Module หลอม (fake/dummy) ที่ยัง **ไม่มีการ implement จริง** แต่สามารถ **ส่งข้อมูล return** เพื่อให้ module ระดับบนสามารถทดสอบได้ ตัวอย่าง: `function getUser() { return { id: 1, name: 'Test' }; }`

```
     [Main]
       ↓
   [Module A] (real) ← ทดสอบจริง
     ↓     ↓
  [Stub]  [Stub]  ← หลอม ไม่มี code จริง
```

**ข้อดี:**

- เห็น UI/Main flow ได้เร็ว
- Major design flaws พบเร็ว

**ข้อเสีย:**

- ต้องเขียน stubs (งานเพิ่ม)
- Low-level modules ไม่ได้ทดสอบจริงจนกว่าจะเสร็จ

---

#### **3. Bottom-Up Integration**

เริ่มจาก low-level modules ขึ้นมา high-level modules (ใช้ **drivers** เรียก modules ระดับล่าง)

📌 **Driver คืออะไร?** → Code ที่ **สร้างมาเพื่อทดสอบ** module ระดับล่าง โดย **เรียกใช้ function** ของ module นั้นแล้วตรวจสอบผลลัพธ์ ไม่ใช่ real application code ตัวอย่าง: test file ที่เรียก `bookService.createBook()`

```
  [TestDriver1] [TestDriver2]  ← Test code ที่เรียก modules
        ↓              ↓
  [Module A]     [Module B] (real) ← ทดสอบจริง
         ↓              ↓
    [Database] (real) ← ทดสอบจริง
```

**ข้อดี:**

- Core functionality ทดสอบได้เร็ว
- ไม่ต้องรอ UI เสร็จ

**ข้อเสีย:**

- ไม่เห็น overall behavior จนกว่า main modules จะเสร็จ

---

#### **4. Sandwich (Hybrid) Integration**

ผสม Top-Down และ Bottom-Up - ทำทั้งสองอย่างพร้อมกัน

```
    [Main] (real)
      ↓
  [Module A] (real)
      ↓
  [Database] (real)
```

**ข้อดี:**

- Flexible - เลือกวิธีที่เหมาะสมกับแต่ละส่วน
- ทดสอบได้เร็วกว่า

**ข้อเสีย:**

- ซับซ้อนในการจัดการ

---

### 2.2 ในโลกจริง...

**💡 Real-World Truth:**

- ส่วนใหญ่ใช้ **Continuous Integration** approach
- Integrate modules เมื่อพร้อม (ไม่รอให้ครบ)
- ใช้ **automated testing** ตรวจสอบทุกครั้งที่ integrate
- Strategy ขึ้นกับ architecture:
  - **Microservices** → Test each service's API independently
  - **Monolith** → Mix of Bottom-Up และ Top-Down
  - **Layered Architecture** → Layer by layer (Database → Business Logic → API → UI)

**Modern Approach:**

```
Developer writes code → Push to Git → CI runs tests automatically
                                    ↓
                          Unit Tests → Integration Tests → Deploy
```

---

## 🔧 PART 3: Integration Testing กับ Database และ External APIs

### 3.1 Integration Testing กับ Database

เมื่อทดสอบกับ Database เราต้องทดสอบ:

- CRUD operations ทำงานถูกต้อง
- Data constraints (NOT NULL, UNIQUE, FOREIGN KEY)
- Transactions (commit/rollback)
- Query performance

#### **Demo: Testing Database Integration**

```javascript
// bookService.js - Business Logic Layer
// สอดคล้องกับ Book module ของ Library Management
class BookService {
  constructor(database) {
    this.db = database; // Database instance ส่งเข้ามา (Dependency Injection)
  }

  // Create: เพิ่มหนังสือใหม่
  async createBook(bookData) {
    // Step 1: Validation - ตรวจสอบ input ก่อน
    if (!bookData.title || !bookData.author) {
      throw new Error("Title and author are required");
    }

    // Step 2: Save to database - บันทึกลง DB แท้จริง
    const book = await this.db.books.create(bookData);
    return book;
  }

  // Read: ค้นหามหนังสือจาก ID
  async findBookById(id) {
    const book = await this.db.books.findById(id);
    if (!book) {
      throw new Error("Book not found");
    }
    return book;
  }

  // Update: แก้ไขข้อมูลหนังสือ
  async updateBook(id, updates) {
    const book = await this.db.books.update(id, updates);
    return book;
  }

  // Delete: ลบหนังสือ
  async deleteBook(id) {
    await this.db.books.delete(id);
  }
}

module.exports = BookService;
```

```javascript
// bookService.integration.test.js
const BookService = require("./bookService");
const Database = require("./database"); // Real database connection

describe("BookService Integration Tests", () => {
  let bookService;
  let db;

  beforeAll(async () => {
    // Setup test database
    db = new Database({
      host: "localhost",
      database: "library_test", // ใช้ test database แยก
      user: "testuser",
      password: "testpass",
    });
    await db.connect();
    bookService = new BookService(db);
  });

  beforeEach(async () => {
    // Clean database before each test
    await db.books.deleteAll();
  });

  afterAll(async () => {
    // Cleanup
    await db.disconnect();
  });

  test("should create book in database", async () => {
    const bookData = {
      title: "Test Book",
      author: "Test Author",
      isbn: "978-1234567890",
    };

    const book = await bookService.createBook(bookData);

    expect(book).toHaveProperty("id");
    expect(book.title).toBe("Test Book");
    expect(book.author).toBe("Test Author");

    // Verify in database
    const savedBook = await db.books.findById(book.id);
    expect(savedBook).toBeDefined();
    expect(savedBook.title).toBe("Test Book");
  });

  test("should enforce database constraints", async () => {
    const invalidBook = {
      title: "", // Empty title - violates NOT NULL
      author: "Test Author",
    };

    await expect(bookService.createBook(invalidBook)).rejects.toThrow();
  });

  test("should handle duplicate ISBN", async () => {
    const book1 = {
      title: "Book 1",
      author: "Author 1",
      isbn: "978-1111111111",
    };

    await bookService.createBook(book1);

    const book2 = {
      title: "Book 2",
      author: "Author 2",
      isbn: "978-1111111111", // Same ISBN
    };

    await expect(bookService.createBook(book2)).rejects.toThrow(/duplicate/i);
  });

  test("should update book in database", async () => {
    const book = await bookService.createBook({
      title: "Original Title",
      author: "Original Author",
    });

    const updated = await bookService.updateBook(book.id, {
      title: "Updated Title",
    });

    expect(updated.title).toBe("Updated Title");
    expect(updated.author).toBe("Original Author"); // Unchanged

    // Verify in database
    const dbBook = await db.books.findById(book.id);
    expect(dbBook.title).toBe("Updated Title");
  });

  test("should delete book from database", async () => {
    const book = await bookService.createBook({
      title: "To Be Deleted",
      author: "Test Author",
    });

    await bookService.deleteBook(book.id);

    // Verify deleted
    const deletedBook = await db.books.findById(book.id);
    expect(deletedBook).toBeNull();
  });
});
```

**💡 Best Practices:**

- ใช้ **test database แยก** (ไม่ใช้ production DB)
- **Clean data** ก่อน/หลังแต่ละ test
- ทดสอบ **constraints และ validations**
- ทดสอบ **edge cases** (null values, invalid data)

---

### 3.2 Integration Testing กับ External APIs

เมื่อทดสอบกับ External APIs มี 2 approaches:

1. **Real API calls** - เหมาะกับ staging/pre-production
2. **Mock API** - เหมาะกับ local development และ CI

#### **Demo: Testing External API Integration**

```javascript
// emailService.js - External Service (บริการส่งอีเมล)
const axios = require("axios");

class EmailService {
  // Constructor: รับ API key เพื่อ authenticate กับ SendGrid service
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseURL = "https://api.sendgrid.com/v3";
  }

  // async sendEmail: ส่งอีเมลไปยัง external email service
  async sendEmail(to, subject, body) {
    const response = await axios.post(
      `${this.baseURL}/mail/send`,
      {
        personalizations: [{ to: [{ email: to }] }],
        from: { email: "library@example.com" },
        subject: subject,
        content: [{ type: "text/plain", value: body }],
      },
      {
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          "Content-Type": "application/json",
        },
      },
    );

    return response.data;
  }
}

// borrowService.js
class BorrowService {
  // Constructor: รับ database instance และ emailService instance
  // ใช้ Dependency Injection เพื่อให้สามารถ mock ได้ในการทดสอบ
  constructor(database, emailService) {
    this.db = database;
    this.emailService = emailService;
  }

  async borrowBook(userId, bookId) {
    // Step 1: ตรวจสอบว่าหนังสือมีอยู่ในระบบ
    const book = await this.db.books.findById(bookId);
    if (!book) throw new Error("Book not found");

    // Step 2: ตรวจสอบว่าหนังสือยังมีจำนวนที่ยืมได้
    if (book.availableCopies === 0) throw new Error("Book not available");

    // Step 3: บันทึก record การยืมหนังสือลงฐานข้อมูล
    const borrow = await this.db.borrows.create({
      userId,
      bookId,
      borrowDate: new Date(),
      dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14 days
    });

    // Step 4: อัปเดตจำนวนหนังสือที่มีในคลัง
    await this.db.books.update(bookId, {
      availableCopies: book.availableCopies - 1,
    });

    // Step 5: ส่งอีเมลยืนยันให้ผู้ใช้ (Graceful Degradation)
    // ถ้าส่งอีเมลล้มเหลว ให้ยืมสำเร็จต่อได้ (ไม่ fail transaction ทั้งหมด)
    const user = await this.db.users.findById(userId);
    try {
      await this.emailService.sendEmail(
        user.email,
        "Book Borrowed Successfully",
        `You have borrowed "${book.title}". Due date: ${borrow.dueDate}`,
      );
    } catch (error) {
      console.error("Failed to send email:", error);
      // Don't fail the operation if email fails
    }

    return borrow;
  }
}

module.exports = { EmailService, BorrowService };
```

```javascript
// borrowService.integration.test.js - Using REAL API
const { EmailService, BorrowService } = require("./borrowService");
const Database = require("./database");

describe("BorrowService Integration Tests (Real API)", () => {
  let borrowService;
  let db;

  beforeAll(async () => {
    db = new Database({
      /* test config */
    });
    await db.connect();

    // Use real email service (with test API key)
    const emailService = new EmailService(process.env.SENDGRID_TEST_API_KEY);
    borrowService = new BorrowService(db, emailService);
  });

  // ... tests with real external API calls
});
```

---

### 3.3 Mocking External Services ด้วย MSW

**ปัญหาของ Real API calls:**

- ช้า (network latency: ต้องรอการตอบกลับจาก external server)
- ค่าใช้จ่าย (API quota: ทุก test ใช้ credit หรือ API calls)
- ไม่ stable (network issues, service downtime)
- ทดสอบ error scenarios ยาก (ยากที่จะจำลองเมื่อ SendGrid return 500)

**Solution: Mock Service Worker (msw)**

📌 **MSW ทำงานอย่างไร?**
แทนที่จะส่ง request จริงไปยัง `https://api.sendgrid.com` MSW จะ **intercept** HTTP request ให้แล้วส่ง response fake กลับมาจากที่เดียว

```
ไม่ใช้ MSW:
  Test Code → HTTP Request → Internet → SendGrid API → Response

ใช้ MSW:
  Test Code → HTTP Request → [MSW Intercepts] → Fake Response
```

```javascript
// borrowService.integration.test.js - Using MSW
const { rest } = require("msw");
const { setupServer } = require("msw/node");
const { EmailService, BorrowService } = require("./borrowService");
const Database = require("./database");

// Setup mock server
const server = setupServer(
  rest.post("https://api.sendgrid.com/v3/mail/send", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({ success: true, messageId: "mock-message-id" }),
    );
  }),
);

describe("BorrowService Integration Tests (Mocked API)", () => {
  let borrowService;
  let db;

  beforeAll(async () => {
    server.listen(); // Start mock server

    db = new Database({
      /* test config */
    });
    await db.connect();

    const emailService = new EmailService("mock-api-key");
    borrowService = new BorrowService(db, emailService);
  });

  afterEach(() => {
    server.resetHandlers(); // Reset to default handlers
  });

  afterAll(async () => {
    server.close(); // Stop mock server
    await db.disconnect();
  });

  beforeEach(async () => {
    await db.books.deleteAll();
    await db.users.deleteAll();
    await db.borrows.deleteAll();
  });

  test("should borrow book and send email", async () => {
    // Setup test data
    const user = await db.users.create({
      email: "user@test.com",
      name: "Test User",
    });

    const book = await db.books.create({
      title: "Test Book",
      author: "Test Author",
      availableCopies: 5,
    });

    // Borrow book
    const borrow = await borrowService.borrowBook(user.id, book.id);

    // Verify borrow record created
    expect(borrow).toHaveProperty("id");
    expect(borrow.userId).toBe(user.id);
    expect(borrow.bookId).toBe(book.id);
    expect(borrow.dueDate).toBeDefined();

    // Verify book availability updated
    const updatedBook = await db.books.findById(book.id);
    expect(updatedBook.availableCopies).toBe(4);
  });

  test("should handle email service failure gracefully", async () => {
    // Mock email failure
    server.use(
      rest.post("https://api.sendgrid.com/v3/mail/send", (req, res, ctx) => {
        return res(ctx.status(500), ctx.json({ error: "Service unavailable" }));
      }),
    );

    const user = await db.users.create({
      email: "user@test.com",
      name: "Test User",
    });

    const book = await db.books.create({
      title: "Test Book",
      author: "Test Author",
      availableCopies: 5,
    });

    // Should still succeed even if email fails
    const borrow = await borrowService.borrowBook(user.id, book.id);
    expect(borrow).toBeDefined();

    // Verify book still borrowed
    const updatedBook = await db.books.findById(book.id);
    expect(updatedBook.availableCopies).toBe(4);
  });

  test("should not borrow if book not available", async () => {
    const user = await db.users.create({
      email: "user@test.com",
      name: "Test User",
    });

    const book = await db.books.create({
      title: "Test Book",
      author: "Test Author",
      availableCopies: 0, // No copies available
    });

    await expect(borrowService.borrowBook(user.id, book.id)).rejects.toThrow(
      "Book not available",
    );

    // Verify no borrow record created
    const borrows = await db.borrows.findAll();
    expect(borrows).toHaveLength(0);
  });
});
```

**💡 MSW Benefits:**

- Fast (no network calls)
- Reliable (no external dependencies)
- Free (no API costs)
- Easy to test error scenarios
- Works offline

---

## 📋 PART 4: API Contract Testing

### 4.1 API Contract Testing คืออะไร?

**API Contract** คือ "สัญญา" ที่กำหนดว่า API จะ:

- รับ input อะไร (request format)
- ส่ง output อะไรกลับมา (response format)
- Response มี structure และ data types อย่างไร

**ทำไมต้องมี API Contract Testing?**

- **Microservices** - หลาย services ต้องคุยกัน ผ่าน APIs
- **API Versioning** - แก้ API ต้องไม่ทำให้ client พัง
- **Frontend-Backend** - ตกลง API format ไว้ล่วงหน้า
- **Third-party Integration** - รับประกันว่า API ตรงตาม documentation

### 4.2 JSON Schema Validation

**JSON Schema** คือภาษาสำหรับบรรยาย structure ของ JSON data

📌 **ทำไมต้อง JSON Schema?**

- เป็นสัญญา (contract) ระหว่าง API producer (backend) และ consumer (frontend)
- ตรวจสอบว่า API response matches requirements
- ไม่ได้ทดสอบ business logic (ใช้ integration tests) แค่ format/structure

#### **Demo: Basic JSON Schema**

```javascript
// Book API Response Schema
const bookSchema = {
  type: "object", // ← Response ต้องเป็น object
  properties: {
    // id: ต้องเป็น number และ ≥ 1
    id: {
      type: "number",
      minimum: 1,
    },
    title: {
      type: "string",
      minLength: 1,
      maxLength: 200,
    },
    author: {
      type: "string",
      minLength: 1,
      maxLength: 100,
    },
    isbn: {
      type: "string",
      pattern: "^[0-9-]+$",
      minLength: 10,
      maxLength: 17,
    },
    // publishedYear: 1000-2100
    publishedYear: {
      type: "number",
      minimum: 1000,
      maximum: 2100,
    },
    // availableCopies: ≥ 0 (ไม่สามารถ negative ได้)
    availableCopies: {
      type: "number",
      minimum: 0,
    },
    // totalCopies: ≥ 1 (ต้องมีหนังสือ ติดลบด้วย)
    totalCopies: {
      type: "number",
      minimum: 1,
    },
  },
  // 🔑 required: fields ที่บังคับต้องมี
  // ถ้า response ไม่มี id หรือ title → validation fail
  required: ["id", "title", "author"],

  // 🚫 additionalProperties: false = ห้ามมี field พิเศษ
  additionalProperties: false,
};
```

### 4.3 การใช้ Ajv (Another JSON Schema Validator)

```javascript
// contract-tests.js
const Ajv = require("ajv");
const ajv = new Ajv({ allErrors: true });
const request = require("supertest");
const app = require("../app");

describe("Book API Contract Tests", () => {
  const bookSchema = {
    type: "object",
    properties: {
      id: { type: "number", minimum: 1 },
      title: { type: "string", minLength: 1 },
      author: { type: "string", minLength: 1 },
      isbn: { type: "string", pattern: "^[0-9-]+$" },
      publishedYear: { type: "number", minimum: 1000, maximum: 2100 },
    },
    required: ["id", "title", "author"],
  };

  const bookListSchema = {
    type: "array",
    items: bookSchema,
    minItems: 0,
  };

  test("GET /api/books/:id returns valid book schema", async () => {
    // Create test book first
    const createResponse = await request(app).post("/api/books").send({
      title: "Contract Test Book",
      author: "Test Author",
      isbn: "978-1234567890",
      publishedYear: 2024,
    });

    const bookId = createResponse.body.id;

    // Get book
    const response = await request(app).get(`/api/books/${bookId}`);

    expect(response.status).toBe(200);

    // Validate schema
    const validate = ajv.compile(bookSchema);
    const valid = validate(response.body);

    if (!valid) {
      console.log("Validation errors:", validate.errors);
    }

    expect(valid).toBe(true);
  });

  test("GET /api/books returns valid array of books", async () => {
    const response = await request(app).get("/api/books");

    expect(response.status).toBe(200);

    // Validate array schema
    const validate = ajv.compile(bookListSchema);
    const valid = validate(response.body);

    expect(valid).toBe(true);
  });

  test("POST /api/books with invalid data returns 400", async () => {
    const invalidBook = {
      title: "", // Empty title - invalid
      author: "Test Author",
    };

    const response = await request(app).post("/api/books").send(invalidBook);

    expect(response.status).toBe(400);
  });

  test("Response has no unexpected fields", async () => {
    const response = await request(app).get("/api/books/1");

    const allowedFields = [
      "id",
      "title",
      "author",
      "isbn",
      "publishedYear",
      "availableCopies",
      "totalCopies",
      "createdAt",
      "updatedAt",
    ];

    const actualFields = Object.keys(response.body);
    const unexpectedFields = actualFields.filter(
      (f) => !allowedFields.includes(f),
    );

    expect(unexpectedFields).toEqual([]);
  });
});
```

### 4.4 Complex Schema Example

```javascript
// User with Borrowed Books Schema - Nested objects
const userWithBooksSchema = {
  type: "object",
  properties: {
    id: { type: "number" },
    name: { type: "string" },
    // 📧 email: string with format validator
    // format: "email" = validate email format (e.g., user@example.com)
    email: { type: "string", format: "email" },
    // 📚 borrowedBooks: array of objects
    borrowedBooks: {
      type: "array", // ← ต้องเป็น array
      // 📌 items: describes each item ใน array
      items: {
        type: "object",
        properties: {
          bookId: { type: "number" },
          title: { type: "string" },
          // 📅 date-time format validator
          // "2024-02-20T10:30:00Z" (ISO 8601)
          // "2024-02-20" (just date, missing time)
          borrowDate: { type: "string", format: "date-time" },
          dueDate: { type: "string", format: "date-time" },
          // 🎁 enum: only allow specific values
          // "active", "overdue", "returned"
          // "pending", "completed"
          status: {
            type: "string",
            enum: ["active", "overdue", "returned"],
          },
        },
        required: ["bookId", "title", "borrowDate", "dueDate", "status"],
      },
    },
  },
  required: ["id", "name", "email", "borrowedBooks"],
};

test("GET /api/users/:id/borrowed-books returns valid schema", async () => {
  const response = await request(app).get("/api/users/1/borrowed-books");

  const validate = ajv.compile(userWithBooksSchema);
  const valid = validate(response.body);

  expect(valid).toBe(true);
});
```

### 4.5 API Contract Testing Best Practices

**DO:**

- Define schemas สำหรับทุก API endpoint
- ทดสอบทั้ง success และ error responses
- ใช้ `format` validators (email, date-time, uri)
- ระบุ `required` fields ชัดเจน
- กำหนด min/max สำหรับ numbers และ strings

**DON'T:**

- Schema ไม่ควรซับซ้อนเกินไป (ยากต่อการ maintain)
- ไม่ควร validate business logic ใน schema (ใช้ integration tests)
- ไม่ควรกำหนด schema strict เกินไป (ทำให้ refactor ยาก)

---

## 🎯 PART 5: Integration Testing Tools & Frameworks

### 5.1 Supertest - HTTP Assertion Library

**Supertest** ทำให้ทดสอบ APIs ง่ายขึ้น โดยไม่ต้อง start server จริง

```javascript
const request = require("supertest");
const app = require("../app");

describe("Book API", () => {
  test("GET /api/books returns 200", async () => {
    const response = await request(app)
      .get("/api/books")
      .expect(200)
      .expect("Content-Type", /json/);

    expect(Array.isArray(response.body)).toBe(true);
  });

  test("POST /api/books creates book", async () => {
    const newBook = {
      title: "New Book",
      author: "New Author",
    };

    const response = await request(app)
      .post("/api/books")
      .send(newBook)
      .expect(201)
      .expect("Content-Type", /json/);

    expect(response.body).toHaveProperty("id");
    expect(response.body.title).toBe("New Book");
  });
});
```

### 5.2 MSW - Mock Service Worker

**MSW** ช่วย mock HTTP requests ทั้ง REST และ GraphQL

```javascript
const { rest } = require("msw");
const { setupServer } = require("msw/node");

const server = setupServer(
  // Mock GET request
  rest.get("/api/external/weather", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({ temperature: 25, condition: "sunny" }),
    );
  }),

  // Mock POST request
  rest.post("/api/external/payment", (req, res, ctx) => {
    const { amount } = req.body;

    if (amount > 10000) {
      return res(ctx.status(400), ctx.json({ error: "Amount too high" }));
    }

    return res(
      ctx.status(200),
      ctx.json({ transactionId: "txn-123", status: "success" }),
    );
  }),
);

// Enable API mocking
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```

---

## 📚 Glossary - คำศัพท์สำคัญ

| คำศัพท์                  | ความหมาย                                               | ตัวอย่าง                              |
| ------------------------ | ------------------------------------------------------ | ------------------------------------- |
| **Integration Testing**  | ทดสอบการทำงานร่วมกันของ modules หลายๆ ตัว              | ทดสอบ UserService + Database          |
| **Stub**                 | Module หลอม (fake) ที่ return data แต่ไม่มี logic จริง | `{ id: 1, name: 'Mock' }`             |
| **Driver**               | Code/test ที่เรียกใช้ module เพื่อทดสอบมัน             | Test file ที่เรียก `service.create()` |
| **Mock**                 | Object หลอมที่คาดการณ์ input/output ของ dependency     | MSW mocking SendGrid API              |
| **Contract**             | สัญญา format ระหว่าง producer และ consumer             | API response schema                   |
| **JSON Schema**          | ภาษาบรรยาย structure ของ JSON data                     | วิธีระบุว่า `id` ต้องเป็น number      |
| **MSW**                  | Mock Service Worker - library สำหรับ mock HTTP         | Intercept axios calls                 |
| **Graceful Degradation** | ระบบยังทำงานได้แม้บาง feature ล้มเหลว                  | ยืมสำเร็จแม้ส่งอีเมลล้มเหลว           |

---

## 🎓 สรุป

### Integration Testing Key Points:

1. **Purpose:** ทดสอบการทำงานร่วมกันของ modules หลายๆ ตัว

2. **Integration Strategies:**
   - Big Bang, Top-Down, Bottom-Up, Sandwich
   - ในโลกจริง: ขึ้นกับ architecture และใช้ CI/CD

3. **Database Integration:**
   - ใช้ test database แยก
   - Clean data ก่อน/หลังแต่ละ test
   - ทดสอบ constraints และ transactions

4. **External API Integration:**
   - Mock ด้วย MSW สำหรับ local dev
   - ทดสอบ error scenarios
   - Handle failures gracefully

5. **API Contract Testing:**
   - ใช้ JSON Schema validate API responses
   - รับประกันความถูกต้องของ API format
   - สำคัญสำหรับ microservices

### Tools Recap:

- **supertest** - HTTP testing
- **msw** - API mocking
- **Ajv** - JSON Schema validation
- **Jest** - Test runner

---

## 💡 อย่าลืม:

**Integration Testing**

- ทดสอบการเชื่อมต่อระหว่าง parts
- ใช้ real database (test DB)
- Mock external APIs (MSW)

**API Contract Testing**

- Verify response format
- ไม่ใช่ business logic testing
- ใช้ JSON Schema

**Best Practices**

- Clean data before/after tests
- Test error scenarios
- Graceful degradation

---
