Mở đầu — vì sao bài này quan trọng
Ở Bài 7, bạn đã dựng được môi trường Appium và chạy thành công phiên session đầu tiên. Nhưng dựng được Appium mới chỉ là mở cửa bước vào nhà — muốn "điều khiển" được ứng dụng, bạn phải trỏ đúng vào từng phần tử (element) trên màn hình: ô nhập username, nút "Đăng nhập", cái checkbox "Ghi nhớ tôi"… Cách bạn xác định và "chỉ tay" vào từng phần tử đó chính là locator strategy — chủ đề trọn vẹn của bài này.
Nghe thì đơn giản, nhưng đây lại là nơi 80% test tự động thất bại trong thực tế. Một team QA ở một fintech tại TP.HCM từng kể với tôi rằng bộ regression 300 test case của họ đỏ (fail) mỗi sáng, không phải vì app hỏng mà vì locator viết ẩu: hôm nay dev đổi thứ tự một cái LinearLayout, thế là XPath dài loằng ngoằng gãy hàng loạt. Họ mất trung bình 2 giờ mỗi ngày chỉ để "sửa test cho xanh lại". Đó là cái giá của việc không hiểu locator strategy.
Nắm vững bài này, bạn sẽ chọn được locator vừa ổn định (không gãy khi UI thay đổi nhẹ), vừa nhanh (Appium tìm phần tử tính bằng mili-giây thay vì vài giây), lại chạy được trên cả iOS lẫn Android với ít khác biệt nhất có thể. Đây là kỹ năng nền tảng mà mọi bài viết test sau này (login flow ở Bài 9, xử lý wait ở Bài 10) đều dựa lên.
Khái niệm cốt lõi
Trong Appium, mỗi lần bạn muốn thao tác với một phần tử, bạn gọi một hàm dạng driver.findElement(By.<strategy>, "<value>"). Trong đó strategy là cách tìm, còn value là chuỗi định danh. Toàn bộ nghệ thuật nằm ở việc chọn đúng cặp này.
Bảng các strategy phổ biến
| Strategy | Ý nghĩa | Android (dựa trên) | iOS (dựa trên) |
|---|---|---|---|
accessibility id | ID trợ năng, xuyên nền tảng | content-desc | accessibilityIdentifier / label |
id | ID tài nguyên | resource-id | name (hạn chế) |
class name | Loại widget | android.widget.Button… | XCUIElementTypeButton… |
xpath | Đường dẫn cây XML | có | có |
-android uiautomator | UiSelector API gốc | chỉ Android | — |
-ios predicate string | NSPredicate query | — | chỉ iOS |
-ios class chain | Chuỗi lớp iOS | — | chỉ iOS |
1. accessibility id — lựa chọn số một
Đây là locator tôi luôn khuyên dùng đầu tiên, vì nó là chiếc cầu hiếm hoi bắc qua cả hai nền tảng. Trên Android nó ánh xạ vào thuộc tính content-desc (content description — mô tả dành cho trình đọc màn hình), còn trên iOS nó ánh xạ vào accessibilityIdentifier.
Ưu điểm cực lớn: nếu đội dev đặt cùng một giá trị accessibility id cho nút "Đăng nhập" trên cả app iOS và Android (ví dụ "btn_login"), bạn viết một dòng locator dùng chung cho cả hai:
driver.findElement(AppiumBy.accessibilityId("btn_login")).click();
Nó nhanh (Appium tra thẳng thuộc tính, không phải duyệt cây), ổn định (không phụ thuộc vị trí trong layout), và còn "một mũi tên trúng hai đích" là buộc app phải có accessibility label — tốt cho cả người khuyết tật. Nhược điểm duy nhất: nó chỉ tồn tại nếu dev chịu đặt. Vì thế phần "Hướng dẫn từng bước" bên dưới sẽ nói cách bạn thương lượng với dev để có sẵn các id này.
2. id — resource-id trên Android
Trên Android, mỗi widget thường có resource-id dạng com.vng.zalo:id/login_button. Đây là locator nhanh và khá ổn trên Android. Cú pháp:
driver.findElement(AppiumBy.id("com.vng.zalo:id/login_button"));
Lưu ý: bạn có thể bỏ phần package prefix, Appium tự thêm. Nhược điểm: id trên iOS gần như vô dụng (iOS không có khái niệm resource-id thật sự), nên đây là locator "nghiêng về Android".
3. class name — hiếm khi dùng một mình
class name trỏ vào loại widget: android.widget.EditText, XCUIElementTypeTextField… Vấn đề là một màn hình có thể có 10 cái EditText, nên dùng một mình gần như vô dụng cho việc định danh duy nhất. Nó chỉ hữu ích khi kết hợp trong XPath hoặc khi bạn cần lấy danh sách (findElements) tất cả phần tử cùng loại.
4. xpath — mạnh nhất nhưng nguy hiểm nhất
XPath cho phép bạn diễn tả bất kỳ đường dẫn nào trong cây XML của UI. Nó là "con dao đa năng": làm được mọi thứ, kể cả tìm theo text hiển thị.
// Tìm nút theo text (hữu ích khi không có id)
driver.findElement(AppiumBy.xpath("//android.widget.Button[@text='Đăng nhập']"));
Nhưng XPath có hai điểm yếu chí mạng. Thứ nhất, chậm: Appium phải serialize toàn bộ cây XML rồi query, trên màn hình phức tạp có thể mất 1–3 giây mỗi lần. Thứ hai, dễ gãy nếu bạn viết absolute path kiểu /hierarchy/android.widget.FrameLayout[1]/…[3]/…[2] — chỉ cần dev chèn thêm một view, mọi index lệch hết. Nguyên tắc vàng: nếu buộc phải dùng XPath, hãy dùng relative XPath dựa trên thuộc tính (@text, @content-desc, @resource-id) chứ tuyệt đối không dùng đường dẫn tuyệt đối theo index.
5. -android uiautomator — vũ khí gốc của Android
Đây là locator dùng trực tiếp API UiSelector của Android, cực nhanh và linh hoạt, chạy ngay trong tiến trình UiAutomator2 nên không tốn công serialize XML như XPath.
driver.findElement(AppiumBy.androidUIAutomator(
"new UiSelector().textContains(\"Đăng nhập\")"));
Nó hỗ trợ textContains, resourceIdMatches (regex), descriptionStartsWith… Rất mạnh cho các trường hợp text động. Nhược điểm: chỉ chạy Android.
6. -ios predicate string và -ios class chain — cặp đôi của iOS
Trên iOS, khi accessibility id không đủ, hai locator này thay thế XPath và nhanh hơn XPath nhiều.
// Predicate: tìm theo thuộc tính
driver.findElement(AppiumBy.iOSNsPredicateString(
"type == 'XCUIElementTypeButton' AND name == 'Đăng nhập'"));
// Class chain: giống XPath nhưng tối ưu cho iOS
driver.findElement(AppiumBy.iOSClassChain(
"**/XCUIElementTypeButton[`name == 'Đăng nhập'`]"));
Quy tắc cho iOS: ưu tiên accessibility id → nếu cần lọc phức tạp dùng -ios predicate string → nếu cần đi theo phân cấp/thứ tự dùng -ios class chain → chỉ dùng xpath khi bất đắc dĩ.
Thứ tự ưu tiên tôi khuyên dùng
accessibility id(xuyên nền tảng, nhanh, ổn định)id(Android) — nhanh, ổn-android uiautomator/-ios predicate string(locator gốc, nhanh)-ios class chain(iOS, thay XPath)xpathrelative (chỉ khi không còn cách nào khác)xpathabsolute — không bao giờ
Tình huống thực tế
Ví dụ 1 — Bộ regression đỏ mỗi sáng ở một fintech TP.HCM
Team QA 6 người của một ví điện tử tại quận 1 duy trì bộ 300 test Appium cho app Android. Mỗi sáng vào làm, khoảng 40–60 case fail. Điều tra ra: 90% locator của họ là XPath absolute do một bạn junior sinh tự động từ Appium Inspector rồi copy nguyên xi, kiểu //android.widget.FrameLayout[2]/android.widget.LinearLayout[1]/…/android.widget.Button[3].
Chỉ cần dev thêm một banner khuyến mãi ở đầu màn hình Home, toàn bộ index con phía dưới lệch một bậc và hàng loạt test gãy. Chi phí ẩn: 2 giờ/ngày × 22 ngày × 6 người ≈ 264 giờ/tháng chỉ để vá test — gần bằng 1,5 nhân sự full-time.
Cách họ xử lý: QA lead ngồi lại với team dev, thống nhất mọi phần tử tương tác quan trọng phải có content-desc (tương ứng accessibility id) đặt sẵn trong code. Trong 3 sprint, họ refactor dần XPath sang accessibility id. Tỷ lệ fail buổi sáng giảm từ ~50 case xuống dưới 5. Bài học: locator không phải chuyện riêng của QA — nó là hợp đồng giữa dev và QA. Ổn định của test bắt đầu từ dòng code dev đặt id.
Ví dụ 2 — Một team dùng chung code cho iOS và Android tại Singapore
Một startup logistics ở Singapore có app tài xế trên cả hai nền tảng. Ban đầu họ viết hai bộ test riêng: iOS dùng XPath theo XCUIElementType…, Android dùng resource-id. Mỗi tính năng mới phải viết locator hai lần, và khi UI thay đổi phải sửa hai chỗ — công sức nhân đôi.
Giải pháp: họ ban hành quy ước đặt tên accessibilityIdentifier (iOS) và contentDescription (Android) giống hệt nhau cho mọi phần tử, ví dụ nút xác nhận giao hàng đều là "delivery_confirm_btn". Nhờ đó phần lớn test case dùng chung một tầng locator:
By CONFIRM_BTN = AppiumBy.accessibilityId("delivery_confirm_btn");
Kết quả: lượng code test giảm ~35%, và khi thiết kế lại màn hình giao hàng, họ chỉ sửa page object một lần. Bài học: accessibility id biến việc test đa nền tảng từ "hai dự án" thành "một dự án", nhưng chỉ khi có kỷ luật đặt tên thống nhất từ đầu.
Ví dụ 3 — Text tiếng Việt có dấu làm gãy locator
Một team e-commerce đặt locator nút theo text: //android.widget.Button[@text='Thanh toán']. Test chạy tốt trên máy dev nhưng fail trên CI. Điều tra ra hai nguyên nhân: (1) file test lưu không đúng UTF-8 nên chữ "toán" bị lệch mã; (2) app hỗ trợ đa ngôn ngữ — khi thiết bị CI đặt tiếng Anh, nút hiển thị "Payment" chứ không phải "Thanh toán", locator theo text đương nhiên không tìm thấy.
Họ chuyển sang accessibility id "btn_checkout" — một giá trị không đổi theo ngôn ngữ và không có dấu. Test xanh trở lại trên mọi cấu hình ngôn ngữ. Bài học: đừng bao giờ neo locator vào text hiển thị cho người dùng, đặc biệt với app đa ngôn ngữ hoặc text tiếng Việt có dấu — text là thứ dễ thay đổi nhất trên đời (Bài 27 về l10n/i18n sẽ đào sâu chủ đề này).
Hướng dẫn từng bước
Bước 1 — Mở Appium Inspector để "soi" cây phần tử. Kết nối tới session đang chạy, chọn phần tử trên màn hình. Panel bên phải hiện đủ thuộc tính: resource-id, content-desc, text, class, và cả gợi ý XPath.
Bước 2 — Áp dụng "thang ưu tiên" locator. Với phần tử vừa chọn, tự hỏi theo thứ tự: Có content-desc/accessibility id không? → dùng ngay. Không có nhưng có resource-id? → dùng id. Cả hai đều không? → chuyển sang -android uiautomator (Android) hoặc -ios predicate string (iOS). Hết cách mới tới XPath relative.
Bước 3 — Nếu thiếu id, ghi lại và gửi dev. Lập một danh sách "phần tử thiếu accessibility id" và đưa vào backlog dev. Đây là đầu tư một lần, lợi lâu dài.
Bước 4 — Viết locator dưới dạng hằng số trong Page Object. Đừng rải locator khắp test. Gom vào một class:
public class LoginPage {
static final By USERNAME = AppiumBy.accessibilityId("input_username");
static final By PASSWORD = AppiumBy.accessibilityId("input_password");
static final By LOGIN_BTN = AppiumBy.accessibilityId("btn_login");
}
Khi locator thay đổi, bạn sửa đúng một chỗ.
Bước 5 — Kiểm tra locator có duy nhất không. Dùng findElements (số nhiều) rồi kiểm tra size == 1. Nếu ra nhiều hơn 1, locator của bạn chưa đủ đặc trưng và sẽ gây flaky.
Bước 6 — Đo tốc độ. Nếu một locator mất trên 1 giây để tìm, gần như chắc chắn đó là XPath. Cân nhắc đổi sang locator gốc.
Lỗi thường gặp & mẹo
- Dùng XPath absolute copy từ Inspector. Đây là lỗi phổ biến nhất. Inspector gợi ý XPath tuyệt đối cho tiện, nhưng nó gãy ngay khi UI đổi. Luôn viết lại thành relative theo thuộc tính.
- Neo locator vào text hiển thị. Text đổi theo ngôn ngữ, theo A/B test, theo mùa (ví dụ nút "Mua ngay" đổi thành "Săn sale"). Dùng id ổn định thay vì text.
- Quên khác biệt nền tảng.
idchạy tốt Android nhưng gần như vô dụng iOS. Đừng bê nguyên locator Android sang iOS mà không kiểm tra. - Không xử lý phần tử động. Danh sách sản phẩm sinh id trùng nhau hoặc index thay đổi. Dùng
textContains,resourceIdMatches(regex) trong-android uiautomatorđể bắt theo mẫu. - Bỏ qua Vietnamese diacritics. Nếu buộc phải match text tiếng Việt, đảm bảo file source lưu UTF-8 và cân nhắc dùng
textContainsvới phần không dấu để giảm rủi ro. - Mẹo lớn nhất: đưa việc "đặt accessibility id" vào Definition of Done của dev. Locator ổn định là kết quả của quy trình, không phải của mẹo viết XPath khéo.
Bài tập thực hành
- Mở Appium Inspector trên một app bất kỳ (có thể là app demo). Chọn 5 phần tử tương tác và với mỗi cái, viết ra hai locator khác nhau, xếp theo thang ưu tiên. Ghi rõ tại sao locator số 1 tốt hơn.
- Tìm một phần tử không có accessibility id. Viết locator cho nó bằng
-android uiautomator(nếu Android) hoặc-ios predicate string(nếu iOS), rồi so sánh tốc độ tìm với một XPath tương đương (in ra thời gian chạy). - Viết một class
LoginPagegom 3 locator (username, password, nút login) dưới dạngaccessibility id, giả định app chạy được trên cả iOS và Android với cùng id. - Tạo một danh sách "backlog thiếu id" gồm ít nhất 3 phần tử bạn thấy nên có accessibility id, kèm giá trị id đề xuất để gửi cho dev.
Tóm tắt
Locator strategy là kỹ năng quyết định độ ổn định và tốc độ của toàn bộ bộ test Appium. Hãy nhớ thang ưu tiên: accessibility id (xuyên nền tảng, tốt nhất) → id (Android) → locator gốc -android uiautomator / -ios predicate string → -ios class chain → XPath relative → và không bao giờ dùng XPath absolute. Đừng neo locator vào text hiển thị, đặc biệt với app đa ngôn ngữ hay text tiếng Việt có dấu. Quan trọng nhất: locator ổn định bắt đầu từ dev đặt id — hãy biến nó thành thỏa thuận giữa QA và dev, gom locator vào Page Object để sửa một chỗ. Nắm chắc bài này, bạn đã sẵn sàng viết test login flow hoàn chỉnh ở Bài 9.