Playwright เป็นเครื่องมือทดสอบเว็บที่ควบคุมเบราว์เซอร์จริงได้ และมี Playwright Test เป็นตัวรันเทสต์ที่รองรับการแยกข้อมูลระหว่างเทสต์ การรันขนาน รายงานผล และการเก็บหลักฐานเมื่อเกิดข้อผิดพลาด แต่เครื่องมือที่ดีไม่ได้ทำให้ทุกเทสต์เสถียรโดยอัตโนมัติ คุณภาพของชุดทดสอบขึ้นกับการเลือกสิ่งที่จะตรวจสอบ การจัดการข้อมูล และวิธีวิเคราะห์ความล้มเหลว

บทความนี้ใช้ TypeScript + Playwright Test เป็นตัวอย่าง โดยเริ่มจากเทสต์ที่ทดลองรันได้บน TodoMVC ของ Playwright แล้วอธิบายแนวทางนำไปใช้กับแอปของทีม

#1. วางขอบเขตให้ถูกก่อนเขียนเทสต์

เทสต์แบบ end-to-end (E2E) เหมาะกับเส้นทางสำคัญที่ผู้ใช้ทำจริง เช่น ล็อกอิน สร้างรายการ สั่งซื้อ หรือเปลี่ยนสิทธิ์ ส่วนเงื่อนไขจำนวนมากของฟังก์ชันเดียวควรตรวจด้วย unit หรือ integration test เพื่อให้ผลเร็วและวิเคราะห์ง่าย ไม่จำเป็นต้องจำลองทุกกรณีผ่าน UI

ระดับ ตัวอย่าง จุดที่เหมาะ
Unit คำนวณราคารวมและส่วนลด กฎธุรกิจที่มีกรณีทดสอบมาก
Integration/API POST แล้วตรวจข้อมูลในฐานข้อมูลหรือ API การเชื่อมต่อระหว่างส่วนประกอบ
Playwright E2E เพิ่มสินค้า → ชำระเงิน → เห็นคำสั่งซื้อ เส้นทางใช้งานสำคัญข้ามหลายชั้น

นิยาม ผลที่สังเกตได้ ของผู้ใช้ก่อนเสมอ เช่น “กดบันทึกแล้วรายการปรากฏและสถานะถูกต้อง” ไม่ใช่เพียง “คลิกปุ่มแล้วสคริปต์ไม่ล้ม”

#2. ติดตั้งและลองรัน

เตรียม Node.js และ npm แล้วสร้างหรือเปิดโปรเจกต์เว็บ:

npm init playwright@latest
npx playwright install
npx playwright test
npx playwright test --ui

ตัวช่วยตั้งค่าจะถามว่าจะใช้ TypeScript หรือ JavaScript, ตำแหน่งเก็บเทสต์ และการติดตั้งเบราว์เซอร์ หากติดตั้งผ่านตัวช่วยแล้ว อาจไม่ต้องรัน npx playwright install ซ้ำ คำสั่ง --ui เปิด UI Mode เพื่อดูและดีบักเทสต์อย่างเป็นขั้นตอน

#ตัวอย่างโครงสร้าง

project/
├── playwright.config.ts
├── tests/
│   ├── todo.spec.ts
│   ├── auth.setup.ts       # เพิ่มเมื่อมีระบบล็อกอิน
│   └── pages/              # เพิ่มเมื่อมีการใช้งานซ้ำจริง
├── playwright/.auth/       # ห้าม commit ไฟล์สถานะล็อกอิน
└── .github/workflows/playwright.yml

#3. ตั้งค่าให้เทสต์ทำซ้ำและวิเคราะห์ผลได้

