Page Object Model (POM) là gì? Ưu và nhược điểm

⏱︎

Read time:

4–6 minutes
Minh họa Page Object Model: trang, thao tác và kết quả

Khi nhiều test đều nhập email, mật khẩu và bấm Đăng nhập, cùng một nhóm locator dễ bị lặp ở khắp nơi. Page Object Model (POM) tổ chức các thao tác của một trang hoặc một phần giao diện vào một class. Test gọi hành động có ý nghĩa và tự kiểm tra kết quả mong đợi.

Lưu ý
POM là 1 design pattern phổ biến nhất trong Test Automation. Bên cạnh POM, còn nhiều cách tổ chức code khác, ví dụ như Screenplay, Keyword-driven…

Ví dụ nhỏ: trang đăng nhập

Ví dụ dưới đây, màn hình Đăng nhập được miêu tả bằng 1 class có tên là LoginPage, nó bao gồm:

  • Các thành phần locator trên màn hình (như trường email, trường password, nút login…)
  • Các thao tác mà màn hình đó cung cấp (ví dụ như login(), open())
import { Locator, Page } from "@playwright/test";

export class LoginPage {
  private readonly emailInput: Locator;
  private readonly passwordInput: Locator;
  private readonly loginButton: Locator;
  readonly welcomeHeading: Locator;

  constructor(private readonly page: Page) {
    this.emailInput = page.getByLabel("Email");
    this.passwordInput = page.getByLabel("Mật khẩu");
    this.loginButton = page.getByRole("button", { name: "Đăng nhập" });
    this.welcomeHeading = page.getByRole("heading");
  }

  async open(): Promise<void> {
    await this.page.goto("/login");
  }

  async login(email: string, password: string): Promise<void> {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }
}

Page là tab trình duyệt Playwright được truyền qua constructor. Các locator được lưu thành biến riêng và khởi tạo một lần trong constructor, nên method login() chỉ còn mô tả các bước nghiệp vụ.

Dưới đây là file test case sẽ gọi đến file LoginPage bên trên:

import { test, expect } from "@playwright/test";
import { LoginPage } from "./login-page";

test("đăng nhập thành công", async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.open();
  await loginPage.login("an@example.com", "secret123");

  await expect(loginPage.welcomeHeading).toHaveText("Xin chào An");
});

Test vẫn thể hiện điều cần kiểm tra qua expect(); page object mô tả cách thao tác. Khi tên hoặc cấu trúc của nút đổi, bạn chỉnh locator ở LoginPage và chạy lại các test liên quan. Code gọn gàng, dễ maintain.

Page object thường chứa gì?

  • Constructor nhận Page hoặc vùng giao diện liên quan.
  • Locator ổn định và method thao tác như login(), search().
  • Các method đọc thông tin trên trang, thao tác tương tác với trang
Lưu ý
Kỳ vọng của từng test (expect) thường được viết ở file test case chứ không viết vào file Page nhé

Các page có thể kế thừa 1 BasePage nếu cần

Khi nhiều page cùng cần một số hành vi như mở đường dẫn, đọc tiêu đề hoặc chờ trang tải xong, có thể đặt phần dùng chung vào BasePage. Các page cụ thể sẽ kế thừa class đó:

BasePage

import { Page } from "@playwright/test";

export class BasePage {
  constructor(protected readonly page: Page) {}

  async openPath(path: string): Promise<void> {
    await this.page.goto(path);
  }
}

ProductsPage

import { Locator, Page } from "@playwright/test";

import { BasePage } from "./base-page";

export class ProductsPage extends BasePage {
  private readonly searchInput: Locator;

  constructor(page: Page) {
    super(page);
    this.searchInput = page.getByPlaceholder("Tìm sản phẩm");
  }

  async open(): Promise<void> {
    await this.openPath("/products");
  }

  async search(keyword: string): Promise<void> {
    await this.searchInput.fill(keyword);
    await this.searchInput.press("Enter");
  }
}

ProductsPage kế thừa page và openPath()/open_path() từ BasePage. Lệnh super(page) gọi constructor của class cha; page con chỉ cần khai báo locator và hành vi riêng.

Cảnh báo
Chỉ tạo BasePage khi thật sự có logic chung. Một BasePage quá lớn chứa đủ loại locator và method sẽ khiến các page con phụ thuộc lẫn nhau và khó bảo trì.

Sử dụng Composition để khởi tạo menu xuyên suốt các page

Menu, header, sidebar hoặc modal thường xuất hiện trên nhiều trang nhưng không phải là “class cha” của các trang đó. Với composition, ta tách chúng thành component object rồi khởi tạo component bên trong từng page object.

MainMenu

import { Locator, Page } from "@playwright/test";

export class MainMenu {
  private readonly homeLink: Locator;
  private readonly profileLink: Locator;

  constructor(page: Page) {
    this.homeLink = page.getByRole("link", { name: "Trang chủ" });
    this.profileLink = page.getByRole("link", { name: "Hồ sơ" });
  }

  async openHome(): Promise<void> {
    await this.homeLink.click();
  }

  async openProfile(): Promise<void> {
    await this.profileLink.click();
  }
}

HomePage

import { Page } from "@playwright/test";

import { MainMenu } from "./main-menu";

export class HomePage {
  readonly menu: MainMenu;

  constructor(page: Page) {
    this.menu = new MainMenu(page);
  }
}

ArticlePage

import { Page } from "@playwright/test";

import { MainMenu } from "./main-menu";

export class ArticlePage {
  readonly menu: MainMenu;

  constructor(page: Page) {
    this.menu = new MainMenu(page);
  }
}

Cả HomePage và ArticlePage đều có một MainMenu. Test có thể gọi homePage.menu.openProfile() hoặc articlePage.menu.openHome() mà không lặp lại locator của menu trên từng page.

Mẹo nhớ
Inheritance diễn tả quan hệ “is-a”: ProductsPage là một BasePage.
Composition diễn tả quan hệ “has-a”: mỗi page có một MainMenu.

Ưu và nhược điểm của POM

Ưu điểmNhược điểm / rủi ro
Giảm lặp locator và thao tácPhải tạo thêm class và file, có thể rườm rà với test nhỏ
Tập trung phần code cần sửa khi UI thay đổiPage object quá lớn khó bảo trì
Test đọc gần với hành vi người dùngẨn nhiều bước khiến khó thấy test thật sự làm gì
Tái sử dụng thao tác chungMột method chung sửa sai có thể ảnh hưởng nhiều test