Module 07: Page Object Model — Tổ Chức Code Chuyên Nghiệp¶
🎯 Mục Tiêu Module¶
- Hiểu Page Object Model (POM) là gì
- Biết khi nào nên dùng POM
- Thực hành tạo Page Object cho Practice Website
- Hiểu cách tái sử dụng code với POM
7.1. Page Object Model là gì?¶
Vấn đề: Code trùng lặp¶
// Test 1: Login
test('test login', async ({ page }) => {
await page.goto('https://practice.automationtesting.in/my-account/');
await page.locator('#username').fill('testuser123');
await page.locator('#password').fill('Test@123456');
await page.locator('[name="login"]').click();
});
// Test 2: Login và mua hàng
test('test login and buy', async ({ page }) => {
await page.goto('https://practice.automationtesting.in/my-account/');
await page.locator('#username').fill('testuser123'); // Trùng lặp!
await page.locator('#password').fill('Test@123456'); // Trùng lặp!
await page.locator('[name="login"]').click(); // Trùng lặp!
// ... mua hàng
});
// Test 3: Login và xem đơn hàng
test('test login and view orders', async ({ page }) => {
await page.goto('https://practice.automationtesting.in/my-account/');
await page.locator('#username').fill('testuser123'); // Trùng lặp!
await page.locator('#password').fill('Test@123456'); // Trùng lặp!
await page.locator('[name="login"]').click(); // Trùng lặp!
// ... xem đơn hàng
});
Giải pháp: Page Object Model¶
┌─────────────────────────────────────────────────────┐
│ │
│ Page Object Model (POM): │
│ │
│ - Mỗi trang web → 1 file Page Object │
│ - Page Object chứa locator + method │
│ - Test case gọi method thay vì viết lại locator │
│ - Thay đổi locator → Chỉ sửa 1 nơi │
│ │
└─────────────────────────────────────────────────────┘
7.2. Cấu Trúc POM¶
Cấu trúc thư mục¶
project/
├── pages/
│ ├── LoginPage.ts
│ ├── ShopPage.ts
│ └── BasketPage.ts
├── tests/
│ ├── login.spec.ts
│ ├── add-to-cart.spec.ts
│ └── checkout.spec.ts
└── playwright.config.ts
Ví dụ LoginPage¶
// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly welcomeMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('#username');
this.passwordInput = page.locator('#password');
this.loginButton = page.locator('[name="login"]');
this.welcomeMessage = page.locator('.woocommerce-MyAccount-content');
}
async goto() {
await this.page.goto('https://practice.automationtesting.in/my-account/');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectLoginSuccess() {
await expect(this.welcomeMessage).toBeVisible();
}
}
Ví dụ Test dùng POM¶
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('test login success', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser123', 'Test@123456');
await loginPage.expectLoginSuccess();
});
7.3. Khi Nào Dùng POM?¶
Decision Framework¶
┌─────────────────────────────────────────────────────┐
│ Số lượng test case? │
│ │
│ 1-5 test case │
│ └── KHÔNG cần POM │
│ └── Code đơn giản, dễ maintain │
│ │
│ 5-15 test case │
│ └── NÊN dùng POM cơ bản │
│ └── Tách Page Object cho các trang chính │
│ │
│ 15+ test case │
│ └── BẮT BUỘC dùng POM │
│ └── POM đầy đủ + fixtures + test data │
└─────────────────────────────────────────────────────┘
Bảng quyết định¶
| Số test case | Pattern | Lý do |
|---|---|---|
| 1-5 | Script đơn giản | Không cần abstraction |
| 5-15 | POM cơ bản | Tái sử dụng login, navigation |
| 15+ | POM đầy đủ | Maintainability, scalability |
7.4. Ví Dụ Hoàn Chỉnh¶
LoginPage¶
// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly registerEmailInput: Locator;
readonly registerPasswordInput: Locator;
readonly registerButton: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('#username');
this.passwordInput = page.locator('#password');
this.loginButton = page.locator('[name="login"]');
this.registerEmailInput = page.locator('#reg_email');
this.registerPasswordInput = page.locator('#reg_password');
this.registerButton = page.locator('[name="register"]');
}
async goto() {
await this.page.goto('https://practice.automationtesting.in/my-account/');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async register(email: string, password: string) {
await this.registerEmailInput.fill(email);
await this.registerPasswordInput.fill(password);
await this.registerButton.click();
}
async expectLoginSuccess() {
await expect(this.page.locator('.woocommerce-MyAccount-content')).toBeVisible();
}
}
ShopPage¶
// pages/ShopPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class ShopPage {
readonly page: Page;
readonly searchInput: Locator;
readonly productTitle: Locator;
constructor(page: Page) {
this.page = page;
this.searchInput = page.locator('#s');
this.productTitle = page.locator('.product-title');
}
async goto() {
await this.page.goto('https://practice.automationtesting.in/shop/');
}
async searchProduct(keyword: string) {
await this.searchInput.fill(keyword);
await this.searchInput.press('Enter');
}
async clickProduct(name: string) {
await this.page.locator(`text=${name}`).click();
}
async expectProductVisible(name: string) {
await expect(this.page.locator(`text=${name}`)).toBeVisible();
}
}
BasketPage¶
// pages/BasketPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class BasketPage {
readonly page: Page;
readonly productName: Locator;
readonly proceedToCheckout: Locator;
constructor(page: Page) {
this.page = page;
this.productName = page.locator('.product-name');
this.proceedToCheckout = page.locator('.checkout-button');
}
async goto() {
await this.page.goto('https://practice.automationtesting.in/basket/');
}
async expectProductInBasket(name: string) {
await expect(this.productName).toContainText(name);
}
async proceedToCheckoutButton() {
await this.proceedToCheckout.click();
}
}
Test dùng POM¶
// tests/full-flow.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { ShopPage } from '../pages/ShopPage';
import { BasketPage } from '../pages/BasketPage';
test('test full flow: login, add to cart, checkout', async ({ page }) => {
// 1. Login
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser123', 'Test@123456');
await loginPage.expectLoginSuccess();
// 2. Mua hàng
const shopPage = new ShopPage(page);
await shopPage.goto();
await shopPage.clickProduct('HTML5 Forms');
await page.locator('text=Add to basket').click();
await page.locator('text=View Basket').click();
// 3. Kiểm tra giỏ hàng
const basketPage = new BasketPage(page);
await basketPage.expectProductInBasket('HTML5 Forms');
await basketPage.proceedToCheckoutButton();
});
7.5. Lợi Ích Của POM¶
1. Tái sử dụng code¶
Không dùng POM:
- 10 test case × 5 dòng login = 50 dòng trùng lặp
Dùng POM:
- 1 LoginPage × 1 lần viết = 10 dòng
- 10 test case × 1 dòng gọi method = 10 dòng
→ Tiết kiệm 40 dòng code
2. Dễ maintain¶
Không dùng POM:
- Locator thay đổi → Sửa 10 file test
Dùng POM:
- Locator thay đổi → Sửa 1 file Page Object
→ Tiết kiệm thời gian maintenance
3. Dễ đọc¶
Không dùng POM:
await page.locator('#username').fill('testuser');
await page.locator('#password').fill('Test@123');
await page.locator('[name="login"]').click();
Dùng POM:
await loginPage.login('testuser', 'Test@123');
→ Code ngắn gọn, dễ hiểu
📝 Bài Tập¶
Bài Tập 1: Tạo LoginPage¶
Tạo Page Object cho trang Login:
- URL: https://practice.automationtesting.in/my-account/
- Methods: goto(), login(), register(), expectLoginSuccess()
Bài Tập 2: Tạo ShopPage¶
Tạo Page Object cho trang Shop:
- URL: https://practice.automationtesting.in/shop/
- Methods: goto(), searchProduct(), clickProduct()
Bài Tập 3: Viết Test dùng POM¶
Viết test case sử dụng Page Object đã tạo.
✅ Checklist Hoàn Thành Module¶
- [ ] Hiểu Page Object Model là gì
- [ ] Biết khi nào nên dùng POM
- [ ] Thực hành tạo Page Object
- [ ] Viết test dùng POM
- [ ] Hoàn thành bài tập