NỘI DUNG BÀI HỌC

  • Làm chủ Page Object Model (POM)
  • Thiết kế kiến trúc BasePage
  • Kỹ thuật Kế thừa (Inheritance) trong TypeScript:
  • Triển khai POM "Truyền thống"
  • Viết Test Case "Sạch":


 

🔬 Phần 1: Giải phẫu Mẫu thiết kế Page Object Model (POM)

 Định nghĩa Học thuật: POM như một Triết lý về Tách biệt Trách nhiệm (Separation of Concerns)

Page Object Model (POM) là một mẫu thiết kế (design pattern) được áp dụng rộng rãi trong lĩnh vực kiểm thử tự động giao diện người dùng (UI). Về bản chất, đây không chỉ là một kỹ thuật tổ chức code, mà là một biểu hiện cụ thể của một nguyên tắc nền tảng trong kỹ thuật phần mềm: Tách biệt Trách nhiệm (Separation of Concerns - SoC).

Nguyên tắc cốt lõi của POM là phân chia rõ ràng hai lĩnh vực trách nhiệm khác nhau trong một bộ khung kiểm thử (test framework):

  • 📈 Kịch bản Test (Test Script): Lớp này đại diện cho nghiệp vụ (business logic). Nó trả lời câu hỏi: "Chúng ta đang kiểm thử cái gì?" (ví dụ: "người dùng đăng nhập thành công", "thêm sản phẩm vào giỏ hàng"). Lớp này không nên chứa bất kỳ chi tiết kỹ thuật nào về cách các thành phần UI được tìm thấy hay tương tác.

  • ⚙️ Đối tượng Trang (Page Object): Lớp này đại diện cho kỹ thuật (technical implementation). Nó trả lời câu hỏi: "Chúng ta tương tác với UI như thế nào?" (ví dụ: "tìm_nút_đăng_nhập_bằng_selector_X", "điền_vào_ô_username").

Trong kiến trúc này, mỗi Page Object (PO) – một class đại diện cho một trang hoặc một thành phần UI – hoạt động như một lớp trừu tượng (abstraction layer). Nó cung cấp một API (Giao diện Lập trình Ứng dụng) cho giao diện người dùng của ứng dụng đang được kiểm thử. Kịch bản test (*.spec.ts) đóng vai trò là "khách hàng" (client) của API này. Nó chỉ gọi các phương thức nghiệp vụ (ví dụ: loginPage.login(...)) mà không cần biết các chi tiết triển khai bên dưới (ví dụ: page.locator(...).click()).

🏛️ Phân tích Ba Trụ cột Giá trị của POM

Việc áp dụng POM giải quyết ba thách thức lớn nhất trong kiểm thử tự động: bảo trì, tái sử dụng, và khả năng đọc hiểu.

Tính Dễ Bảo trì (Maintainability): Nguyên tắc "Nguồn Chân lý Duy nhất" (Single Source of Truth) 🔧

Đây là lý do kiến trúc quan trọng nhất để áp dụng POM. Các giao diện người dùng (UI) vốn dĩ không ổn định; chúng thay đổi thường xuyên trong suốt vòng đời phát triển sản phẩm.

