Bỏ qua

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