Section 04 · Module 05 · Lesson 02 · ~15 นาที

Before/After Hooks

เวลาเรียน: 15 นาที ต้องรู้มาก่อน: Module 05, Lesson 01: Test Fixtures รูปแบบ: อ่าน + แบบฝึกสั้น

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

  • ใช้ beforeEach, afterEach, beforeAll, afterAll ได้ถูกต้อง
  • จัดกลุ่ม test ด้วย test.describe()
  • ออกแบบ setup/teardown patterns ที่ดี
  • refactor duplicated setup ให้ใช้ hooks
1 · Hooks คืออะไร?

Hooks คือ function ที่รันก่อนหรือหลัง test — ใช้สำหรับ setup (จัดเตรียม) และ teardown (ทำความสะอาด)

Hook รันเมื่อ ใช้สำหรับ
beforeEach ก่อนแต่ละ test navigate, reset state
afterEach หลังแต่ละ test cleanup, take screenshot
beforeAll ก่อน test ตัวแรกใน group สร้าง test data, seed database
afterAll หลัง test ตัวสุดท้ายใน group ลบ test data, close connection
2 · beforeEach / afterEach
import { test, expect } from '@playwright/test';

test.describe('Product Page', () => {
  test.beforeEach(async ({ page }) => {
    // รันก่อนทุก test — ไปที่หน้า products
    await page.goto('/products');
  });

  test.afterEach(async ({ page }) => {
    // รันหลังทุก test (ไม่ค่อยจำเป็น เพราะ page ถูกสร้างใหม่ทุก test)
    console.log(`Test finished: ${test.info().title}`);
  });

  test('should display product list', async ({ page }) => {
    // page อยู่ที่ /products แล้ว
    await expect(page.getByRole('listitem')).toHaveCount(10);
  });

  test('should filter by category', async ({ page }) => {
    // page อยู่ที่ /products แล้ว
    await page.getByRole('combobox', { name: 'Category' }).selectOption('Electronics');
    await expect(page.getByRole('listitem').first()).toContainText('Electronics');
  });
});
3 · beforeAll / afterAll
import { test, expect } from '@playwright/test';

test.describe('Order Management', () => {
  let orderId: string;

  test.beforeAll(async ({ request }) => {
    // สร้าง test data ผ่าน API ก่อนเริ่ม test ทั้งหมด
    const response = await request.post('/api/orders', {
      data: { product: 'Test Product', quantity: 1 }
    });
    const data = await response.json();
    orderId = data.id;
  });

  test.afterAll(async ({ request }) => {
    // ลบ test data หลัง test ทั้งหมดจบ
    await request.delete(`/api/orders/${orderId}`);
  });

  test('should display order details', async ({ page }) => {
    await page.goto(`/orders/${orderId}`);
    await expect(page.getByText('Test Product')).toBeVisible();
  });

  test('should update order status', async ({ page }) => {
    await page.goto(`/orders/${orderId}`);
    await page.getByRole('button', { name: 'Ship' }).click();
    await expect(page.getByText('Shipped')).toBeVisible();
  });
});

สิ่งสำคัญ: beforeAll/afterAll ได้รับ request fixture (ไม่ใช่ page) เพราะรันนอก context ของแต่ละ test

4 · test.describe() — จัดกลุ่ม Test
test.describe('Shopping Cart', () => {
  test.describe('when cart is empty', () => {
    test.beforeEach(async ({ page }) => {
      await page.goto('/cart');
    });

    test('should show empty message', async ({ page }) => {
      await expect(page.getByText('Your cart is empty')).toBeVisible();
    });

    test('should hide checkout button', async ({ page }) => {
      await expect(page.getByRole('button', { name: 'Checkout' })).toBeHidden();
    });
  });

  test.describe('when cart has items', () => {
    test.beforeEach(async ({ page }) => {
      // เพิ่มสินค้าก่อน
      await page.goto('/products');
      await page.getByRole('button', { name: 'Add to Cart' }).first().click();
      await page.goto('/cart');
    });

    test('should show item count', async ({ page }) => {
      await expect(page.getByRole('listitem')).toHaveCount(1);
    });

    test('should enable checkout button', async ({ page }) => {
      await expect(page.getByRole('button', { name: 'Checkout' })).toBeEnabled();
    });
  });
});
5 · Setup/Teardown Patterns

