Trang chủ · Blog về kiểm thử ·

Playwright cho tester: tự động hóa UI và API từ đầu

Automated test đầu tiên hữu ích không chỉ click button: nó tự tạo data, kiểm tra business outcome và để lại trace dễ hiểu khi fail. Bài này chuyển manual scenario thành Playwright suite có thể bảo trì.

QA engineer xem Playwright test, API request, trace và kết quả CI
Vòng đời automated test ổn định
  1. Chọn risk và oracle
  2. Chuẩn bị state qua API
  3. Thực hiện user journey
  4. Lưu evidence và cleanup
Đặt mỗi risk ở layer phù hợp nhất
Risk Layer tốt nhất Lý do
Công thức discountUnit/APIBao phủ nhiều data nhanh
Quyền với object của người khácAPI + một UI pathKiểm tra server boundary và UX
Customer checkoutUI end-to-endKiểm tra tích hợp critical path
Response schemaAPI contractChẩn đoán nhanh và chính xác
Layout và accessibilityUI + visual/a11yCần giao diện đã render

1. Chọn scenario đầu tiên theo risk, không theo sự tiện lợi

Bắt đầu với path ngắn, ổn định và oracle rõ: login, tạo customer hoặc thêm sản phẩm vào cart. Không bắt đầu bằng order flow dài với nhiều integration. Ghi manual scenario, precondition, data, business effect expected và cleanup.

Phân bố check theo layer. Calculation và rule rẻ hơn ở dưới UI; chỉ dùng browser cho behavior thật sự cần page. Một end-to-end test không thay được API, permission và persistence test, còn hàng chục UI variant làm suite chậm và dễ vỡ.

  • Scenario có business risk.
  • Oracle quan sát được bởi người khác.
  • Data có thể tạo và xóa độc lập.
  • Test không phụ thuộc kết quả test khác.
  • Failure được định vị bằng step và evidence.
Bài thực hành

Với CRM, chọn tạo customer, bulk import hoặc report phức tạp. Giải thích bằng risk, data, stability và diagnostics.

2. Viết test tối thiểu theo Arrange, Act và Assert

Playwright Test có runner, assertion, fixture, parallelism và report. Cấu trúc scenario thành Arrange, Act, Assert: chuẩn bị user và data, thực hiện một business action, kiểm tra outcome quan sát được. Tên test mô tả rule, không phải chuỗi click.

Dùng web-first assertion để chờ state cần thiết trong timeout và báo actual. Không thay bằng waitForTimeout: sleep cố định làm chậm test nhanh nhưng vẫn quá ngắn trên CI chậm.

Test đầu tiên có business meaning
import { test, expect } from '@playwright/test';

test('manager converts a new lead into one customer', async ({ page }) => {
  await page.goto('/leads/42');
  await page.getByRole('button', { name: 'Convert lead' }).click();
  await expect(page.getByRole('heading', { name: 'Customer Alex' })).toBeVisible();
  await expect(page.getByText('Converted')).toBeVisible();
});
Bài thực hành

Viết lại manual login scenario theo Arrange, Act, Assert. Nêu oracle cho success và oracle riêng cho invalid password.

3. Chọn locator theo user-facing contract

Ưu tiên getByRole, getByLabel, getByText và getByTestId như fallback được thỏa thuận. Role và accessible name phản ánh điều user và assistive technology thấy. CSS dài theo div:nth-child sẽ hỏng sau thay đổi markup không liên quan behavior.

Locator phải duy nhất. Nếu có nhiều nút Save, hãy thu hẹp region hoặc dùng tên phân biệt; không gọi first() chỉ để test pass. Thiếu accessible name có thể là defect sản phẩm chứ không phải bất tiện selector.

Locator ổn định và dễ vỡ
// Ổn định: ý nghĩa với user
await page.getByRole('button', { name: 'Save customer' }).click();
await page.getByLabel('Email').fill('alex@example.test');

// Dễ vỡ: cấu trúc DOM
await page.locator('.card:nth-child(2) > div > button').click();
Bài thực hành

Chọn locator cho customer name, email, owner và Save. Giải thích defect accessibility mà mỗi lựa chọn có thể phát hiện.

4. Cô lập data bằng fixture và API setup

Test không được phụ thuộc thứ tự. Mỗi test có user hoặc business context riêng và chỉ tạo data cần thiết. Tạo lead qua API nhanh và đáng tin hơn lặp UI form trước mỗi check. Giữ UI cho behavior đang test.

Fixture đóng gói setup và teardown nhưng không nên che nửa business flow. Tạo data unique cho parallel worker và xóa bằng API được hỗ trợ hoặc TTL. Shared account nhanh chóng gây race và false failure.

API setup rồi assertion qua UI
test('owner sees the assigned customer', async ({ page, request }) => {
  const created = await request.post('/api/test/customers', {
    data: { name: 'Alex', owner: 'manager-a' }
  });
  const { id } = await created.json();
  await page.goto(`/customers/${id}`);
  await expect(page.getByText('Owner: manager-a')).toBeVisible();
});
Bài thực hành

Thiết kế fixture cho hai manager và customer A. Nêu unique data, authentication state, cleanup và parallel execution.

5. Dùng APIRequestContext cho setup và API test độc lập

