Page Object Model

Page Object Model (POM) trong Playwright: Thiết kế Automation Framework dễ bảo trì

Page Object Model là gì?

Page Object Model (POM) Playwirght là một design pattern trong automation testing, trong đó mỗi trang (page) hoặc component của ứng dụng được đại diện bởi một class riêng. Class này đóng gói toàn bộ locator và hành động liên quan đến trang đó, tách biệt hoàn toàn với logic test.

Nói đơn giản: thay vì viết page.click('#login-btn') trực tiếp trong từng test, bạn gói nó vào loginPage.clickLoginButton() — một lần viết, dùng ở mọi nơi.

Page Object Model

Tại sao cần Page Object Model trong Playwright?

Khi dự án automation lớn dần, bạn sẽ gặp ngay các vấn đề sau nếu không dùng POM:

  • Locator bị lặp khắp nơi: Đổi một selector phải sửa hàng chục file test
  • Test khó đọc: Logic nghiệp vụ bị pha trộn với chi tiết UI
  • Bảo trì tốn kém: Mỗi lần UI thay đổi là một đợt “đập đi xây lại”
  • Tái sử dụng thấp: Không thể share logic giữa các test suite

Page Object Model giải quyết tất cả những vấn đề trên bằng cách áp dụng nguyên tắc Single ResponsibilityDRY (Don’t Repeat Yourself).

Cấu trúc thư mục Page Object Model chuẩn với Playwright

playwright-project/
├── tests/
│   ├── auth/
│   │   ├── login.spec.ts
│   │   └── register.spec.ts
│   └── checkout/
│       └── checkout.spec.ts
├── pages/
│   ├── BasePage.ts
│   ├── LoginPage.ts
│   ├── DashboardPage.ts
│   └── CheckoutPage.ts
├── components/
│   ├── Header.ts
│   └── Modal.ts
├── fixtures/
│   └── index.ts
├── utils/
│   └── testData.ts
└── playwright.config.ts

Nguyên tắc phân chia:

  • pages/Mỗi file tương ứng một route/trang
  • components/UI component tái sử dụng (header, sidebar, modal)
  • fixtures/ Dependency injection cho page objects
  • utils/ Helper functions và test data

Xây dựng Page Object Model Playwright từng bước

Bước 1: Tạo BasePage

BasePage chứa các phương thức dùng chung cho tất cả pages:

typescript
// pages/BasePage.ts
import { Page, Locator } from '@playwright/test'; export class BasePage { protected page: Page; constructor(page: Page) { this.page = page; } async navigate(path: string) { await this.page.goto(path); } async waitForPageLoad() { await this.page.waitForLoadState('networkidle'); } async getTitle(): Promise<string> { return this.page.title(); } async scrollToElement(locator: Locator) { await locator.scrollIntoViewIfNeeded(); } }

Bước 2: Tạo Page Class cụ thể

typescript
// pages/LoginPage.ts
import { Page, expect } from '@playwright/test'; import { BasePage } from './BasePage'; export class LoginPage extends BasePage { // Khai báo locators như properties của class private readonly emailInput = this.page.getByLabel('Email'); private readonly passwordInput = this.page.getByLabel('Mật khẩu'); private readonly loginButton = this.page.getByRole('button', { name: 'Đăng nhập' }); private readonly errorMessage = this.page.getByRole('alert'); private readonly rememberMeCheckbox = this.page.getByLabel('Ghi nhớ đăng nhập'); constructor(page: Page) { super(page); } async goto() { await this.navigate('/login'); } // Action methods — mô tả hành động người dùng async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.loginButton.click(); } async loginWithRememberMe(email: string, password: string) { await this.rememberMeCheckbox.check(); await this.login(email, password); } // Assertion helpers — kiểm tra trạng thái trang async expectErrorMessage(message: string) { await expect(this.errorMessage).toContainText(message); } async expectLoginFormVisible() { await expect(this.emailInput).toBeVisible(); await expect(this.passwordInput).toBeVisible(); await expect(this.loginButton).toBeVisible(); } }

Bước 3: Sử dụng trong test

typescript// tests/auth/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { DashboardPage } from '../../pages/DashboardPage';

test.describe('Đăng nhập', () => {
  let loginPage: LoginPage;
  let dashboardPage: DashboardPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    dashboardPage = new DashboardPage(page);
    await loginPage.goto();
  });

  test('đăng nhập thành công với thông tin hợp lệ', async ({ page }) => {
    await loginPage.login('admin@test.com', 'Admin@123');
    await expect(page).toHaveURL('/dashboard');
    await dashboardPage.expectWelcomeMessageVisible();
  });

  test('hiển thị lỗi khi mật khẩu sai', async () => {
    await loginPage.login('admin@test.com', 'wrong-password');
    await loginPage.expectErrorMessage('Sai tên đăng nhập hoặc mật khẩu');
  });

  test('hiển thị lỗi khi để trống email', async () => {
    await loginPage.login('', 'Admin@123');
    await loginPage.expectErrorMessage('Email không được để trống');
  });
});

Test file trở nên sạch, dễ đọc như tài liệu nghiệp vụ.

Playwright Fixtures: Nâng cấp POM lên tầm tiếp theo

Thay vì khởi tạo page object trong mỗi beforeEach, Playwright Fixtures giúp inject tự động:

typescript// fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
import { CheckoutPage } from '../pages/CheckoutPage';

type PageFixtures = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
  checkoutPage: CheckoutPage;
};

export const test = base.extend<PageFixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  dashboardPage: async ({ page }, use) => {
    await use(new DashboardPage(page));
  },
  checkoutPage: async ({ page }, use) => {
    await use(new CheckoutPage(page));
  },
});