🤯 Hãy xem xét một kịch bản không có POM, nơi một selector cho nút "Đăng nhập" (ví dụ: button#login) được sử dụng lặp lại trong 50 file test khác nhau. Khi đội ngũ phát triển thay đổi selector này thành button.submit-btn, kỹ sư kiểm thử phải đối mặt với một cơn ác mộng về bảo trì: họ phải tìm và thay thế thủ công ở 50 vị trí. Quá trình này không chỉ tốn thời gian mà còn tiềm ẩn rủi ro cao, có thể dẫn đến việc bỏ sót hoặc thay thế nhầm.

Chi phí bảo trì trong kịch bản này tăng tuyến tính (hoặc tệ hơn) với số lượng test case. Điều này tạo ra "gánh nặng bảo trì" (maintenance debt) khiến bộ test nhanh chóng trở nên lỗi thời, không đáng tin cậy và cuối cùng bị từ bỏ.

💡 POM giải quyết vấn đề này một cách triệt để. Bằng cách tập trung tất cả các selectors và phương thức tương tác của một trang vào một class Page Object duy nhất (ví dụ: LoginPage.ts), class đó trở thành "Nguồn Chân lý Duy nhất" (Single Source of Truth - SSOT) cho giao diện của trang đó. Khi UI thay đổi, kỹ sư chỉ cần cập nhật selector ở một dòng duy nhất trong một file duy nhất (LoginPage.ts). Tất cả 50 file test sử dụng class này sẽ tự động hoạt động trở lại.

Về mặt phân tích thuật toán, POM giảm chi phí thay đổi (cost of change) đối với các selector UI từ O(N) (với N$là số lượng test case) xuống tiệm cận O(1) (thời gian không đổi).


Tính Tái sử dụng (Reusability): Xây dựng API Nghiệp vụ Composable ♻️

POM không chỉ là việc nhóm các selector; nó là việc tạo ra các phương thức đại diện cho các hành vi nghiệp vụ (business behaviors).

Trong một Page Object, chúng ta nên phân biệt hai cấp độ phương thức:

  • ⚛️ Phương thức Nguyên tử (Atomic Methods): Các hành động chi tiết, trực tiếp lên một element. (ví dụ: fillUsername(user), fillPassword(pass), clickLoginButton()).

  • 🧬 Phương thức Phân tử (Molecular/Workflow Methods): Các hành vi nghiệp vụ được tổng hợp (compose) từ các phương thức nguyên tử.

Ví dụ được cung cấp, loginPage.login(user, pass), là một "workflow method" cổ điển. Nó đóng gói ba hành động nguyên tử (điền user, điền pass, click submit) thành một hành vi nghiệp vụ duy nhất.

Sức mạnh của cách tiếp cận này là khả năng lắp ghép (composability). Page Object trở thành một thư viện các API nghiệp vụ có thể tái sử dụng:

  • Một test đăng nhập đơn giản có thể gọi: await loginPage.login('user', 'pass');

  • Một kịch bản test "mua hàng" phức tạp hơn có thể lắp ghép hành vi này:

    await loginPage.login('user', 'pass'); 
    await productPage.searchFor('laptop'); 
    await productPage.addToCart('item_123'); 
    await checkoutPage.proceedToPayment();

Tính tái sử dụng của POM vượt xa việc tiết kiệm vài dòng code; nó cho phép xây dựng các kịch bản E2E (End-to-End) phức tạp bằng cách lắp ghép các khối hành vi nghiệp vụ đã được định nghĩa và kiểm chứng.

 

 Tính Dễ đọc (Readability): Kịch bản Test dưới dạng "Ngôn ngữ Miền Cụ thể" (DSL) 📖

Một hệ quả trực tiếp của việc tách biệt trách nhiệm và tạo ra các workflow method là sự thay đổi triệt để về cấp độ trừu tượng (level of abstraction) trong các file test.

Hãy so sánh hai kịch bản sau đây:

🔀 Không dùng POM ✨ Có dùng POM Phân tích (Cấp độ Trừu tượng)
test('should edit profile', async ({ page }) => { test('should edit profile', async () => {  
await page.locator('input[name="username"]').fill('user'); await loginPage.login('user', 'pass'); 📈 Nghiệp vụ (Business-level): Tập trung vào CÁI GÌ đang được thực hiện (Đăng nhập).
await page.locator('button[type="submit"]').click();   ⚙️ Kỹ thuật (Implementation-level): Tập trung vào CÁCH thực hiện (CSS selectors, loại input).
await page.locator('.profile-icon').click(); await profilePage.goToSettings(); 📈 Nghiệp vụ: CÁI GÌ (Đi đến Cài đặt).
await page.locator('#firstName').fill('New Name'); await profilePage.updateFirstName('New Name'); 📈 Nghiệp vụ: CÁI GÌ (Cập nhật Tên).
//... //...  
}); });  

Như bảng phân tích trên cho thấy, kịch bản "Không dùng POM" là một mớ hỗn độn các chi tiết kỹ thuật. Nó mô tả cách trình duyệt hoạt động. Ngược lại, kịch bản "Có dùng POM" đọc giống như một kịch bản nghiệp vụ hoặc một danh sách các yêu cầu.

Việc áp dụng POM thành công sẽ biến các file *.spec.ts thành một "Ngôn ngữ Miền Cụ thể" (Domain-Specific Language - DSL) cho ứng dụng của bạn. Điều này tạo ra thứ được gọi là "Tài liệu sống" (Living Documentation). 👥 Một thành viên không chuyên về kỹ thuật, như Quản lý Sản phẩm (PM) hoặc Chuyên viên Phân tích Nghiệp vụ (BA), có thể đọc file test và hiểu chính xác các luồng nghiệp vụ nào đang được kiểm thử. Điều này thu hẹp khoảng cách giữa các bên liên quan, nâng cao giá trị của bộ test tự động từ một "công cụ kiểm tra" đơn thuần thành một "tài sản xác thực nghiệp vụ" của tổ chức.

🏗️ Phần 2: Kiến trúc BasePage – Nền tảng của sự Kế thừa (🧬) và Nguyên tắc DRY (🚫🔁)

Trong một kiến trúc POM trưởng thành 🌳, chúng ta nhanh chóng nhận ra rằng nhiều thành phần và hành vi được lặp lại 🔁 trên mọi trang của ứng dụng.

Ví dụ: Header, Footer, Menu điều hướng chính, Nút đăng xuất. Nếu không có một chiến lược quản lý, chúng ta sẽ vi phạm nguyên tắc DRY (Don't Repeat Yourself) bằng cách định nghĩa lại các thành phần này trong mọi Page Object.

Đây là lúc kiến trúc BasePage (Trang Cơ sở) phát huy vai trò của mình.

 

📜 Định nghĩa BasePage: "Hợp đồng Giao diện Chung" (📝 Shared UI Contract)

 

BasePage là một class Cha (👪 Parent Class) trừu tượng mà tất cả các Page Object cụ thể khác (như LoginPage, ProfilePage) sẽ kế thừa (🧬 extend).

Nó không đại diện cho một trang cụ thể nào, mà đại diện cho khung ứng dụng (🖼️ application shell) – những thành phần và hành vi phổ quát, tồn tại bất kể người dùng đang ở trang nào. BasePage đóng vai trò là "Hợp đồng Giao diện Chung", định nghĩa tập hợp các yếu tố và hành động mà mọi trang trong ứng dụng đều phải có quyền truy cập.

 

📐 Thiết kế BasePage hiệu quả: Xác định Hành vi Phổ quát

Câu hỏi kiến trúc quan trọng nhất ❓ khi thiết kế BasePage là: Khi nào một thành phần hoặc hành động nên thuộc về BasePage?

  • 👍 Quy tắc (Rule of Thumb): Nếu một thành phần (ví dụ: userMenu) hoặc một hành động (ví dụ: logout()) có thể được truy cập từ nhiều trang độc lập (như ProfilePage, DashboardPage, SettingsPage), nó là một ứng cử viên lý tưởng ⭐ cho BasePage.

  • ❌ Ngược lại, nếu một thành phần (ví dụ: usernameInput) chỉ xuất hiện trên một trang duy nhất (LoginPage), nó phải thuộc về class Page Object của trang đó.

Các ứng cử viên phổ biến 🏆 cho BasePage bao gồm:

  • 📍 Locators: Header, Footer, Menu điều hướng chính, Avatar người dùng, Nút Logout, Thanh tìm kiếm, Biểu tượng giỏ hàng.

  • 🎬 Hành động: clickLogoToHome(), search(text), logout(), openUserMenu(), getCartItemCount().

 

💻Phân tích Kỹ thuật (Code): pages/base.page.ts

Hãy phân tích một ví dụ triển khai BasePage "truyền thống" trong Playwright sử dụng TypeScript.

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

export class BasePage {
  // 1. Phụ thuộc cốt lõi
  readonly page: Page;

  // 2. Định nghĩa các locators chung
  readonly userMenu: Locator;
  readonly logoutButton: Locator;
  readonly mainLogo: Locator;

  // 3. Constructor để "tiêm" phụ thuộc
  constructor(page: Page) {
    this.page = page;

    // 4. Khởi tạo locators chung
    this.userMenu = page.locator('button[aria-label="User Menu"]');
    this.logoutButton = page.locator('a[href="/logout"]');
    this.mainLogo = page.locator('.header-logo');
  }

  // 5. Định nghĩa các hành động chung
  async logout() {
    await this.userMenu.click();
    await this.logoutButton.click();
  }
}
 

🔍 Phân tích chuyên sâu các thành phần chính:

  • 🔗 readonly page: Page; (Dòng 4)

    Đây là phụ thuộc (dependency) cốt lõi của mọi Page Object. BasePage (và tất cả các class con của nó) cần đối tượng page của Playwright để thực hiện các hành động (như click(), fill()) và tìm kiếm locators.

  • 🏷️ readonly userMenu: Locator; (Dòng 7-9)

    Các locators được định nghĩa là thuộc tính (properties) của class. Việc định nghĩa chúng ở cấp class và khởi tạo trong constructor đảm bảo rằng logic tìm kiếm 🕵️ ('button[aria-label="User Menu"]') được định nghĩa một lần và được tái sử dụng bởi tất cả các phương thức trong class (như logout()). Điều này tuân thủ nguyên tắc SSOT 🎯 ở cấp độ class.

  • 💉 constructor(page: Page) (Dòng 12)

    Đây là cơ chế Truyền Phụ thuộc qua Constructor (Constructor Dependency Injection). constructor là điểm vào (🚪 entry point) để "tiêm" (inject) đối tượng page từ bên ngoài (từ file test) vào bên trong class BasePage.

  • 🛡️ this.page = page; (Dòng 13)

    Dòng này gán đối tượng page được tiêm vào cho thuộc tính this.page của class. Việc sử dụng từ khóa readonly (ở dòng 4) là một thực hành tốt về lập trình phòng thủ (defensive programming). Nó đảm bảo rằng this.page không thể bị gán lại 🔒 một cách vô tình bởi bất kỳ phương thức nào khác sau khi đối tượng đã được khởi tạo.

  • 🤝 async logout() (Dòng 21)

    Đây là một hành vi chung (shared behavior) được xây dựng từ các thành phần chung (shared locators). Bất kỳ class Page Object nào kế thừa BasePage giờ đây đều tự động 🤖 có khả năng logout() mà không cần viết lại một dòng code nào. ✨

    💻 Phần 3: Triển khai Kỹ thuật: Xây dựng Chuỗi Kế thừa (🔗 Inheritance Chain)

    Với BasePage được định nghĩa làm nền tảng 🧱, chúng ta có thể xây dựng các Page Objects cụ thể bằng cách sử dụng tính kế thừa trong Lập trình Hướng đối tượng (OOP) 👨‍👩‍👧‍👦.

    📁 Phân tích Cấu trúc Thư mục Dự án

    Một cấu trúc dự án "truyền thống" 📜 tuân thủ POM sẽ trông như sau:

    my-playwright-project/
    ├── node_modules/
    ├── pages/          # 1. 🤖 Thư mục API Giao diện (CÁCH LÀM)
    │   ├── base.page.ts  # (QUAN TRỌNG) 👨‍👩‍👧 Class Cha
    │   ├── login.page.ts # 👶 Class Con
    │   └── profile.page.ts # 👶 Class Con
    ├── tests/          # 2. 📈 Thư mục Kịch bản Nghiệp vụ (LÀM CÁI GÌ)
    │   ├── login.spec.ts
    │   └── profile.spec.ts
    ├── package.json
    ├── playwright.config.ts
    └── tsconfig.json
    

    Cấu trúc thư mục này là biểu hiện vật lý của nguyên tắc Tách biệt Trách nhiệm 🧩.

    • Thư mục pages/ 🤖 chứa toàn bộ logic kỹ thuật và các selectors. Đây là "API" của UI.

    • Thư mục tests/ 📈 chứa logic nghiệp vụ và các kịch bản kiểm thử. Đây là "client" sử dụng API đó.

    Sự tách biệt này cho phép các kỹ sư làm việc song song 👥. Một kỹ sư có thể cập nhật selectors trong pages/ để đáp ứng thay đổi UI mà không làm gián đoạn logic nghiệp vụ, trong khi một kỹ sư khác có thể viết kịch bản test mới trong tests/ bằng cách sử dụng các phương thức Page Object đã có.

     

    👶 Triển khai Class Con: pages/login.page.ts

    Class con kế thừa từ BasePage và bổ sung thêm ➕ các locators cũng như hành vi của riêng trang đó.

    import { type Page, type Locator } from '@playwright/test';
    import { BasePage } from './base.page'; // 1. 📥 Import class Cha
    
    export class LoginPage extends BasePage { // 2. 🧬 Kế thừa BasePage
      
      // Định nghĩa các Locators của riêng trang Login
      readonly usernameInput: Locator;
      readonly passwordInput: Locator;
      readonly loginButton: Locator;
    
      // Constructor
      constructor(page: Page) {
        // 3. ⬆️ 'super(page)' là BẮT BUỘC
        // Nó gọi constructor của BasePage và đưa 'page' vào đó
        super(page);
        
        // Khởi tạo locators của riêng trang này
        this.usernameInput = page.locator('input[name="username"]');
        this.passwordInput = page.locator('input[name="password"]');
        this.loginButton = page.locator('button[type="submit"]');
      }
    
      // Hành động của riêng trang này
      async goTo() {
        await this.page.goto('/login');
      }
    
      async login(username: string, password?: string) {
        await this.usernameInput.fill(username);
        if (password) {
          await this.passwordInput.fill(password);
        }
        await this.loginButton.click();
      }
    }​

    🔬 Phân tích các thành phần kế thừa:

    • 📥 import { BasePage } from './base.page'; (Dòng 2): Import class Cha.

    • 🧬 export class LoginPage extends BasePage (Dòng 4): Từ khóa extends là trái tim ❤️ của mô hình kế thừa. Nó khai báo rằng LoginPage là một (is-a) BasePage. Ngay tại thời điểm này, LoginPage ngay lập tức "thừa hưởng" 🎁 mọi thuộc tính và phương thức public từ BasePage, bao gồm this.page, this.userMenu, this.mainLogo, và quan trọng nhất là phương thức async logout().

    • super(page); (Dòng 14): Đây có lẽ là dòng code quan trọng nhất 🌟 trong toàn bộ class con.

      • constructor của BasePage (Cha 👨‍👩‍👧) yêu cầu một đối tượng page để có thể khởi tạo các locators chung (this.userMenu, this.logoutButton...).

      • Khi một đối tượng LoginPage (Con 👶) được tạo (ví dụ: new LoginPage(page)), constructor của nó được gọi.

      • constructor của Con bắt buộc phải gọi constructor của Cha (thông qua super(page)) và "chuyền" (pass) đối tượng page lên cho Cha.

      • Nếu dòng super(page) này bị thiếu ❌, this.page và tất cả các locators trong BasePage sẽ là undefined. Mọi lời gọi (kể cả gọi ngầm) đến các thành phần của BasePage (ví dụ: await this.logout()) sẽ thất bại 💥 ngay lập tức vì this.userMenu không bao giờ được khởi tạo. Lệnh super(page) là "đường ống" (🔧) kết nối phụ thuộc page từ Con lên Cha, đảm bảo toàn bộ đối tượng được khởi tạo chính xác.

      • Sau khi gọi super(page), constructor của LoginPage tiếp tục khởi tạo các locators của riêng nó (như this.usernameInput).

     

    ✅  Triển khai Class Con: pages/profile.page.ts (Minh chứng cho sự kế thừa)

    Tương tự 👯, ProfilePage cũng kế thừa BasePage và chỉ tập trung vào các yếu tố của riêng nó.

    import { type Page, type Locator, expect } from '@playwright/test';
    import { BasePage } from './base.page'; // 📥 Import class Cha
    
    export class ProfilePage extends BasePage { // 🧬 Kế thừa BasePage
      
      readonly welcomeMessage: Locator;
      readonly editProfileButton: Locator;
    
      constructor(page: Page) {
        super(page); // ⬆️ Bắt buộc gọi 'super'
    
        this.welcomeMessage = page.locator('h1.welcome');
        this.editProfileButton = page.locator('button[aria-label="Edit Profile"]');
      }
    
      async goTo() {
        await this.page.goto('/profile');
      }
    
      async expectWelcomeMessage(message: string) {
        await expect(this.welcomeMessage).toContainText(message);
      }
    }

    Sức mạnh 💪
    của mô hình này được thể hiện khi chúng ta sử dụng ProfilePage. Một đối tượng profilePage (thuộc type ProfilePage) giờ đây có thể gọi:

    • 👤 profilePage.expectWelcomeMessage(...): Phương thức của riêng nó, được định nghĩa trong ProfilePage.

    • 🎁 profilePage.logout(): Phương thức được kế thừa từ BasePage.

    Hành vi logout chỉ được định nghĩa một lần 1️⃣ trong base.page.ts nhưng có thể được sử dụng bởi mọi Page Object kế thừa từ nó. Nguyên tắc DRY (🚫🔁) đã được tuân thủ triệt để. ✅



    🚀 Phần 4: Kích hoạt Kịch bản Test (Test Script Implementation)

    Đây là lớp "Khách hàng" (👨‍💼 Client) của POM, nơi logic nghiệp vụ được định nghĩa và các Page Objects được điều phối 🎶.

     

    🌉 Phân tích tests/login.spec.ts: Cầu nối giữa Nghiệp vụ và Kỹ thuật

    File *.spec.ts là nơi mọi thứ được kết hợp lại. Nó không chứa bất kỳ logic UI nào (không có selectors, không có page.locator). Nó chỉ điều phối (orchestrate) 🎶 các lệnh gọi đến các Page Objects đã được định nghĩa trong thư mục pages/.

    📜 Chiến lược Khởi tạo "Truyền thống": Vai trò của test.beforeEach

    Khi không sử dụng Test Fixtures của Playwright, phương pháp "truyền thống" để cung cấp Page Objects cho các test case là khởi tạo chúng thủ công ✋ trong hook test.beforeEach.

    import { test, expect, type Page } from '@playwright/test';
    import { LoginPage } from '../pages/login.page';
    import { ProfilePage } from '../pages/profile.page';
    // Bạn không cần import BasePage ở đây
    
    // 1. Khai báo các biến Page Object ở phạm vi 'describe'
    let loginPage: LoginPage;
    let profilePage: ProfilePage;
    
    test.describe('🧪 Login & Profile Tests', () => {
    
      // 2. Khởi tạo tất cả Page Objects TRƯỚC MỖI TEST
      test.beforeEach(async ({ page }) => { // (A) ⏰
        // 'page' được Playwright cung cấp tự động
        loginPage = new LoginPage(page); // (B) ✨
        profilePage = new ProfilePage(page); // (C) ✨
      });
    
      // 3. Viết test case "sạch"
      //...
    });
     

    🔍 Phân tích cơ chế khởi tạo này:

    • Dòng (A) async ({ page }): Đây là cơ chế cốt lõi ⚙️ của Playwright. Playwright tự động tạo và cung cấp (inject) một đối tượng page mới, độc lập, và có context riêng cho mỗi test case.

    • Dòng (B) và (C) new LoginPage(page);: Đây là phương pháp khởi tạo "truyền thống". Chúng ta đang khởi tạo thủ công ✋ (manual instantiation) các Page Objects. Chúng ta lấy đối tượng page (mà Playwright cung cấp ở bước A) và truyền nó vào constructor của Page Object. Đây chính là cách "Truyền Phụ thuộc qua Constructor" 💉 được thực hiện ở cấp độ test.

    • Hook test.beforeEach ⏰ đảm bảo rằng trước mỗi test (ví dụ: TC_LOGIN_01), chúng ta có một loginPageprofilePage mới tinh ✨ (fresh) được liên kết với context page mới tinh của test đó. Điều này đảm bảo các test hoàn toàn độc lập và tách biệt với nhau (Test Isolation 🛡️).

     

    🌊 4.3. Phân tích các Test Case (Minh họa luồng nghiệp vụ)

    Các test case giờ đây trở nên vô cùng "sạch sẽ" 🧼, chỉ tập trung vào nghiệp vụ.

    Phân tích TC_LOGIN_01: Should login successfully ✅

    test('TC_LOGIN_01: Should login successfully', async ({ page }) => {
        await loginPage.goTo();
        await loginPage.login('valid_user', 'correct_pass');
        
        // Đã đăng nhập, giờ dùng profilePage
        await profilePage.expectWelcomeMessage('Chào mừng, valid_user!');
    
        // Bạn có thể dùng phương thức từ BasePage (vì đã kế thừa)
        // Ví dụ: await profilePage.logout();
      });​

    Luồng thực thi ➡️ của test case này như sau:

    1. Test TC_LOGIN_01 bắt đầu.

    2. Hook test.beforeEach ⏰ chạy, tạo ra các thực thể (instances) loginPageprofilePage mới.

    3. await loginPage.goTo(): ➡️ Gọi phương thức từ LoginPage để điều hướng.

    4. await loginPage.login(...): 🔑 Gọi phương thức workflow từ LoginPage. Trình duyệt thực hiện đăng nhập và (giả sử) tự động điều hướng đến trang profile.

    5. await profilePage.expectWelcomeMessage(...): 👀 Gọi phương thức từ ProfilePage để xác thực (Assert) trạng thái mong muốn sau khi hành động (Act) đăng nhập hoàn tất.

    Test case này minh họa cách một luồng nghiệp vụ E2E có thể điều phối nhiều Page Objects 🎶: sử dụng loginPage để thực hiện hành động và profilePage để xác thực kết quả.

     

    Phân tích TC_PROFILE_03: Should logout successfully (Minh chứng kế thừa 🎁)

    test('TC_PROFILE_03: Should logout successfully', async ({ page }) => {
        // Giả sử test này đã đăng nhập (setup)
        await loginPage.goTo();
        await loginPage.login('valid_user', 'correct_pass');
    
        // Gọi hàm logout() từ BasePage (thông qua profilePage)
        await profilePage.logout(); // <-- 🎯 ĐIỂM MẤU CHỐT
    
        // Kiểm tra đã về trang login
        await expect(page).toHaveURL('/login');
      });

    Đây là bằng chứng cuối cùng 🏁 cho thấy kiến trúc BasePage hoạt động:

    1. Test case này gọi profilePage.logout().

    2. Nếu chúng ta kiểm tra file profile.page.ts, chúng ta thấy không có 👻 phương thức nào tên là logout().

    3. Tuy nhiên, vì ProfilePage kế thừa (🧬 extends) BasePage, và base.page.ts có định nghĩa một phương thức async logout(), lời gọi này hoàn toàn hợp lệ ✅ và thành công.

    4. Đối tượng profilePage đã "thừa hưởng" 🎁 tất cả các phương thức public từ BasePage.

    Kiến trúc này đã loại bỏ hoàn toàn nhu cầu phải viết lại logic logout trong ProfilePage, SettingsPage, DashboardPage, và mọi Page Object khác, tuân thủ tuyệt đối nguyên tắc DRY (🚫🔁).

Teacher

Teacher

Nguyên Hoàng

Automation Engineer

With 7+ years of hands-on experience across multiple languages and frameworks. I'm here to share knowledge, helping you turn complex processes into simple and effective solutions.

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học