Pattern 1: API-based setup (แนะนำ)

test.beforeEach(async ({ request, page }) => {
  // สร้าง data ผ่าน API (เร็วกว่าผ่าน UI)
  await request.post('/api/reset-test-data');
  await page.goto('/dashboard');
});

Pattern 2: Data cleanup after test

test.afterEach(async ({ request }) => {
  await request.delete('/api/test-data/cleanup');
});

Pattern 3: Conditional setup

test.beforeEach(async ({ page }, testInfo) => {
  // setup เฉพาะ test ที่ต้องการ login
  if (testInfo.title.includes('[auth]')) {
    await page.goto('/login');
    // login...
  }
});
6 · ลำดับการรัน Hooks
beforeAll (outer describe)
  beforeAll (inner describe)
    beforeEach (outer)
      beforeEach (inner)
        TEST
      afterEach (inner)
    afterEach (outer)
  afterAll (inner describe)
afterAll (outer describe)
แบบฝึกหัด

Refactor test suite ด้านล่างที่มี duplicated setup ให้ใช้ hooks:

Before (duplicated setup)

test('should add item to cart', async ({ page }) => {
  await page.goto('/products');
  await page.getByRole('button', { name: 'Add to Cart' }).first().click();
  await expect(page.getByTestId('cart-count')).toHaveText('1');
});

test('should show product details', async ({ page }) => {
  await page.goto('/products');
  await page.getByRole('link', { name: 'iPhone 15' }).click();
  await expect(page.getByRole('heading', { name: 'iPhone 15' })).toBeVisible();
});

test('should filter products', async ({ page }) => {
  await page.goto('/products');
  await page.getByRole('combobox', { name: 'Category' }).selectOption('Phones');
  await expect(page.getByRole('listitem').first()).toContainText('Phone');
});
ดูคำตอบแนะนำ

After (ใช้ hooks)

test.describe('Products Page', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/products');
  });

  test('should add item to cart', async ({ page }) => {
    await page.getByRole('button', { name: 'Add to Cart' }).first().click();
    await expect(page.getByTestId('cart-count')).toHaveText('1');
  });

  test('should show product details', async ({ page }) => {
    await page.getByRole('link', { name: 'iPhone 15' }).click();
    await expect(page.getByRole('heading', { name: 'iPhone 15' })).toBeVisible();
  });

  test('should filter products', async ({ page }) => {
    await page.getByRole('combobox', { name: 'Category' }).selectOption('Phones');
    await expect(page.getByRole('listitem').first()).toContainText('Phone');
  });
});

สิ่งที่เปลี่ยน:

  1. ย้าย page.goto('/products') ที่ซ้ำกันทุก test ไป beforeEach
  2. ครอบทั้งหมดด้วย test.describe() เพื่อจัดกลุ่ม
  3. แต่ละ test สั้นลงและอ่านง่ายขึ้น

สรุปสิ่งที่ต้องจำ

  • beforeEach ใช้บ่อยที่สุด — สำหรับ navigate, reset state ก่อนทุก test
  • beforeAll/afterAll สำหรับ setup/teardown ที่ทำครั้งเดียว (seed data, cleanup)
  • test.describe() จัดกลุ่ม test ที่เกี่ยวข้อง — hooks ใน describe รันเฉพาะ test ใน group นั้น
  • ใช้ API-based setup (request) แทน UI-based setup เมื่อเป็นไปได้ — เร็วกว่ามาก
  • อย่าลืม cleanup — test data ที่สร้างใน beforeAll ต้องลบใน afterAll
  • เมื่อ setup ซ้ำกัน → ย้ายไป hooks เพื่อลด duplication