- 1. วางขอบเขตให้ถูกก่อนเขียนเทสต์
- 2. ติดตั้งและลองรัน
- 3. ตั้งค่าให้เทสต์ทำซ้ำและวิเคราะห์ผลได้
- 4. เขียนเทสต์ที่มีความหมายและแยกจากกัน
- 5. Locator และการรอ: กฎที่ลด flaky tests
- 6. แยกข้อมูลทดสอบและจัดการล็อกอิน
- 7. Mock เมื่อโจทย์ต้องการควบคุมเงื่อนไข
- 8. แก้เทสต์ที่ล้มด้วยหลักฐาน
- 9. นำชุดทดสอบเข้ากระบวนการ CI
- 10. ความสามารถเพิ่มเติมที่ควรใช้ตามโจทย์
- Checklist ก่อน merge
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 จากผลจริงของผู้ใช้ แยกข้อมูลให้เทสต์อิสระ และเก็บหลักฐานที่เพียงพอสำหรับแก้ความล้มเหลว เมื่อทำครบ ชุดทดสอบจึงเป็นสัญญาณที่ทีมเชื่อถือได้ก่อนปล่อยซอฟต์แวร์
#เอกสารอ้างอิง
- Playwright, Installation, Best Practices
- Playwright, Locators, Assertions, Isolation
- Playwright, Authentication, Mock APIs, Trace Viewer
- Playwright, Continuous Integration, Accessibility testing