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ụ:
clickOnAddButton() {
// ...
}
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à WebdriverIOCybozu 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
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
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
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
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
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
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:
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.js
có Login class
và export
một instance new Login()
của Login
class
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
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
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
clickOnAddButton() {
// ...
}
inputNotesTextarea(folderNotes) {
// ...
}
hasProcessor(rowIndex, processorName) {
// ...
}
5/ Trong page object KHÔNG implement các phương thức verify (assert)
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
/**
*
* @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:
- 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 - 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. - Á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”.
- 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
-
Phát sinh thêm Page object file trong cấu trúc source code
-
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ư/** * * @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- 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ử