ไฟล์ playwright.config.ts ตัวอย่างนี้ใช้ เว็บไซต์สาธิต เพื่อให้คัดลอกเทสต์ในหัวข้อถัดไปมาลองรันได้ทันที:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  expect: { timeout: 5_000 },
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [['list'], ['html', { open: 'never' }]],
  use: {
    baseURL: 'https://demo.playwright.dev',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

retries: 1 ช่วยเก็บหลักฐานของการรันซ้ำ ไม่ใช่วิธีแก้เทสต์ที่ไม่เสถียร; หากเทสต์ผ่านเฉพาะตอน retry ให้สืบหาสาเหตุ เช่น ข้อมูลชนกัน การรอผิดเงื่อนไข หรือบริการภายนอกไม่แน่นอน สำหรับ CI ที่ทรัพยากรจำกัด การเริ่มด้วย worker เดียวช่วยลดการแย่งทรัพยากร แล้วค่อยเพิ่มเมื่อชุดทดสอบแยกข้อมูลได้ดี เอกสาร configuration · CI

เมื่อนำไปใช้กับแอปของทีม ให้เปลี่ยน baseURL เป็น URL ของแอป และหากมีสคริปต์เปิดเซิร์ฟเวอร์ ให้เพิ่มตัวอย่างนี้ใน config (ปรับคำสั่ง/พอร์ตให้ตรงโปรเจกต์):

webServer: {
  command: 'npm run dev -- --host 127.0.0.1',
  url: 'http://127.0.0.1:3000',
  reuseExistingServer: !process.env.CI,
  timeout: 120_000,
},
use: {
  baseURL: 'http://127.0.0.1:3000',
  trace: 'on-first-retry',
  screenshot: 'only-on-failure',
},

บล็อกหลังเป็น ตัวอย่างค่าที่ใช้แทนส่วน use ของ config เดิม และเพิ่ม webServer ในระดับบนของ config ไม่ได้หมายถึงการวาง use ซ้ำสองครั้งในไฟล์เดียว เอกสาร webServer

#4. เขียนเทสต์ที่มีความหมายและแยกจากกัน

สร้าง tests/todo.spec.ts:

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

test('เพิ่มงานและทำเครื่องหมายว่าเสร็จ', async ({ page }) => {
  await page.goto('/todomvc/');

  const newTodo = page.getByPlaceholder('What needs to be done?');
  await newTodo.fill('Review pull request');
  await newTodo.press('Enter');

  const item = page.locator('.todo-list li').filter({
    hasText: 'Review pull request',
  });
  await expect(item).toHaveCount(1);

  await item.getByRole('checkbox').check();
  await expect(item.getByRole('checkbox')).toBeChecked();
});

test('เริ่มแต่ละเทสต์โดยไม่มีงานค้างจากเทสต์อื่น', async ({ page }) => {
  await page.goto('/todomvc/');
  await expect(page.locator('.todo-list li')).toHaveCount(0);
});

ลองรันเฉพาะ Chromium ก่อน:

npx playwright test tests/todo.spec.ts --project=chromium
npx playwright show-report

Playwright Test สร้าง browser context แยกให้แต่ละเทสต์ ทำให้ cookies และ storage ไม่ไหลข้ามเทสต์ตามค่าเริ่มต้น อย่างไรก็ดี ข้อมูลฝั่งเซิร์ฟเวอร์ยังอาจใช้ร่วมกัน: ถ้าเทสต์สร้างข้อมูลในฐานข้อมูลเดียวกัน ให้ใช้บัญชี/ข้อมูลเฉพาะเทสต์หรือ fixture สำหรับเตรียมและล้างข้อมูล อย่าพึ่งลำดับการรัน เอกสาร isolation

ตัวอย่าง TodoMVC ใช้ CSS เพียงเพื่อกำหนดขอบเขตรายการ (.todo-list li) แล้วใช้ locator ตามความหมายของ UI สำหรับ checkbox ในแอปของทีมควรให้ฟอร์มและปุ่มมีชื่อที่เข้าถึงได้ และใช้ getByRole, getByLabel หรือ getByTestId ที่เป็นข้อตกลงชัดเจน

#5. Locator และการรอ: กฎที่ลด flaky tests

ให้เรียงความสำคัญของตัวระบุองค์ประกอบตามสิ่งที่ผู้ใช้รับรู้ แล้วเลือกตัวที่เฉพาะเจาะจง:

page.getByRole('button', { name: 'บันทึก' });
page.getByLabel('อีเมล');
page.getByPlaceholder('ค้นหาสินค้า');
page.getByTestId('order-status'); // เมื่อชื่อที่ผู้ใช้เห็นไม่เพียงพอ

หลีกเลี่ยง CSS/XPath ที่ยาวตามโครงสร้าง DOM และการใช้ .first() เพื่อกลบปัญหา locator จับหลายองค์ประกอบ ควรเจาะจงขอบเขตด้วย getByRole(...).filter(...) หรือ locator ของ container แทน เอกสาร locators

// เปราะ: เวลาเครื่องแต่ละเครื่องไม่เท่ากัน
await page.waitForTimeout(3000);

// ดีกว่า: รอผลลัพธ์ที่ผู้ใช้มองเห็น
await expect(page.getByRole('status')).toHaveText('บันทึกแล้ว');

การกระทำอย่าง click() และ fill() มีการรอให้องค์ประกอบพร้อมใช้งาน ส่วน await expect(locator).toHaveText(...) จะตรวจซ้ำภายในเวลาที่กำหนด หากจำเป็นต้องรอ network ให้รอ คำขอที่เจาะจงและผลใน UI ไม่ควรใช้เวลาหน่วงคงที่เพื่อหวังว่าระบบจะเสร็จ เอกสาร assertions

#6. แยกข้อมูลทดสอบและจัดการล็อกอิน

  • สร้างข้อมูลด้วย API หรือ fixture ก่อนเทสต์ หากการผ่าน UI หลายหน้ามิใช่สิ่งที่ต้องการตรวจในกรณีนั้น
  • ใช้ ID หรือชื่อข้อมูลเฉพาะต่อเทสต์/worker เพื่อไม่ให้การรันขนานชนกัน และลบข้อมูลทิ้งหลังทดสอบเมื่อทำได้
  • สำหรับกรณีที่ต้องล็อกอินซ้ำ ใช้ setup project เพื่อสร้าง storageState และให้ project ที่ต้องการขึ้นต่อ setup project
  • เพิ่ม playwright/.auth/ ใน .gitignore เพราะไฟล์สถานะอาจมี cookies หรือข้อมูลที่ใช้สวมสิทธิ์
  • หากเทสต์หลายตัวแก้ข้อมูลฝั่งเซิร์ฟเวอร์ผ่านบัญชีเดียวกัน ให้ใช้บัญชีแยกตาม worker หรือเตรียมข้อมูลแยก ไม่ควรใช้สถานะร่วมที่เปลี่ยนได้

ตัวอย่างแนวคิดใน tests/auth.setup.ts สำหรับ แอปของคุณ (ต้องปรับ URL และชื่อช่องฟอร์ม):

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

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('อีเมล').fill(process.env.E2E_USER!);
  await page.getByLabel('รหัสผ่าน').fill(process.env.E2E_PASSWORD!);
  await page.getByRole('button', { name: 'เข้าสู่ระบบ' }).click();
  await expect(page.getByRole('heading', { name: 'แดชบอร์ด' })).toBeVisible();
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});