Playwright gửi được HTTP request trong cùng runner. Dùng nó để arrange state, kiểm tra server contract hoặc xác nhận UI action qua channel khác. Status 200 không chứng minh business success: cần assert schema, value, persistence, permission và side effect.

API test phải sở hữu baseURL, auth và data rõ ràng. Tránh chain một test tạo state cho test sau. Với negative case, xác nhận không mutate sau 4xx; với retry, assert chỉ một effect cuối theo business key.

API test kiểm tra state đã lưu
test('duplicate conversion creates one customer', async ({ request }) => {
  const first = await request.post('/api/leads/42/convert');
  const second = await request.post('/api/leads/42/convert');
  expect([200, 201]).toContain(first.status());
  expect([200, 409]).toContain(second.status());
  const customers = await request.get('/api/customers?source_lead_id=42');
  expect((await customers.json()).data).toHaveLength(1);
});
Bài thực hành

Thêm check response, follow-up GET, stock và duplicate retry cho POST /orders. Chọn assertion ở API và một UI smoke.

6. Chỉ thêm Page Object khi có lặp thực sự

Page Object lưu locator và page action nhưng không nên thành class khổng lồ chứa mọi assertion. Test nên thể hiện business intent, component che cơ chế ổn định. Table hoặc modal component dùng lại thường chính xác hơn object cả page.

Tránh abstraction quá sớm. Ba dòng rõ ràng có thể tốt hơn method chung với mười flag. Tách data, action và assertion, dùng TypeScript type, không nuốt error hay retry trong helper làm che defect.

Object nhỏ với user action
class LeadPage {
  constructor(private page: Page) {}
  async convert() {
    await this.page.getByRole('button', { name: 'Convert lead' }).click();
  }
  customerHeading(name: string) {
    return this.page.getByRole('heading', { name });
  }
}
Bài thực hành

Xem ba CRM scenario và tìm component dùng lại. Không tạo method chung nếu parameter làm đổi ý nghĩa action.

7. Dùng network control, visual check và accessibility có chủ đích

Route interception giúp tái hiện 500, timeout hoặc response hiếm của dependency, nhưng happy path mock không chứng minh integration. Tách API contract test thật khỏi UI behavior với failure được kiểm soát. Assert loading, retry, không duplicate action và message hữu ích cho user.

Visual comparison tìm pixel change bất ngờ nhưng cần font, viewport và data ổn định. Accessibility scan tìm class vi phạm nhưng không thay keyboard navigation và screen-reader semantics. Các kỹ thuật này bổ sung functional assertion chứ không thay thế.

API failure được kiểm soát trong UI test
await page.route('**/api/customers/42', route =>
  route.fulfill({ status: 503, body: JSON.stringify({ error: 'unavailable' }) })
);
await page.goto('/customers/42');
await expect(page.getByRole('alert')).toContainText('Try again');
await expect(page.getByRole('button', { name: 'Retry' })).toBeEnabled();
Bài thực hành

Thiết kế UI test cho 200, response chậm và 503. Nêu mock, UX expected và nơi test real contract riêng.

8. Chẩn đoán flaky test bằng trace và chạy trong CI

Flaky test lúc pass lúc fail với cùng code và expected state. Nguyên nhân thường là shared data, wait sai, race, external dependency, locator dễ vỡ và state leak. Retry giúp lấy signal thứ hai nhưng không sửa nguyên nhân hay biến suite đỏ thành khỏe.

Trong CI lưu trace on-first-retry, screenshot/video theo policy, HTML/JUnit report và network evidence đã làm sạch. Tách smoke nhanh cho pull request khỏi regression theo lịch hoặc trước release. Failure cần chỉ ra commit, browser, environment, test-data id và stack frame hữu ích đầu tiên.

Cấu hình cơ bản để chẩn đoán
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  use: { baseURL: process.env.BASE_URL, trace: 'on-first-retry' },
  reporter: [['html'], ['junit', { outputFile: 'results.xml' }]],
});
Bài thực hành

Test fail 2/20 lần trên CI. Lập kế hoạch evidence, reproduce, hypothesis, experiment và criterion xác nhận đã fix.

Câu hỏi thường gặp về Playwright

Nên dùng ngôn ngữ nào cho Playwright?

TypeScript phổ biến trong web nhờ hệ sinh thái và type, nhưng ngôn ngữ của team và khả năng bảo trì quan trọng hơn. Playwright cũng hỗ trợ Python, Java và .NET.

Có nên automation mọi manual test case không?

Không. Hãy automation check ổn định, lặp lại và có signal giá trị. Scenario exploratory, thay đổi nhanh hoặc rất hiếm có thể giữ manual.

Có thể dùng waitForTimeout không?

Có thể dùng tạm khi debug, nhưng trong suite nó thường là dấu hiệu sync sai. Hãy chờ UI state, response hoặc event cụ thể.

Automated test tốt là gì?

Nó bảo vệ rule quan trọng, độc lập, tái hiện được, ổn định, dễ chẩn đoán và chi phí bảo trì thấp hơn giá trị regression signal.

Playwright trở thành công cụ QA chuyên nghiệp khi test thể hiện product risk, sở hữu data và giải thích failure mà không cần chạy lại toàn bộ flow bằng tay.

Nhận quyền truy cập miễn phí vào bài thực hành QA Bài tiếp theo

← Tất cả bài viết