NỘI DUNG BÀI HỌC
✅ Triết lý POM: Nắm vững sự tách biệt giữa WHAT (nghiệp vụ) và HOW (kỹ thuật).
✅ Xây dựng BasePage: Gom nhóm các hành động chung (click, fill) để dùng lại.
✅ Thiết kế Page Object: Tạo các lớp đại diện cho từng trang web cụ thể.
✅ Tối ưu hóa: Kết nối POM với Test Case để viết code ngắn gọn, dễ bảo trì.
✅ Thực hành: Xây dựng cấu trúc hoàn chỉnh cho
LoginPage và InventoryPage. I. VẤN ĐỀ: KHI CODE LÀ MỘT "MỚ HỖN ĐỘN"
Hãy tưởng tượng bạn viết một kịch bản "Đăng nhập" bằng cách ghi tất cả các bước vào một file duy nhất:
Ví dụ:
def test_login(page):
page.goto("https://www.saucedemo.com/")
page.locator("#user-name").fill("standard_user")
page.locator("#password").fill("secret_sauce")
page.locator("#login-button").click()
expect(page).to_have_url("https://www.saucedemo.com/inventory.html")
Tại sao cách này lại tệ?
-
"Sửa một chỗ, hỏng mười chỗ": Nếu lập trình viên đổi ID của ô nhập từ
#user-namethành#username, bạn sẽ phải đi tìm và sửa tay ở hàng chục file test khác nhau. -
"Đọc không hiểu gì": Một người mới nhìn vào sẽ thấy một đống các dòng lệnh kỹ thuật (click, fill) mà không hiểu được "ý đồ nghiệp vụ" (Đăng nhập, thêm hàng vào giỏ) là gì.
-
"Làm đi làm lại": Mỗi lần muốn test Đăng nhập, bạn lại phải chép lại y hệt 4 dòng trên.
2. Hệ quả
| Vấn đề | Mô tả | Tác động |
|---|---|---|
| Locator trùng lặp | Dùng cùng một selector ở nhiều nơi | Khi UI thay đổi phải sửa hàng loạt test |
| Lẫn lộn nghiệp vụ và kỹ thuật | Code test chứa cả hành động kỹ thuật (click, fill) và ý định nghiệp vụ (đăng nhập) |
Người đọc khó hiểu test đang kiểm tra điều gì |
| Mất tính tái sử dụng | Phải viết lại thao tác tương tự cho nhiều test | Tốn thời gian, dễ lỗi |
3. Nguyên tắc DRY (Don’t Repeat Yourself)
Định nghĩa: Không lặp lại cùng một đoạn mã hoặc logic ở nhiều nơi.
Mục tiêu: Khi một hành vi (như login) thay đổi, chỉ cần chỉnh ở một vị trí duy nhất.
Ý nghĩa trong automation: Đảm bảo test framework dễ bảo trì, nhất quán và mở rộng lâu dài.
🏛️ II. GIẢI PHÁP: PAGE OBJECT MODEL (POM) LÀ GÌ?
1. Khái niệm
POM là một "phong cách" viết code thông minh. Thay vì viết lung tung, chúng ta chia mỗi trang web thành một đối tượng (Object) riêng biệt.
-
Mỗi trang web (Trang Login, Trang Sản Phẩm, Trang Giỏ Hàng) sẽ là một Class (một "tệp hồ sơ" riêng).
-
Trong đó, chúng ta chia làm 2 phần:
-
Dữ liệu (Locators): Danh sách các nút bấm, ô nhập liệu (nếu ID thay đổi, ta chỉ cần sửa đúng 1 chỗ).
-
Hành động (Business Actions): Các việc bạn làm trên trang (Ví dụ: Thay vì click 3 lần, bạn chỉ cần gọi 1 hàm
login()).
-
2. Mục tiêu
-
Tách biệt WHAT và HOW:
-
WHAT: Ý định nghiệp vụ (“đăng nhập thành công”)
-
HOW: Cách thao tác kỹ thuật (“nhập username”, “click nút login”)
-
-
Giảm lặp lại code: Các hành động UI chỉ được định nghĩa một lần.
-
Tăng khả năng bảo trì: Khi locator thay đổi, chỉ cần cập nhật trong Page Object tương ứng.
-
Dễ đọc và dễ mở rộng: Test thể hiện ý định nghiệp vụ thay vì thao tác kỹ thuật.
III. CẤU TRÚC CHUẨN CỦA FRAMEWORK POM
Để dự án dễ quản lý, chúng ta chia theo 3 tầng (3 lớp):
-
Tầng BasePage (Tầng móng): Gom tất cả các thao tác "nhàm chán" (click, điền text, mở web) vào một chỗ để dùng chung.
-
Tầng Page Objects (Tầng nội thất): Mỗi file đại diện cho 1 trang web. Chứa các "kịch bản" riêng của trang đó (login, add_to_cart).
-
Tầng Tests (Tầng người dùng): Nơi bạn viết test case. Code ở đây cực kỳ ngắn gọn, chỉ gọi tên hành động thay vì viết lệnh kỹ thuật.
| Lớp | Nhiệm vụ chính | Ví dụ |
|---|---|---|
| BasePage | Chứa các thao tác Playwright cấp thấp, có thể tái sử dụng | _click(), _fill(), _visit() |
| Page Object | Mô tả nghiệp vụ của một trang cụ thể, kế thừa BasePage | LoginPage.login(), InventoryPage.add_to_cart() |
| Test Case | Gọi các hành động nghiệp vụ để kiểm thử chức năng | test_login_successful() |
IV. BASEPAGE – GOM NHÓM NHỮNG CÁI CƠ BẢN
Thay vì gọi lệnh Playwright trực tiếp, ta tạo một "ông chủ chung" là BasePage để quản lý các thao tác nền tảng:
-
Gom nhóm các hàm Playwright thường dùng (
goto,click,fill,assert) -
Thêm logic phụ trợ: logging, xử lý lỗi, timeout, hoặc wait tùy chỉnh.
-
Đảm bảo các Page Object tuân thủ cùng một chuẩn hành động.
from playwright.sync_api import Page, expect, TimeoutError
class BasePage:
"""Lớp cha mở rộng với Wait, Logging, Screenshot và các hành động tái sử dụng."""
def __init__(self, page: Page):
self.page = page
def _visit(self, url: str):
"""Điều hướng trang web."""
print(f"[NAVIGATE] {url}")
self.page.goto(url, wait_until="domcontentloaded")
def _click(self, locator: str, name: str = ""):
"""Click vào element có chờ sẵn sàng."""
try:
element = self.page.locator(locator)
element.wait_for(state="visible", timeout=5000)
print(f"[CLICK] {name or locator}")
element.click()
except TimeoutError:
print(f"[ERROR] Element {locator} không hiển thị.")
self._take_screenshot(f"error_click_{name}.png")
raise
def _fill(self, locator: str, text: str, name: str = ""):
"""Điền dữ liệu vào ô input."""
print(f"[FILL] {name or locator}: {text}")
self.page.locator(locator).fill(text)
def _assert_visible(self, locator: str, name: str = ""):
"""Kiểm tra element hiển thị."""
expect(self.page.locator(locator)).to_be_visible()
print(f"[ASSERT] {name or locator} hiển thị thành công.")
def _take_screenshot(self, filename: str):
"""Lưu ảnh chụp màn hình (sử dụng khi test fail)."""
path = f"screenshots/{filename}"
self.page.screenshot(path=path)
print(f"[SCREENSHOT] Lưu tại: {path}")
V. PAGE OBJECT – MÔ TẢ TRANG ĐĂNG NHẬP
1. Định nghĩa
Page Object là một lớp (class) mô tả toàn bộ hành vi nghiệp vụ của một trang web cụ thể.
Mỗi Page Object gồm hai phần chính:
-
Locators – tất cả các selector (CSS/XPath) được khai báo ở dạng hằng số.
-
Business Actions – các hành động nghiệp vụ mô phỏng tương tác người dùng.
2. Mục tiêu
-
Gom nhóm tất cả các thao tác liên quan đến một trang duy nhất.
-
Giúp test chỉ gọi đến các hành động nghiệp vụ thay vì mã kỹ thuật chi tiết.
-
Khi UI thay đổi, chỉ cập nhật trong Page Object tương ứng.
3. Ví dụ – pages/login_page.py
from .base_page import BasePage
from playwright.sync_api import expect
class LoginPage(BasePage):
# Phần 1: Gom các cái nút ở đây
URL = "https://www.saucedemo.com/"
USER_INPUT = "#user-name"
PASS_INPUT = "#password"
LOGIN_BTN = "#login-button"
# Phần 2: Mô tả hành động
def login(self, user, password):
self.page.goto(self.URL)
self._fill(self.USER_INPUT, user)
self._fill(self.PASS_INPUT, password)
self._click(self.LOGIN_BTN)
1. Cấu trúc Test
Test case chỉ nên mô tả luồng nghiệp vụ thay vì thao tác kỹ thuật.
Không được chứa Playwright API trực tiếp như locator, click, fill.
2. Ví dụ – tests/test_login_pom.py
3. Đặc điểm
| Tiêu chí | Đặc điểm của test sạch |
|---|---|
| Ngắn gọn | Không chứa locator hoặc lệnh Playwright thô |
| Dễ đọc | Mô tả rõ ý định kiểm thử bằng ngôn ngữ nghiệp vụ |
| Dễ bảo trì | Khi UI thay đổi, test không cần chỉnh sửa |
| Dễ mở rộng | Có thể thêm trang hoặc nghiệp vụ khác mà không ảnh hưởng test cũ |
VII. TỔNG KẾT KHÁI NIỆM TRONG POM
| Khái niệm | Định nghĩa | Ý nghĩa trong automation |
|---|---|---|
|
POM (Page Object Model) |
Mô hình tổ chức test trong đó mỗi trang được biểu diễn bằng một class |
Giúp tách biệt nghiệp vụ và kỹ thuật, dễ bảo trì |
|
BasePage |
Lớp cha chứa hành động cơ bản của Playwright |
Chuẩn hóa thao tác, tránh lặp lại mã |
|
Page Object |
Class đại diện cho một trang cụ thể |
Gom locator và hành động nghiệp vụ tại một nơi |
|
Business Action |
Hành động có ý nghĩa nghiệp vụ (vd: login, add_to_cart) |
Là “ngôn ngữ” của test case, giúp test dễ đọc |
|
Locator |
Selector trỏ tới element trong UI |
Quản lý tập trung, dễ thay đổi khi UI đổi |
|
Test sạch (Clean Test) |
Test chỉ mô tả nghiệp vụ, không chứa chi tiết kỹ thuật |
Dễ đọc, dễ hiểu, dễ mở rộng |
VIII. NHỮNG THÀNH PHẦN CƠ BẢN TRONG CẤU TRÚC PLAYWRIGHT
Một framework chuyên nghiệp thường mở rộng từ kiến trúc POM cơ bản sang mô hình đa tầng sau:
PlaywrightProject/
│
├── pages/ # Gom chung nền tảng & nghiệp vụ
│ ├── base_page.py # Chứa hàm open_url, click, fill, screenshot
│ └── login_page.py # Chứa class login
│
├── resources/ # Nơi chứa data cho scripts test
│ └── config.json # Chứa thông tin tài khoản test
│
├── utils/ # Các hàm bổ trợ cho scriptts test
│ └── file_reader.py # Các hàm đọc file
│
├── tests/ # Chứa scripts automation test
│ └── test_login_pom.py # Flow: Mở → Đăng nhập → Chụp ảnh → Logout
│
└── reports/ # OUTPUT & EVIDENCE LAYER (Gom chung)
├── report.html # File HTML báo cáo kết quả test đơn giản
├── allure-report/ # Thư mục chứa báo cáo Allure chi tiết
│ └── index.html
└── screenshots/ # Thư mục lưu ảnh minh chứng
└── error_screenshot.png # Chụp ảnh báo cáo
IX. DATA-DRIVEN TESTING (DDT) CƠ BẢN
Thay vì tạo thư mục data/ riêng biệt, ta gộp vào resources/ để giữ đúng tính tối giản (1 file dùng chung cho config và data).
1. Ví dụ cấu trúc file – resources/config.json
{
"valid_user": {
"username": "standard_user",
"password": "secret_sauce"
},
"locked_user": {
"username": "locked_out_user",
"password": "secret_sauce"
}
}
2. Cách sử dụng trong test – tests/test_login_pom.py
import json
from pages.login_page import LoginPage
def test_login_from_json(page):
login_page = LoginPage(page)
# Đọc data từ thư mục resources
with open("resources/config.json") as f:
data = json.load(f)
valid = data["valid_user"]
login_page.login(valid["username"], valid["password"])
login_page.assert_login_successful()