กำหนด E2E_USER และ E2E_PASSWORD จาก secret ของ CI ก่อนรัน ตรวจว่ามีไฟล์สถานะก่อนให้ project อื่นใช้งาน และอย่าใส่ค่าจริงในซอร์สโค้ด ดู ตัวอย่าง setup project และ dependencies ฉบับเต็ม

#7. Mock เมื่อโจทย์ต้องการควบคุมเงื่อนไข

การจำลองคำตอบ API มีประโยชน์กับกรณีผิดพลาดหรือ edge case ที่สร้างบนระบบจริงยาก เช่น บริการช้า ไม่มีสินค้า หรือ HTTP 500:

test('แสดงข้อความเมื่อโหลดสินค้าไม่สำเร็จ', async ({ page }) => {
  await page.route('**/api/products', route =>
    route.fulfill({ status: 500, json: { error: 'server error' } })
  );
  await page.goto('/products');
  await expect(page.getByRole('alert')).toContainText('โหลดสินค้าไม่สำเร็จ');
});

ตัวอย่างนี้ใช้กับ แอปสมมติ ที่เรียก /api/products และแสดงข้อความตามที่ระบุ ให้ลงทะเบียน route ก่อน goto() แต่ต้องมีเทสต์บางส่วนที่ต่อกับ backend จริงด้วย เพราะ mock ไม่สามารถพิสูจน์ว่า frontend และ API จริงทำงานร่วมกันถูกต้อง เอกสาร mock API

#8. แก้เทสต์ที่ล้มด้วยหลักฐาน

เมื่อล้มในเครื่องนักพัฒนา ให้รันเฉพาะเคสด้วย --debug หรือเปิด UI Mode; เมื่อล้มใน CI ให้ดู HTML report และ trace ซึ่งแสดงขั้นตอน, DOM snapshot และ network ของเทสต์ที่ retry:

