CupCode.cc
Section 04 · Module 06 · Lesson 02 · ~15–20 นาที

Mocking และ Intercepting Network Requests

เวลาเรียน: 15–20 นาที ต้องรู้มาก่อน: Lesson 01 (API Testing) รูปแบบ: อ่าน + Code Lab + แบบฝึก

จบบทนี้แล้วคุณจะ...

  • ใช้ page.route() เพื่อ intercept และ mock network requests
  • จำลอง API response สำหรับ error states, slow response, empty data
  • ทดสอบ UI error handling โดยไม่ต้องพึ่ง backend จริง
  • เข้าใจว่าเมื่อไหร่ควร mock เมื่อไหร่ควรใช้ API จริง
1 · ทำไมต้อง Mock?

ในโลกจริง backend อาจ:

  • ยังสร้างไม่เสร็จ
  • ไม่เสถียร (ล่มบ้าง ช้าบ้าง)
  • ยากที่จะจำลอง edge case (error 500, timeout, empty data)

Mock ช่วยให้เราควบคุม response ได้ 100% — test จึงเสถียรและเร็ว

2 · page.route() — อาวุธหลัก

page.route() ดักจับ request ที่ browser ส่งออกไป แล้วให้เรา "ตอบเอง" แทน server จริง:

import { test, expect } from '@playwright/test';

test('shows user list from mocked API', async ({ page }) => {
  // ดักจับ request ไปยัง /api/users
  await page.route('/api/users', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'สมชาย ใจดี' },
        { id: 2, name: 'สมหญิง เก่งมาก' }
      ])
    });
  });

  await page.goto('/users');
  await expect(page.locator('.user-item')).toHaveCount(2);
  await expect(page.locator('.user-item').first()).toContainText('สมชาย ใจดี');
});

Pattern ที่ใช้:

  • /api/users — glob pattern จะ match URL ใดก็ได้ที่ลงท้ายด้วย /api/users
  • route.fulfill() — ตอบ response เองโดยไม่ส่งไปถึง server จริง
3 · Mock Error States — ทดสอบ UI เมื่อ API พัง
test('shows error message when API returns 500', async ({ page }) => {
  await page.route('**/api/users', async (route) => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Internal Server Error' })
    });
  });

  await page.goto('/users');
  await expect(page.locator('.error-banner')).toBeVisible();
  await expect(page.locator('.error-banner')).toContainText('เกิดข้อผิดพลาด');
});

นี่คือ test ที่ยากมากถ้าต้องทำกับ backend จริง — แต่ mock ทำได้ใน 3 บรรทัด

4 · จำลอง Slow Response
test('shows loading spinner during slow API', async ({ page }) => {
  await page.route('**/api/users', async (route) => {
    // หน่วงเวลา 3 วินาที
    await new Promise(resolve => setTimeout(resolve, 3000));
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([{ id: 1, name: 'สมชาย' }])
    });
  });

  await page.goto('/users');

  // ระหว่างรอ ต้องเห็น loading
  await expect(page.locator('.loading-spinner')).toBeVisible();

  // หลังโหลดเสร็จ loading หายไป
  await expect(page.locator('.loading-spinner')).toBeHidden({ timeout: 5000 });
  await expect(page.locator('.user-item')).toHaveCount(1);
});
5 · Empty Data — ทดสอบ empty state
test('shows empty state when no data', async ({ page }) => {
  await page.route('**/api/users', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([])
    });
  });

  await page.goto('/users');
  await expect(page.locator('.empty-state')).toBeVisible();
  await expect(page.locator('.empty-state')).toContainText('ไม่พบข้อมูล');
});
6 · Modify Request — เปลี่ยน request ก่อนส่ง
test('add auth header to all API requests', async ({ page }) => {
  await page.route('/api/', async (route) => {
    const headers = {
      ...route.request().headers(),
      'Authorization': 'Bearer fake-token-for-testing'
    };
    await route.continue({ headers });
  });

  await page.goto('/dashboard');
  // page จะส่ง request พร้อม auth header ที่เราเพิ่มให้
});

