Site logo
Tác giả
  • avatar Nguyễn Đức Xinh
    Name
    Nguyễn Đức Xinh
    Twitter
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átkhó 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, UserProfilePageKhô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 classTá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.