Áp dụng Page Object Model (POM)
Áp dụng mẫu Page Object Model để bộ flow lớn dễ bảo trì.
Khi số flow tăng lên, selector xuất hiện trong mọi file flow. Viết cứng id vào hàng chục file YAML thì lúc đầu rất nhanh, nhưng khi developer đổi tên một nút hoặc một nhãn, bạn phải sửa nhiều flow chỉ vì một thay đổi nhỏ trên UI. Cách này không mở rộng được.
Page Object Model (POM) giải quyết đúng vấn đề này. POM là cách tổ chức flow: bạn gom tên các phần tử của mỗi màn hình ("page") vào một file riêng, và flow chỉ dùng tên đó thay vì id thô. Khi UI đổi, bạn sửa một chỗ, và mọi flow phụ thuộc vào nó vẫn chạy được.
Trong Astra Scout, các file này là file JavaScript. Chúng lưu selector vào đối tượng output (nơi chứa biến dùng chung giữa các step của flow).
Ý tưởng cốt lõi
Nguyên tắc của POM rất đơn giản:
Tách phần tử là gì khỏi cách test tương tác với nó.
Bạn làm được điều này bằng cách:
- Định nghĩa selector trong các file JavaScript bằng đối tượng
output. - Nạp các định nghĩa đó khi chạy.
- Tham chiếu biến (không phải
idthô) trong flow.
Flow mô tả việc người dùng muốn làm, không lộ chi tiết kỹ thuật.
Quy trình cơ bản
Quy trình áp dụng POM:
- Lưu
id,texthoặc pattern regex trong các file.jsbằngoutput. - Nhóm selector theo logic: theo màn hình, tính năng hoặc nền tảng.
- Dùng
runScript, hoặc một flow loader chung, để nạp các selector vào flow. - Tham chiếu selector bằng
${output...}trongtapOn,inputText,assertVisiblevà các lệnh khác.
Các phần sau đi qua từng bước.
1. Định nghĩa page object đầu tiên
Bắt đầu bằng việc tạo một file JavaScript đại diện cho một màn hình trong app, ví dụ màn hình đăng nhập.
// login.js
output.login = {
email: 'email_text',
password: 'password_text',
loginBtn: 'loginButton',
registerBtn: 'registerButton'
}File này là "từ điển" của màn hình Login. Key cho biết phần tử là gì, value cho Astra Scout biết cách tìm nó.
Dùng runScript để nạp file vào flow. Sau đó flow không còn ID thô, và mỗi dòng cho thấy rõ ý định.
# login_flow.yaml
---
- runScript: login.js
- tapOn:
id: ${output.login.email}
- inputText: "simon@maestro.com"
- tapOn:
id: ${output.login.password}
- inputText: ${PASSWORD}
- tapOn:
id: ${output.login.loginBtn}2. Tổ chức màn hình phức tạp bằng object lồng nhau
Với màn hình phức tạp, một danh sách selector phẳng khó quản lý. Bạn có thể lồng các object, theo đúng cấu trúc của UI. Cách này hợp với các thành phần dùng lại như card, toolbar hay menu điều hướng. Ví dụ sau mô tả một card:
// cards.js
output.cards = {
cardTitle: 'card_title',
virtualCards: {
createBtn: 'btn_create_virtual',
details: {
number: 'card_number',
expiry: 'card_expiry'
}
}
}Cấu trúc này khớp với phân cấp UI: các phần tử liên quan nằm cùng nhau và không bị trùng tên. Trong flow, bạn đi theo đường dẫn như với một object JavaScript. Chính đường dẫn đã cho biết phần tử nằm ở đâu trên UI, nên flow dễ đọc và dễ sửa hơn.
- runScript: cards.js
- tapOn:
id: ${output.cards.virtualCards.createBtn}
- assertVisible:
id: ${output.cards.virtualCards.details.number}Lợi ích và đánh đổi của POM
POM giúp bộ test dễ bảo trì hơn, nhưng đổi lại có một nhược điểm:
Sửa ở một chỗ: nếu
btn_create_virtualđổi thànhfab_add, bạn cập nhật một file, không phải hàng chục test.Không trùng tên: object lồng nhau tránh va chạm giữa các tên phổ biến như
title,submithaybutton.Hỗ trợ regex: Astra Scout hiểu chuỗi selector là regex (mẫu mô tả chuỗi chữ, xem Selectors). Bạn có thể lưu cả mẫu cho chữ thay đổi, như:
javascriptoutput.welcomeBanner = 'Welcome back, .*'và dùng lại ở nhiều flow.
Fail dễ đọc: khi flow fail, tên như
loginBtndễ hiểu hơn ID thô.
Nhược điểm: flow kém trực quan hơn một chút. Viết trực tiếp thì dễ đọc nhưng dễ hỏng khi UI đổi:
- tapOn: Add to BasketDùng POM thì khó đọc hơn một chút nhưng bền:
- tapOn: ${output.itemPage.addButton}Chọn POM khi bộ test lớn và cần bền hơn là cần đọc trực tiếp từng dòng YAML.
Cấu trúc thư mục khuyến nghị
Tách selector khỏi flow, ví dụ trong một thư mục elements riêng.
Với app chạy cả Android và iOS, nên chia thư mục theo nền tảng. Flow giữ nguyên cho cả hai nền tảng, còn selector riêng theo từng nền tảng.
android/
elements/
login.js
home.js
loadElements.yaml
tests/
login_flow.yaml
ios/
elements/
login.js
home.js
loadElements.yaml
tests/
login_flow.yamlChiến lược loader
Khi project lớn lên, gọi runScript trong từng flow trở nên lặp lại và dễ sót. Dùng một flow loader. Việc duy nhất của loader là nạp mọi page object một lần:
# elements/loadElements.yaml
appId: com.example.app
---
- runScript: cards.js
- runScript: login.js
- runScript: home.js
- runScript: nav.jsGọi loader như một subflow ở đầu flow. Mọi selector được nạp sẵn, nên flow nào cũng bắt đầu giống nhau và không bị sót selector nào.
---
- runFlow: elements/loadElements.yaml
- launchApp
# Mọi selector giờ đã dùng được qua ${output...}Xử lý khác biệt giữa các nền tảng
Android và iOS thường dùng ID khác nhau cho cùng một phần tử. Với POM, bạn giữ nguyên tên và chỉ đổi file nạp vào.
- runFlow:
when:
platform: Android
commands:
- runScript: android/elements/login.js
- runFlow:
when:
platform: iOS
commands:
- runScript: ios/elements/login.jsMỗi nền tảng nạp selector của mình, còn các step của flow không đổi.
- tapOn:
id: ${output.login.loginBtn}Nội dung liên quan
Các trang liên quan:
- JavaScript: dùng JavaScript để lưu dữ liệu dùng chung.
- Tham số và hằng số: đối tượng
outputđược giữ qua các flow như thế nào. - Điều kiện: dùng
whencho phần riêng theo nền tảng.
