Page Object Model: Lựa chọn để test automation đi đường dài

Page Object Model: Lựa chọn để test automation đi đường dài

Xem nhanh

Nếu chúng ta triển khai dự án automation testing có quy mô hàng nghìn test case thì việc áp dụng mô hình, kiểu mẫu (design pattern) phù hợp là cần thiết chúng giúp giải phóng rất nhiều “sức người” khi implement và maintain. Trong bài này chúng ta tìm hiểu một kiểu mẫu triển khai source code tự động hóa cho end-to-end khá phổ biến cũng như nổi tiếng đó là Page Object Model (POM). Kiểu mẫu này mang lại ba lợi ích to lớn là: Giảm sự trùng lắp (reduces code duplication), tăng tính tái sử dụng (reusable) và giàu tính bảo trì (improves test maintenance)

Page Object Model(POM) là gì?

Rất nhiều định nghĩa về POM và chúng ta không khó để có thể tìm kiếm nó. Bên dưới là một định nghĩa từ trang medium mà theo tôi khá phù hợp, như sau:

POM là một kiểu mẫu thiết kế phổ biến trong automation testing để giúp test code giàu tính maintain và giảm thiểu sự trùng lặp code

Còn theo tôi thì POM có thể được hiểu vắn tắt như sau:

POM là mô hình giúp triển khai test code của test tự động với cách để chuyển thể phần tử (Web Elements) từ màn hình tính năng test sang test code, với cách tiếp cận lập trình OOP

Vì là mô hình nên chúng ta có thể dùng bất kỳ ngôn ngữ lập trình yêu thích nào của mình để triển khai. Trong bài viết này tôi sử dụng JavaScript để diễn đạt.

POM diễn đạt dưới công thức

Ở phần trên có thể xem như là định nghĩa về POM. Vì định nghĩa thì ít nhiều tồn tại sự trừu tượng nên chúng ta cố gắng diễn đạt dưới dạng công thức để dễ tham khảo khi cần. Lúc này POM được diễn đạt như sau:

Page Object Class = DOM action methods(*1) [+ Web Elements utility methods(*2)]

Trong đó:

*1: Các phương thức tương tác với phần tử HTML trang, chẳng hạn: Nhập giá trị cho phần tử trên trang; lấy giá trị của phần tử HTML trên trang. Ví dụ: 

Copy
clickOnAddButton() {
   // ...
}
Copy
inputNotesField(folderNotes) { 
   // ...
}

*2: Tùy chọn, các phương thức tiện ích của phần từ DOM, giúp test code hoạt động ổn định hơn. Ví dụ: Xác nhận tồn tại phần tử Title trên trang hay không hasTitleElement()

Cách triển khai POM

Trong bài này sử dụng Login | Cybozu Garoon (username: brown password: brown) để làm minh họa dựa trên Js và WebdriverIO

Cybozu Garoon là một sản phẩm làm việc cộng tác. Nó là một trong các sản phẩm chủ lực của tập đoàn Cybozu. Bạn có thể thử trải nghiệm online để biết thêm về sản phẩm

Bên dưới là ví dụ test script cho test case Login (đăng nhập) hệ thống Garoon

Bước 1: Truy cập vào Garoon testing site, như Hình 1.0 ví dụ: Garoon demo site

Hình 1.0: Google chrome browser truy cập Garoon demo site

Hình 1.0: Google chrome browser truy cập Garoon demo site

Bước 2: Nhập giá trị brown vào textbox có label tên login name (ログイン名)

Bước 3: Nhập giá trị brown vào textbox có label tên password (パスワード)

Bước 4: Click lên button có tên là Login (パスワード) để đăng nhập hệ thống

Hình 2.0: Cung cấp login name: brown và password: brown vào các field tương ứng

Hình 2.0: Cung cấp login name: brown và password: brown vào các field tương ứng

Bước 5: Xác nhận xem, liệu chúng ta có đăng nhập thành công hay không?
Nếu thành công, tên “Foster Brown” sẽ hiển thị như Hình 3.0 bên dưới, ngược lại lỗi xuất hiện “Invalid login name or password.”

