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ì.
- Chọn risk và oracle
- Chuẩn bị state qua API
- Thực hiện user journey
- Lưu evidence và cleanup
| Risk | Layer tốt nhất | Lý do |
|---|---|---|
| Công thức discount | Unit/API | Bao phủ nhiều data nhanh |
| Quyền với object của người khác | API + một UI path | Kiểm tra server boundary và UX |
| Customer checkout | UI end-to-end | Kiểm tra tích hợp critical path |
| Response schema | API contract | Chẩn đoán nhanh và chính xác |
| Layout và accessibility | UI + visual/a11y | Cầ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.
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.
// Ổ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.
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.
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.
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ế.
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.
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.