route.continue() vs route.fulfill():

  • continue() — ส่ง request ต่อไปยัง server จริง (แต่แก้ไข request ได้)
  • fulfill() — ตอบเองโดยไม่ส่งไปถึง server
7 · เมื่อไหร่ควร Mock vs ใช้ API จริง?
สถานการณ์ แนะนำ
ทดสอบ UI error handling Mock
ทดสอบ loading state Mock
Backend ยังไม่เสร็จ Mock
ทดสอบ edge case ที่สร้างจริงยาก Mock
ทดสอบ end-to-end flow ทั้งระบบ API จริง
ทดสอบ integration ระหว่าง frontend-backend API จริง

หลักการ: Mock เพื่อ isolate สิ่งที่ต้องการทดสอบ — ไม่ใช่เพื่อหลีกเลี่ยง backend

แบบฝึกหัด: สร้าง Mock สำหรับ Scenarios ต่างๆ

สถานการณ์: คุณมีหน้า Product List ที่เรียก GET /api/products และแสดงผลดังนี้:

  • สินค้ามี → แสดง list
  • สินค้าไม่มี → แสดง "ยังไม่มีสินค้า"
  • API error → แสดง "โหลดไม่สำเร็จ กรุณาลองใหม่"
  • โหลดช้า → แสดง skeleton loading

โจทย์: เขียน 4 tests ครอบคลุมทั้ง 4 scenarios

ดูคำตอบแนะนำ
import { test, expect } from '@playwright/test';

test.describe('Product List - Network Scenarios', () => {

  test('displays product list when API returns data', async ({ page }) => {
    await page.route('/api/products', async (route) => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({
          data: [
            { id: 1, name: 'กาแฟดริป', price: 120 },
            { id: 2, name: 'ชาเขียว', price: 80 }
          ]
        })
      });
    });

    await page.goto('/products');
    await expect(page.locator('.product-card')).toHaveCount(2);
    await expect(page.locator('.product-card').first()).toContainText('กาแฟดริป');
    await expect(page.locator('.product-card').first()).toContainText('120');
  });

  test('displays empty state when no products', async ({ page }) => {
    await page.route('/api/products', async (route) => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({ data: [] })
      });
    });

    await page.goto('/products');
    await expect(page.locator('.empty-state')).toBeVisible();
    await expect(page.locator('.empty-state')).toContainText('ยังไม่มีสินค้า');
  });

  test('displays error message on API failure', async ({ page }) => {
    await page.route('/api/products', async (route) => {
      await route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ error: 'Server Error' })
      });
    });

    await page.goto('/products');
    await expect(page.locator('.error-message')).toBeVisible();
    await expect(page.locator('.error-message')).toContainText('โหลดไม่สำเร็จ');
    await expect(page.locator('.retry-button')).toBeVisible();
  });

  test('displays skeleton loading during slow response', async ({ page }) => {
    await page.route('/api/products', async (route) => {
      await new Promise(resolve => setTimeout(resolve, 3000));
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({ data: [{ id: 1, name: 'สินค้า', price: 100 }] })
      });
    });

    await page.goto('/products');

    // ระหว่างรอ ต้องเห็น skeleton
    await expect(page.locator('.skeleton-loading')).toBeVisible();

    // หลังโหลดเสร็จ skeleton หายไป
    await expect(page.locator('.skeleton-loading')).toBeHidden({ timeout: 5000 });
    await expect(page.locator('.product-card')).toHaveCount(1);
  });
});

สิ่งที่ต้องจำ

  • page.route() คือเครื่องมือหลักสำหรับ mock — ดักจับ request แล้วตอบเอง
  • route.fulfill() ตอบเอง, route.continue() ส่งต่อ (อาจแก้ไข)
  • Mock ช่วยทดสอบ edge cases ที่สร้างจริงยาก — error, slow, empty
  • อย่า mock ทุกอย่าง — ยังต้องมี test ที่ใช้ API จริงเพื่อตรวจ integration
  • Pattern: /api/ ใช้ glob matching — ยืดหยุ่นในการ match URL