Ở bài cài Appium và kết nối Android, mục tiêu của chúng ta chỉ là tạo được một Appium session và nhìn test pass. Bài này mới bắt đầu viết một test case có đầu có đuôi: mở màn hình Search của ApiDemos, nhập nội dung, gửi tìm kiếm và kiểm tra kết quả.
Nếu bạn đã quen SeleniumLibrary thì cảm giác ban đầu sẽ khá giống: vẫn locator, action, wait, assertion. Nhưng đừng bê nguyên tư duy browser sang mobile. Một ứng dụng native không có HTML, không có CSS selector và cũng chẳng quan tâm thẻ div của bạn đẹp đến đâu.

Ba vùng quan trọng nằm cạnh nhau: màn hình thiết bị, XML hierarchy và locator gợi ý. Ảnh: Appium Inspector (Apache-2.0). Ảnh dùng Android Settings để minh họa cơ chế; test trong bài vẫn chạy với ApiDemos.
“DOM mobile” thực ra là gì?
Dân automation thường gọi cây element trên mobile là DOM cho nhanh miệng. Nói chính xác hơn, với native Android, UiAutomator2 lấy một XML UI hierarchy từ accessibility và automation framework của Android. Appium trả hierarchy đó qua page source.
Một node có thể trông như sau:
<android.widget.EditText
resource-id="io.appium.android.apis:id/txt_query_prefill"
text=""
clickable="true"
enabled="true"
bounds="[24,180][1056,312]" />
Đây không phải HTML. resource-id, content-desc, class, text, clickable và bounds là dữ liệu Appium dùng để tìm hoặc tương tác với element. Khi màn hình đổi, page source cũng có thể đổi theo.
Một test case mobile gồm những phần nào?
Phần mở và đóng session là setup/teardown. Phần giữa mới là logic của test. Trộn cả bảy bước vào từng test case vẫn chạy, nhưng sau năm case bạn sẽ có năm cục capability giống nhau nằm chình ình trong project.
Tách cấu hình USB và AVD
Tạo variables/android.py:
import os
APPIUM_URL = os.getenv("APPIUM_URL", "http://127.0.0.1:4723")
ANDROID_TARGET = os.getenv("ANDROID_TARGET", "avd")
ANDROID_UDID = os.getenv("ANDROID_UDID", "")
ANDROID_AVD = os.getenv("ANDROID_AVD", "Pixel_7_API_35")
APK_PATH = os.path.abspath("demoapp/ApiDemos-debug.apk")
def get_variables():
capabilities = {
"platformName": "Android",
"automationName": "UiAutomator2",
"app": APK_PATH,
"appPackage": "io.appium.android.apis",
"appActivity": ".app.SearchInvoke",
"autoGrantPermissions": True,
}
if ANDROID_TARGET == "usb":
if not ANDROID_UDID:
raise ValueError("ANDROID_UDID is required when ANDROID_TARGET=usb")
capabilities["udid"] = ANDROID_UDID
elif ANDROID_TARGET == "avd":
capabilities["avd"] = ANDROID_AVD
else:
raise ValueError("ANDROID_TARGET must be 'usb' or 'avd'")
return {
"APPIUM_URL": APPIUM_URL,
"CAPABILITIES": capabilities,
}
Robot Framework gọi get_variables() khi import variable file. Dictionary ${CAPABILITIES} luôn có phần chung, sau đó mới bổ sung udid hoặc avd tùy target.
Với USB:
$env:ANDROID_TARGET = 'usb'
$env:ANDROID_UDID = 'SERIAL_TU_ADB_DEVICES'
Với AVD:
$env:ANDROID_TARGET = 'avd'
$env:ANDROID_AVD = 'Pixel_7_API_35'
Test case không phải chứa IF target là điện thoại... ELSE target là emulator.... Nó chỉ nhận một bộ capability đã hoàn chỉnh.
Script hoàn chỉnh
Tạo tests/search.robot:
*** Settings ***
Library AppiumLibrary timeout=10s
Variables ../variables/android.py
Suite Setup Mở ApiDemos
Suite Teardown Close All Applications
*** Variables ***
${QUERY_INPUT} id=io.appium.android.apis:id/txt_query_prefill
${SEARCH_BUTTON} id=io.appium.android.apis:id/btn_start_search
${RESULT_TEXT} id=android:id/search_src_text
*** Test Cases ***
Tìm Kiếm Với Nội Dung Đã Nhập
[Tags] smoke android
Input Text ${QUERY_INPUT} Robot Framework
Click Element ${SEARCH_BUTTON}
Wait Until Page Contains Element ${RESULT_TEXT} timeout=10s
Element Text Should Be ${RESULT_TEXT} Robot Framework
Capture Page Screenshot search-result.png
*** Keywords ***
Mở ApiDemos
Open Application
... ${APPIUM_URL}
... appium:options=${CAPABILITIES}
Suite Setup mở đúng một session cho suite. Suite Teardown đóng mọi session kể cả khi assertion fail. Trong dự án thật, có trường hợp cần tạo session mới cho từng test để bảo đảm isolation, nhưng đó là quyết định về dữ liệu và chi phí khởi động; không phải cứ thấy chữ best practice là đóng mở app liên hoàn như bật tắt đèn disco.
appium:options=${CAPABILITIES} gom capability vào dictionary. Cách này tránh một keyword dài hai chục dòng continuation khi capability tăng lên.
Locator nên chọn theo thứ tự nào?
Resource-id
Input Text id=io.appium.android.apis:id/txt_query_prefill Robot Framework
resource-id thường là lựa chọn đầu tiên vì nó do ứng dụng định danh và không phụ thuộc vị trí element trên màn hình. Nếu đội Android có thể bổ sung ID ổn định cho element cần test, hãy nói chuyện với họ trước khi viết một XPath dài bằng sớ Táo quân.
Accessibility id
Click Element accessibility_id=App
Trên Android, accessibility id thường ánh xạ tới content-desc. Nó vừa thuận tiện cho automation, vừa cho thấy element đã có tên phục vụ accessibility. Tuy nhiên đừng tự động coi mọi text hiển thị là accessibility id; phải kiểm tra page source.
Android UIAutomator
Click Element
... android=new UiSelector().text("App").className("android.widget.TextView")
Selector này chạy theo cơ chế Android và hữu ích khi không có ID nhưng có thuộc tính native đủ ổn định. Đổi ngôn ngữ ứng dụng là locator theo text có thể đi bụi, nên phải biết mình đang đánh đổi cái gì.
XPath
Click Element
... xpath=//android.widget.TextView[@text="App"]
XPath không bị cấm. Nó chỉ thường chậm hơn và dễ gãy nếu bám vào cấu trúc cha-con hoặc index. XPath ngắn dựa trên thuộc tính rõ ràng còn đỡ; //android.widget.FrameLayout[1]/android.widget... kéo qua mười tầng thì giao diện chỉ cần hắt hơi là locator ngã.
Action, wait và assertion không phải một thứ
Click Element ${SEARCH_BUTTON}
Wait Until Page Contains Element ${RESULT_TEXT} timeout=10s
Element Text Should Be ${RESULT_TEXT} Robot Framework
Click Elementgửi action.Wait Until Page Contains Elementchờ hệ thống đạt điều kiện có thể kiểm tra.Element Text Should Bexác nhận kết quả nghiệp vụ mong đợi.
Nếu viết thế này:
Click Element ${SEARCH_BUTTON}
Sleep 5s
Element Text Should Be ${RESULT_TEXT} Robot Framework
thì test luôn trả phí năm giây dù app phản hồi trong 200 ms, nhưng vẫn fail nếu máy chậm mất sáu giây. Sleep phù hợp khi bạn cố ý quan sát hoặc chờ một khoảng thời gian là chính yêu cầu; nó không phải wait strategy.
Lấy source khi đang viết locator
${source}= Get Source
Log ${source}
Get Source yêu cầu Appium lấy XML hierarchy hiện tại. Nó hữu ích để debug nhưng khá nặng; đừng nhét vào mọi keyword chỉ vì “biết đâu cần”. Khi một locator không tìm thấy, chụp screenshot và source tại đúng thời điểm lỗi sẽ có giá trị hơn việc đọc source từ ba bước trước.
AppiumLibrary mặc định chụp screenshot khi keyword fail. Có thể chủ động thêm:
Capture Page Screenshot before-submit.png
${source}= Get Source
Log ${source} level=DEBUG
Chạy và đọc kết quả
Chạy toàn bộ file:
robot --pythonpath . --outputdir results tests\search.robot
Chạy riêng tag smoke:
robot --pythonpath . --include smoke --outputdir results tests
Khi fail, đọc theo thứ tự:
- terminal để biết test và keyword nào fail;
results\log.htmlđể xem từng keyword, argument và screenshot;- Appium server log để biết request đã tới driver chưa;
adb logcatnếu Appium báo UiAutomator2 hoặc ứng dụng Android có vấn đề.
report.html cho bức tranh tổng, còn log.html mới là chỗ mổ xẻ. Nhìn mỗi dòng Element not found ở terminal rồi thay XPath ngẫu nhiên cũng giống sửa xe bằng cách đá từng cái lốp.