Hình 3.0: Kết quả đăng nhập thành công với tên "Foster Brown" sẽ hiển thị trên thanh header

Hình 3.0: Kết quả đăng nhập thành công với tên "Foster Brown" sẽ hiển thị trên thanh header

Bên dưới là test code chúng ta triển khai theo hai phương pháp, không dùng POM và dùng POM

Implement test case "Login" không sử dụng mô hình POM

Phân bổ file spec: system/sign-in/valid-account-no-pom/valid-account-no-pom.spec.js

Bên dưới là test code

Copy
import { element } from '#e2e-core/wdio-wrapper/element';
import { account } from './valid-account.data';

describe('No Page object model used demo', () => {
    it('should be signed in with a valid account by no pom tech', () => {
        browser.url('https://onlinedemo2.cybozu.info/scripts/garoon/grn.exe/index?');    // Bước 1
        element('//input[@name="_account"]').setValue(account.username);                 // Bước 2
        element('//input[@name="_password"]').setValue(account.password);                // Bước 3
        element('//*[@name="login-submit"]').click();                                    // Bước 4
        element('//*[@name="login-submit"]').waitForDisplayed({reverse: true});
        
        const selector = `//*[@id="header"]//span[@title="${account.username}"]`;
        const result = element(selector).isDisplayed();
        assert.isTrue(result, 'Cannot sign in with specified account');                  // Bước 5
    });
});

Đặc thù của cách triển khai này là trong test script này chứa tất cả logic test bao gồm logic tìm và tương tác trên Web elements.
Với cách triển khai này, test code rất "phẳng", mọi thứ đều nằm trên một file. Do đó việc đọc hiểu cũng trở nên đơn giản. Chuyện gì sẽ xảy ra nếu chúng ta có 10 test case khác cũng cần đăng nhập, hoặc có một element của source code đăng nhập cần update selector lúc maintain như vậy tìm 10 nơi và update lần lượt chúng.

Đây cũng chính là nhược điểm mà cách tiếp cận này chỉ phù hợp để làm ví dụ phần lớn không tồn tại ở automation project thật

Implement Login sử dụng POM

Về góc độ triển khai, chúng ta chia hai layer khác nhau: Layer thứ 1 là chứa logic tương ứng với kịch bản test của test. Layer thứ 2 chứa tất cả logic tìm và tương tác trên Web elements. Hai tầng này được thiết kế độc lập nhau

Test code tầng thứ 1: Sử dụng mocha test template

Phân bổ file spec: system/sign-in/valid-account-with-pom/valid-account-with-pom.spec.js

Copy
import { LoginPage } from '#e2e-core/system/pages';

describe('Page object model demo', () => {
    before('Sign in the system with the valid account', () => {
        const account = testData.userCredential;
        LoginPage
            .openLoginPage()                                            // Bước 1
            .inputUsernameTextbox(account.username)                     // Bước 2
            .inputPasswordTextbox(account.password)                     // Bước 3
            .clickOnLoginButton();                                      // Bước 4

        const result = LoginPage.hasUsername(account.username);
        assert.isTrue(result, `Cannot sign in with specified account`); // Bước 5
    });
});

Test code tầng thứ 2

Phân bổ file page object: system/sign-in/shared/login.po.js

Chúng ta có một Login class chứa tất cả action method của Web elements trên trang, thông qua login.js file; việc tương tác trên Web elements tách rời hoàn toàn với test spec

Copy
import { PageNavigator } from '#e2e-core/other/pages';
import { element } from '#e2e-core/wdio-wrapper/element';

class Login {
    constructor(url) {
        this._url = url;
    }
    /**
     *
     * @returns {Login}
     */
    openLoginPage() {
        browser.url(`${PageNavigator.rootUrl()}logout`);
        browser.url(this._url);

        return this;
    }
    /**
     *
     * @param {string} username
     * @returns {Login}
     */
    inputUsernameTextbox(username) {
        element('//input[@name="_account"]').setValue(username);

        return this;
    }
    /**
     *
     * @param {string} password
     * @returns {Login}
     */
    inputPasswordTextbox(password) {
        element('//input[@name="_password"]').setValue(password);

        return this;
    }
    /**
     *
     * @returns {Login}
     */
    clickOnLoginButton() {
        element('//*[@name="login-submit"]').click();
        element('//*[@name="login-submit"]').waitForDisplayed({
            reverse: true,
        });

        return this;
    }
    /**
     *
     * @param {string} username
     * @returns {boolean}
     */
    hasUsername(username) {
        const selector = `//*[@id="header"]//span[@title="${username}"]`;

        return element(selector).isDisplayed();
    }
}
module.exports = new Login(PageNavigator.garoonRootUrl());

Cuối cùng chúng ta có cấu trúc phân bổ như sau:

Kết quả của việc tổ chức source code cho test case đăng nhập hệ thống

Hình 4.0: Kết quả của việc tổ chức source code cho test case đăng nhập hệ thống

Như vậy, chúng ta vừa triển khai xong test code cho test case đăng nhập theo mô hình POM. Tiếp theo, chúng ta xem qua một số điểm giúp automation project có tính nhất quán giúp tăng cường khả năng maintain. Đặc những điểm bên dưới giúp developer làm việc chung hiệu quả

Một số quy tắc để khởi tạo POM

1/ Tên file *.po.js của POM được lấy theo định dạng tài nguyên duy nhất (URI: uniform resource identifier), và nó được phân bố theo cấu trúc thư mục của sản phẩm triển khai test để thuận tiện trong quá trình maintain. Ví dụ như sau:

  • URI: mydomain.com/schedule/index.csp
  • Tên POM cũng phát sinh như sau: schedule/index.po.js

2/ Mỗi màn hình tính năng (sản phẩm triển khai test tự động) được ánh xạ thành một POM duy nhất và cũng export duy nhất một instance

Ví dụ: system/sign-in/shared/login.po.jsLogin classexport một instance new Login() của Login class

Copy
class Login {
    constructor(url) {
        this._url = url;
    }
// ...
}
module.exports = new Login(PageNavigator.garoonRootUrl());

3/ Action methods trong page object phần lớn return về this

Copy
inputUsernameTextbox(username) {
    element('//input[@name="_account"]').setValue(username);

    return this; // Return chính nó
}

ngoại trừ phương thức như xác nhận sự tồn tại giá trị/phần tử trên trang, ví dụ code snippet bên dưới return về boolean

Copy
hasUsername(username) {
    const selector = `//*[@id="header"]//span[@title="${username}"]`;

    return element(selector).isDisplayed(); // Return boolean
}

Điều ngày giúp chúng ta gọi các actions method theo hình thức method chaining.

4/ Các action method trong page object nên được đặt tên theo nguyên lý bên dưới giúp dễ dàng maintain:

Cú pháp:

Action method = Hành động + Tên đối tượng + Hậu tố

Ví dụ tổng quan:

clickOnXXXSuffix

getXXXSuffix

hasXXX

Trong đó:

Trong đó XXX là tên của Web Element/context word giúp developer hình dung nó trên trang

Suffix cho từng DOM element chúng ta nên mô tả rõ ngay từ đầu. Điều này giúp developer có thể tham khảo để triển khai source code đồng nhất và dễ maintain. Ví dụ một số suffix phổ biến:

Web element Suffix
Text input field Textbox
Button Button
Link Link
Checkbox Checkbox
Tab Tab
Table Table
File Upload FileUpload
Radio Button Radio
Drop down list DropDown
List box Listbox
Label Label
Image Image
... ...

Ví dụ: action method kết thúc với suffix tương ứng

Copy
clickOnAddButton() {
   // ...
}

inputNotesTextarea(folderNotes) {
   // ...
}

hasProcessor(rowIndex, processorName) {
   // ...
}