export { expect } from '@playwright/test';

Sử dụng:

typescript
// tests/auth/login.spec.ts
import { test, expect } from '../../fixtures'; // Import từ fixture, không phải @playwright/test test('đăng nhập thành công', async ({ loginPage }) => { await loginPage.goto(); await loginPage.login('admin@test.com', 'Admin@123'); // loginPage được inject tự động — không cần new LoginPage(page) });
Page Object Model

Component Object Model — Xử lý UI Components tái sử dụng

Với các component xuất hiện ở nhiều trang (Header, Modal, Datepicker), tạo riêng một lớp Component:

typescript
// components/Header.ts
import { Page } from '@playwright/test'; export class Header { private readonly page: Page; private readonly userMenu = this.page.getByTestId('user-menu'); private readonly logoutBtn = this.page.getByRole('menuitem', { name: 'Đăng xuất' }); private readonly notificationBell = this.page.getByTestId('notification-bell'); constructor(page: Page) { this.page = page; } async logout() { await this.userMenu.click(); await this.logoutBtn.click(); } async getNotificationCount(): Promise<number> { const text = await this.notificationBell.textContent(); return parseInt(text ?? '0'); } } // Sử dụng trong DashboardPage export class DashboardPage extends BasePage { readonly header = new Header(this.page); async logout() { await this.header.logout(); } }

Các Anti-pattern cần tránh khi dùng POM

Anti-patternVấn đềCách đúng
Đặt expect() trong page objectPage object trở nên brittle, khó tái sử dụngTách assertion ra test hoặc dùng helper method riêng
Page object quá lớn (God Object)Khó maintain, vi phạm Single ResponsibilityTách theo feature hoặc tạo sub-page
Hardcode test data trong page objectData và logic bị couplingTruyền data từ ngoài vào qua tham số
Dùng waitForTimeout trong page objectTest chậm, flakyDùng auto-wait của Playwright
Không dùng private cho locatorsLocator bị truy cập tùy tiện từ ngoàiLuôn khai báo locators là private readonly

So sánh: Không dùng POM vs. Có dùng POM

Không dùng POM:

typescript
test
('checkout thành công', async ({ page }) => { await page.goto('/login'); await page.fill('#email', 'user@test.com'); await page.fill('#password', 'Pass@123'); await page.click('button[type="submit"]'); await page.click('.product-card:first-child .add-to-cart'); await page.click('#cart-icon'); await page.click('#checkout-btn'); await page.fill('[name="card-number"]', '4111111111111111'); // ... 20 dòng nữa });

Có dùng POM:

typescript
test
('checkout thành công', async ({ loginPage, productPage, checkoutPage }) => { await loginPage.loginAs('user@test.com', 'Pass@123'); await productPage.addFirstProductToCart(); await checkoutPage.completeOrderWithCard('4111111111111111'); await checkoutPage.expectOrderConfirmation(); });

Cùng một kịch bản nhưng version POM đọc như một user story, không phải một đống code.

Checklist POM chuẩn với Playwright

  • Mỗi page/route có một class riêng, kế thừa BasePage
  • Locators được khai báo là private readonly properties
  • Action methods mô tả hành động người dùng (không phải hành động UI)
  • Không đặt assertion logic trong page object
  • Component tái sử dụng được tách thành Component class riêng
  • Dùng Playwright Fixtures để inject page objects
  • Tên method rõ nghĩa: login(), addToCart(), không phải clickBtn3()
  • Không hardcode URL, selector, hay test data trong page object

🎯 Muốn xây Automation Framework với POM bài bản?

Khóa học Playwright chuyên sâu của CO-WELL Tech Academy hướng dẫn bạn:

  • ✅ Thiết kế POM Framework từ zero đến production-ready
  • ✅ Áp dụng Playwright Fixtures để quản lý dependencies
  • ✅ Xây Component Object Model cho dự án enterprise
  • ✅ Tích hợp CI/CD với GitHub Actions
  • ✅ Viết test bền vững, không flaky, dễ bảo trì lâu dài

🚀 Khai giảng: 09/06/2025
Số lượng học viên có giới hạn, đăng ký sớm để đảm bảo suất học!

Đăng ký ngay Khóa Playwright CO-WELL →

Bạn đang có băn khoăn nào cần được giải đáp?

Không. POM là pattern tổ chức code, không ảnh hưởng đến tốc độ thực thi. Playwright vẫn chạy song song các test bình thường.

Không khuyến khích. Assertion nên nằm trong test để rõ ràng về kỳ vọng. Page Object chỉ nên cung cấp getter để lấy trạng thái, để test tự assert.

POM tổ chức theo trang (page-centric), Screenplay Pattern tổ chức theo actor và task (actor-centric). POM phổ biến hơn và phù hợp với hầu hết dự án Playwright.

Khi một UI component (modal, dropdown, datepicker) xuất hiện trên nhiều trang khác nhau. Component Object giúp tái sử dụng logic mà không duplicate code.

Playwright không enforce POM nhưng hỗ trợ đầy đủ TypeScript class và Fixtures — đây là nền tảng lý tưởng để implement POM một cách clean và type-safe.

Thông thường, các lớp học sẽ yêu cầu học viên chuẩn bị máy tính cá nhân để tiếp cận đầy đủ tài liệu học tập. Các yêu cầu khác đã được nêu rõ trong thông tin từng khoá học. Nếu học viên chưa có máy hoặc gặp khó khăn nào khác, vui lòng liên hệ CO-WELL Tech Academy để có thể hỗ trợ tư vấn giải pháp phù hợp.

Các tin tức khác