Bỏ qua, đến nội dung

Á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:

  1. Định nghĩa selector trong các file JavaScript bằng đối tượng output.
  2. Nạp các định nghĩa đó khi chạy.
  3. Tham chiếu biến (không phải id thô) 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:

  1. Lưu id, text hoặc pattern regex trong các file .js bằng output.
  2. Nhóm selector theo logic: theo màn hình, tính năng hoặc nền tảng.
  3. Dùng runScript, hoặc một flow loader chung, để nạp các selector vào flow.
  4. Tham chiếu selector bằng ${output...} trong tapOn, inputText, assertVisible và 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.

javascript
// 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.

yaml
# 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:

javascript
// 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.

yaml
- 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ành fab_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, submit hay button.

  • 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ư:

    javascript
    output.welcomeBanner = 'Welcome back, .*'

    và dùng lại ở nhiều flow.

  • Fail dễ đọc: khi flow fail, tên như loginBtn dễ 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 Basket

Dù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.

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.yaml

Chiế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:

yaml
# elements/loadElements.yaml
appId: com.example.app
---
- runScript: cards.js
- runScript: login.js
- runScript: home.js
- runScript: nav.js

Gọ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.

yaml
---
- 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.

yaml
- runFlow:
    when: 
        platform: Android 
    commands:
      - runScript: android/elements/login.js

- runFlow:
    when: 
        platform: iOS
    commands:
      - runScript: ios/elements/login.js

Mỗi nền tảng nạp selector của mình, còn các step của flow không đổi.

yaml
- tapOn:
    id: ${output.login.loginBtn}

Các trang liên quan: