Khi học viên của tôi viết script automation đầu tiên, code chạy được nhưng chỉ vài tuần sau là rối tung mỗi lần giao diện đổi. Page Object Model (POM) là một design pattern automation mô hình hóa mỗi màn hình thành một class riêng, tách locator và thao tác ra khỏi test case — nhờ đó code dễ đọc, dễ tái sử dụng và cực dễ bảo trì. Đây là mô hình pom phổ biến nhất trong test automation.

page object model - sơ đồ tách page class chứa locator ra khỏi test class trong automation

Trong bài này tôi đi từ khái niệm, qua lý do vì sao không dùng POM thì khổ, đến cấu trúc page class + test class, một ví dụ POM đầy đủ cho màn hình đăng nhập, và cuối cùng là Page Factory. Nếu bạn chưa rõ nền, hãy đọc qua Automation testing là gì trước.

Page Object Model là gì?

Page Object Model (POM) là mẫu thiết kế trong đó mỗi trang (hoặc thành phần giao diện) của ứng dụng được biểu diễn bằng một class riêng. Class đó chứa hai thứ: các locator (định vị phần tử trên trang) và các method thể hiện hành động người dùng có thể làm trên trang đó — ví dụ nhập username, bấm nút Đăng nhập.

Ý tưởng cốt lõi của page object pattern rất đơn giản: test case chỉ nên mô tả "làm gì" (đăng nhập với tài khoản A), còn "làm bằng cách nào" (tìm ô input bằng ID nào, bấm nút bằng selector nào) thì giấu hết vào page class. Test không được biết tới locator. Đây chính là nguyên tắc separation of concerns — tách mối quan tâm — áp dụng vào code kiểm thử.

Nói cho dễ hình dung: page class giống như một "điều khiển từ xa" của màn hình. Test viết loginPage.login("tuan", "123") mà không cần quan tâm bên trong nút bấm nằm ở đâu. Khi cần tra cứu các khái niệm liên quan, bạn có thể xem thuật ngữ kiểm thử.

Vì sao cần POM (vấn đề khi không dùng)

Hãy tưởng tượng bạn không dùng POM. Locator (ví dụ driver.findElement(By.id("username"))) nằm rải rác trong 50 test case khác nhau. Một ngày đẹp trời, dev đổi id="username" thành id="user-name". Bạn phải sửa 50 chỗ thủ công, sót một chỗ là test fail giả. Đây là ác mộng bảo trì kinh điển của automation.

POM giải quyết đúng nỗi đau này bằng nguyên tắc DRY (Don't Repeat Yourself): mỗi locator chỉ khai báo một lần duy nhất trong page class. UI đổi thì bạn sửa một chỗ, toàn bộ test dùng lại được ngay. Bảng dưới so sánh trực tiếp hai cách viết, có thể trích dẫn được:

Tiêu chí Không dùng POM Dùng POM
Vị trí locator Rải rác khắp test case Tập trung trong page class
Khi UI đổi selector Sửa nhiều nơi, dễ sót Sửa một chỗ duy nhất
Trùng lặp code Cao (copy-paste) Thấp (tái sử dụng method)
Test case đọc có dễ không Khó, lẫn kỹ thuật Dễ, gần ngôn ngữ nghiệp vụ
Chi phí bảo trì lâu dài Tăng nhanh theo số test Ổn định, dễ mở rộng

Ba lợi ích lớn nhất tôi luôn nhấn với học viên: dễ bảo trì (sửa một chỗ), tái sử dụng (một method login() dùng cho hàng chục test), và dễ đọc (test đọc gần như tiếng người). Đó là lý do POM gần như là chuẩn mặc định trong mọi test automation framework nghiêm túc.

Cấu trúc POM (page class + test)

Một dự án POM tối thiểu tách làm hai lớp rõ ràng:

  • Page class: đại diện cho một màn hình. Bên trong khai báo locator (private) và các method public thể hiện hành động (enterUsername, clickLogin, login). Page class không chứa assertion (kiểm tra) — nó chỉ cung cấp hành động và trả về dữ liệu.
  • Test class: chứa các test case thật. Nó gọi method của page class để thực hiện kịch bản, rồi assert kết quả. Đây là nơi duy nhất có mệnh đề kiểm tra (ví dụ assertEquals).

Ranh giới này quan trọng: hành động ở page class, kiểm tra ở test class. Trộn assertion vào page class là lỗi thường gặp khiến page object khó tái sử dụng. Với ứng dụng lớn, người ta còn thêm một lớp BasePage chứa những thao tác chung (mở URL, chờ phần tử) để mọi page class kế thừa — giảm trùng lặp thêm một tầng nữa.

Ví dụ POM cho màn hình đăng nhập

Lấy ví dụ quen thuộc trong khóa học của tôi: "Hệ thống đặt phòng họp nội bộ", màn hình đăng nhập. Đây là page class viết bằng Selenium Java:

```java public class LoginPage { private WebDriver driver;

// Locator khai báo tập trung — chỉ một chỗ
private By usernameInput = By.id("username");
private By passwordInput = By.id("password");
private By loginButton   = By.cssSelector("button[type='submit']");
private By errorMessage   = By.cssSelector(".alert-danger");

public LoginPage(WebDriver driver) {
    this.driver = driver;
}

// Method thể hiện hành động người dùng
public void login(String user, String pass) {
    driver.findElement(usernameInput).sendKeys(user);
    driver.findElement(passwordInput).sendKeys(pass);
    driver.findElement(loginButton).click();
}

public String getErrorMessage() {
    return driver.findElement(errorMessage).getText();
}

} ```

Và đây là test class dùng lại page object đó — sạch sẽ, gần như đọc thành lời:

```java @Test public void loginSaiMatKhau_hienThongBaoLoi() { LoginPage loginPage = new LoginPage(driver); loginPage.login("tuan", "sai-mat-khau");

// Assertion nằm ở test class, KHÔNG ở page class
assertEquals("Sai tài khoản hoặc mật khẩu", loginPage.getErrorMessage());

} ```

Bạn thấy rõ chưa? Test chỉ nói "đăng nhập với mật khẩu sai rồi kiểm tra thông báo lỗi". Nếu mai này selector #username đổi, tôi chỉ sửa đúng dòng usernameInput trong LoginPage. Muốn nắm chắc cú pháp By, findElement, hãy ôn lại Selenium cơ bản.

POM + Page Factory

Page Factory là một lớp hỗ trợ có sẵn của Selenium (WebDriver) giúp khởi tạo các phần tử trong page object một cách gọn hơn, dùng annotation @FindBy thay cho việc gọi driver.findElement thủ công. Nó là biến thể phổ biến của POM trong hệ Selenium Java.

Với Page Factory, page class được viết lại như sau:

```java public class LoginPage { @FindBy(id = "username") private WebElement usernameInput;

@FindBy(id = "password")
private WebElement passwordInput;

@FindBy(css = "button[type='submit']")
private WebElement loginButton;

public LoginPage(WebDriver driver) {
    // Khởi tạo các @FindBy ở trên
    PageFactory.initElements(driver, this);
}

public void login(String user, String pass) {
    usernameInput.sendKeys(user);
    passwordInput.sendKeys(pass);
    loginButton.click();
}

} ```

PageFactory.initElements(driver, this) sẽ quét các trường có @FindBy và gắn WebElement tương ứng. Cú pháp gọn hơn, nhưng lưu ý: đây là tính năng đặc thù của Selenium. Các công cụ mới như Playwright hay Cypress không dùng Page Factory — với chúng, bạn viết POM theo cách "thủ công" (locator + method) như ví dụ đầu, thường tận dụng auto-wait sẵn có. POM là mẫu thiết kế, còn Page Factory chỉ là một cách hiện thực của riêng Selenium.

Lưu ý về thuật ngữ: ISTQB không định nghĩa riêng "Page Object Model" hay "Page Factory" — đây là mẫu thiết kế (design pattern) thuộc kỹ thuật lập trình test automation, không phải khái niệm trong syllabus Foundation. Tuy nhiên ISTQB nhấn mạnh test automation cần khả năng bảo trì (maintainability) và tách biệt tầng test — đúng tinh thần mà POM hướng tới.