npx playwright test tests/todo.spec.ts --project=chromium --debug
npx playwright test --ui
npx playwright show-report

trace: 'on-first-retry' จะเก็บ trace เฉพาะการ retry ครั้งแรก ถ้าเทสต์ล้มครั้งแรกแต่ผ่าน retry ให้เปิด trace เพื่อวิเคราะห์ ถ้าต้องการหลักฐานของ การล้มครั้งแรก ด้วย ให้พิจารณา trace: 'retain-on-failure' ซึ่งใช้พื้นที่และเวลามากขึ้น เอกสาร trace

ระวังการอัปโหลด report หรือ trace ที่มีข้อมูลส่วนบุคคล โทเคน หรือข้อมูลในฟอร์ม ให้ใช้พื้นที่เก็บ artifact ที่เข้าถึงได้เฉพาะทีมและกำหนดอายุการเก็บ

#9. นำชุดทดสอบเข้ากระบวนการ CI

เมื่อมี package-lock.json แล้ว สามารถเริ่มด้วย .github/workflows/playwright.yml:

name: Playwright Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v5
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

ตัวอย่างนี้รันกับเว็บไซต์สาธิตตาม config ด้านบน ใช้เพื่อเรียนรู้เท่านั้น: ชุดทดสอบจริงควรทดสอบแอปที่ทีมควบคุมเอง ตั้งค่า webServer หรือบริการทดสอบของ CI ให้พร้อม และกำหนด secrets ตามจำเป็น เริ่มจาก Chromium สำหรับการตรวจทุก pull request แล้วเพิ่ม Firefox/WebKit ตามความเสี่ยงของผลิตภัณฑ์; ปรับ worker หรือแบ่ง shard เมื่อข้อมูลแยกกันดีและเวลารันเริ่มสูง เอกสาร CI

#10. ความสามารถเพิ่มเติมที่ควรใช้ตามโจทย์

ความสามารถ ใช้เมื่อ ข้อควรจำ
API testing (request) เตรียม/ตรวจข้อมูลและทดสอบ endpoint แยกคำรับรองของ API ออกจาก UI
Page Object / fixtures มีขั้นตอนหรือ locator ใช้ซ้ำหลายเทสต์ สร้าง abstraction เท่าที่ช่วยให้อ่านเทสต์ง่าย
Visual comparisons หน้าตาหรือ layout เป็นส่วนสำคัญ ควบคุม browser, OS, ฟอนต์ และข้อมูลที่เปลี่ยนได้
Accessibility + axe ตรวจปัญหาที่เครื่องมือตรวจได้ ต้องมีการตรวจด้วยมนุษย์เพิ่มเติม

อ่านเพิ่ม: API testing, fixtures, Page Object Model, visual comparisons, accessibility testing

#Checklist ก่อน merge

  • [ ] เทสต์ระบุพฤติกรรมและตรวจผลลัพธ์ที่ผู้ใช้เห็น
  • [ ] Locator มีความหมายและชี้ไปยังองค์ประกอบเดียว
  • [ ] ไม่ใช้ waitForTimeout() เพื่อซ่อนปัญหาการรอ
  • [ ] ข้อมูลและบัญชีของเทสต์ไม่ชนกันเมื่อรันขนาน
  • [ ] ไม่ commit secret หรือ storageState
  • [ ] วิเคราะห์ความล้มเหลวด้วย report/trace ไม่เพิ่ม retry เพื่อให้ไฟเขียวอย่างเดียว
  • [ ] CI รันกับสภาพแวดล้อมที่ควบคุมได้ และจำกัดการเข้าถึง artifact

สรุป: การทดสอบ Playwright อย่างมืออาชีพคือการเลือกเส้นทางสำคัญ เขียน assertion จากผลจริงของผู้ใช้ แยกข้อมูลให้เทสต์อิสระ และเก็บหลักฐานที่เพียงพอสำหรับแก้ความล้มเหลว เมื่อทำครบ ชุดทดสอบจึงเป็นสัญญาณที่ทีมเชื่อถือได้ก่อนปล่อยซอฟต์แวร์

#เอกสารอ้างอิง

  1. Playwright, Installation, Best Practices
  2. Playwright, Locators, Assertions, Isolation
  3. Playwright, Authentication, Mock APIs, Trace Viewer
  4. Playwright, Continuous Integration, Accessibility testing