5/ Trong page object KHÔNG implement các phương thức verify (assert)

Copy
import PageNavigator from '#e2e-core/other/pages/common/PageNavigator';
import { element } from '#e2e-core/wdio-wrapper/element';

class Login {
    // ...
    
    /**
     * @param {string} username
     * @returns {Login}
     */
    verifyUsernameExisted(username) {
        const result = this.hasUsername(username);
        assert.isTrue(result, `Cannot sign in with specified account`);
        
        return this;
    }
}
module.exports = new Login(PageNavigator.garoonRootUrl());

6/ Điền JSDoc đầy đủ, chính xác cho các action method có parameter

Ví dụ: Action method với JSDOC đầy đủ và chính xác giúp hạn chế lỗi tiềm ẩn trong chương trình

Copy
/**
  *
  * @param {string} password
  * @returns {Login}
  */
inputPasswordTextbox(password) {
  // ...
}

Ưu điểm triển khai theo POM

Ngoài ba yếu tố cốt lõi đã nêu ra ở phần mở đầu bài viết (Giảm sự trùng lặp mã nguồn, tăng tính tái sử dụng, giàu tính bảo trì), bên dưới là những ưu điểm khi triển khai theo hình thức POM:

  1. Chỉ một nơi định nghĩa logic action method cho Web Elements đa nơi sử dụng các action method của Web Elements
  2. Dễ dàng trong việc thay đổi công nghệ/kỹ thuật của action method. Vì test script/test spec chỉ quan tâm tới tổng thể (triệu gọi action method) mà không quan tâm tới implementation chi tiết bên trong.
  3. Áp dụng lập trình hướng đối tượng trong cách triển khai, điều này giúp phần lớn lập trình viên nhanh chóng tiếp cận và phát triển thêm test case mà “không cần đào tạo nhiều”.
  4. Mô hình triển khai đơn giản và phổ biến nên cũng có thể dễ dàng maintain project qua nhiều thế hệ lập trình viên; hay nói cách khác là mô hình phù hợp với test automation project đi đường dài

Nhược điểm triển khai theo POM

  1. Phát sinh thêm Page object file trong cấu trúc source code

  2. Cứng nhắc và bùng nổ source code
    Quay lại ví dụ login ở trên, chúng ta định nghĩa ra những action method một cách cứng nhắc như

    Copy
    /**
      *
      * @param {string} password
      * @returns {Login}
      */
    inputPassword(password) {
      // ...
    }

    vậy chúng ta muốn có action method như clear password của password Web Element thì chuyện gì sẽ xảy ra? Như vậy phải phát sinh mới action method

    1. Việc triệu gọi actions method ở test spec vẫn còn dài và lặp lại ở nhiều nơi. Điều này sẽ dẫn đến nhận định "test spec chỉ quan tâm tới tính tổng thể mà không quan tâm đến chi tiết bên trong" thật sự chưa trọn vẹn. Do đó ở những bài viết tiếp theo sẽ có cách triển khai khắc phục điều này thông qua test flow - một phương pháp tổ chức các actions trong automation testing của Cybozu đang áp dụng.

    Lời kết

    POM là một model phổ biến và tin dùng cho những bạn mới bắt đầu test automation. Tại Cybozu, POM cũng đang được triển khai trong Garoon’s e2e testing. Nhiều bài viết và nhiều kiến thức được viết về POM nhưng nếu cần phải nhớ, phải hiểu chúng ta có thể nhớ những điểm quan trọng như:

    • PageObject mô tả các hành động của Web elements tương ứng bằng test code như khi thực hiện manual trên màn hình
    • Không implement các phương thức verify(assert) trong POM
    • Các phương thức trong POM nên trả về chính nó để triển khai test script theo dạng method chaining
    • Cuối cùng POM chỉ nên chứa action methods và POM không phải là cách triển khai tốt nhất. Nó chỉ là cách triển khai phổ biến và dễ dàng tiếp cận với mọi người trong tự động hóa kiểm thử

Các bài viết cùng chủ đề