- Tác giả
- Name
- Nguyễn Đức Xinh
- Ngày xuất bản
- Ngày xuất bản
Playwright Page Object Model - Tổ chức code test hiệu quả và chuyên nghiệp
Trong bài trước, chúng ta đã học cách sử dụng Assertions và Wait để kiểm tra trạng thái UI và xử lý các tình huống bất đồng bộ một cách hiệu quả. Tuy nhiên, khi dự án phát triển và số lượng test cases tăng lên, việc tổ chức code test trở nên vô cùng quan trọng.
Khi số lượng test tăng lên, việc lặp lại mã code để thao tác với các phần tử giao diện trở nên khó kiểm soát và khó bảo trì. Đây là lúc chúng ta cần áp dụng Page Object Model (POM) để cấu trúc lại test.
Page Object Model (POM) là một design pattern phổ biến trong automation testing giúp chúng ta tạo ra một lớp trừu tượng giữa test code và UI elements. Điều này giúp code test trở nên dễ đọc, dễ bảo trì và có khả năng tái sử dụng cao.
1. Page Object Model là gì?
Page Object Model (POM) là 1 design pattern giúp đóng gói các thao tác với trang web thành class/đối tượng, từ đó tách biệt logic test với logic giao diện.
- Mỗi page/component của ứng dụng được đại diện bởi một class riêng biệt
- Tất cả elements và actions của page đó được đóng gói trong class
- Test scripts chỉ tương tác với page objects, không trực tiếp với UI elements
- Khi UI thay đổi, chỉ cần update page object, không cần sửa tất cả test cases
Ưu điểm của Page Object Model:
- Tái sử dụng code: Cùng một page object có thể được sử dụng trong nhiều test cases
- Dễ bảo trì: Khi UI thay đổi, chỉ cần update page object
- Code rõ ràng: Test logic tách biệt khỏi UI implementation details
- Giảm code duplication: Tránh lặp lại cùng một logic nhiều lần
2. Tại sao nên dùng POM trong Playwright?
Lợi ích | Giải thích |
---|---|
✅ Dễ bảo trì | Nếu UI thay đổi, chỉ cần cập nhật 1 file class |
✅ Dễ tái sử dụng | Các hành động (login, fill form, ...) có thể gọi ở nhiều test |
✅ Gọn và sạch | File test ngắn gọn, tập trung vào logic kiểm thử |
✅ Có thể mở rộng | Dễ tích hợp thêm UI Component hoặc phần mềm quản lý test |
3. Cấu trúc thư mục sử dụng POM
Trước khi bắt đầu coding, hãy tổ chức thư mục project một cách khoa học:
tests/
├── pages/
│ ├── base/
│ │ └── BasePage.ts
│ ├── LoginPage.ts
│ ├── HomePage.ts
│ ├── ProductPage.ts
│ └── CheckoutPage.ts
├── fixtures/
│ └── pageFixtures.ts
├── utils/
│ └── testData.ts
└── specs/
├── login.spec.ts
├── checkout.spec.ts
└── product.spec.ts
4. Tạo Page Object class
📄 LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByPlaceholder('Email');
this.passwordInput = page.getByPlaceholder('Password');
this.loginButton = page.getByRole('button', { name: 'Đăng nhập' });
this.errorMessage = page.getByTestId('login-error');
}
async goto() {
await this.page.goto('https://dummy-demo-njndex.web.app/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
// Assertions/Verifications
async verifyLoginError(expectedMessage: string): Promise<void> {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(expectedMessage);
}
async verifyLoginFormVisible(): Promise<void> {
await expect(this.usernameInput).toBeVisible();
await expect(this.passwordInput).toBeVisible();
await expect(this.loginButton).toBeVisible();
}
async expectLoginError(msg: string) {
await expect(this.errorMessage).toHaveText(msg);
}
// Utility methods
async isLoginButtonEnabled(): Promise<boolean> {
return await this.loginButton.isEnabled();
}
async getErrorMessage(): Promise<string> {
return await this.errorMessage.textContent() || '';
}
}
5. Viết test sử dụng Page Object
📄 login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Login Functionality', () => {
test('should show error for invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
// Perform login
await loginPage.login('sai@example.com', 'saimatkhau');
// Verify error message
await loginPage.verifyLoginError('Tài khoản hoặc mật khẩu không đúng');
});
test('should login successfully with valid credentials', async ({ page }) => {
// Verify login form is displayed
const loginPage = new LoginPage(page);
await loginPage.verifyLoginFormVisible();
// Perform login
await loginPage.login('testuser@example.com', 'password123');
// Verify successful login
await homePage.verifyWelcomeMessage('testuser');
await homePage.verifyProductsDisplayed();
});
});
6. Tách riêng dữ liệu test
📄 test-data.ts
// tests/utils/testData.ts
export const testData = {
users: {
validUser: {
email: 'test@example.com',
password: 'password123'
},
invalidUser: {
email: 'invalid@example.com',
password: 'wrongpassword'
}
},
products: {
laptop: {
name: 'MacBook Pro',
price: '$2499'
}
}
};
7. Best Practices khi dùng Page Object Model
✅ Đặt tên rõ ràng: LoginPage
, DashboardPage
, UserProfilePage
✅ Không assert bên trong Page class: chỉ cung cấp hàm expectSomething()
, assertion nên đặt ở file test
✅ Tách riêng hành động và dữ liệu: ví dụ như login()
chỉ xử lý thao tác, không hard-code giá trị
✅ Không mix nhiều page trong 1 class
✅ Tái sử dụng với Fixtures: Tích hợp POM vào fixtures
của Playwright để dễ chia sẻ giữa các test
Ví dụ về cách Đặt tên
// ✅ Good
private readonly submitOrderButton: Locator;
private readonly shippingAddressForm: Locator;
// ❌ Bad
private readonly btn1: Locator;
private readonly form: Locator;
Ví dụ về Tách biệt Actions và Assertions
// ✅ Good - Action methods
async clickAddToCart(): Promise<void> {
await this.addToCartButton.click();
}
// ✅ Good - Verification methods
async verifyProductAdded(): Promise<void> {
await expect(this.successMessage).toBeVisible();
}
Ví dụ về cách Tách riêng hành động và dữ liệu
// ✅ Good
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
// ❌ Bad
async login() {
await this.emailInput.fill('testuser@example.com');
await this.passwordInput.fill('password123');
await this.loginButton.click();
}
8. Tổng kết
Page Object Model là một pattern vô cùng mạnh mẽ giúp tổ chức code test một cách khoa học và bền vững. Thông qua việc áp dụng POM, chúng ta đạt được:
- Code rõ ràng, dễ đọc và maintain: Test logic tách biệt khỏi UI implementation
- Tái sử dụng cao: Page objects có thể được sử dụng trong nhiều test cases
- Giảm effort bảo trì: Khi UI thay đổi, chỉ cần update page objects
- Team collaboration tốt hơn: Cấu trúc rõ ràng giúp team members dễ dàng collaborate
- Tăng tính linh hoạt: Page objects có thể được mở rộng hoặc thay thế mà không ảnh hưởng đến các test cases khác
Đây là một mô hình không thể thiếu khi làm test automation với Playwright ở quy mô lớn.