Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 12 — Espresso — Native Android Testing

Đã đọc 0/14 mục · còn khoảng 10 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Ở bài trước, chúng ta đã tìm hiểu XCUITest — công cụ native cho iOS. Bây giờ hãy bước sang thế giới Android, nơi Google cung cấp một framework tương đương nhưng có triết lý thiết kế rất riêng: Espresso.

Nếu bạn từng viết test bằng Appium và phải chịu đựng những lần test "chập chờn" — chạy 10 lần thì rớt 2 lần dù app không có lỗi gì — thì Espresso sẽ là một luồng gió mới. Lý do nằm ở kiến trúc: Espresso chạy in-process (cùng tiến trình với app), thay vì giao tiếp qua HTTP như Appium. Điều này cho phép Espresso "biết" chính xác khi nào UI thread rảnh rỗi và tự động đồng bộ hóa các thao tác — thứ mà dân QA gọi là "auto-synchronization". Kết quả là test nhanh hơn nhiều lần và gần như không bị flaky.

Với một QA đang làm việc tại các công ty sản phẩm Android ở Việt Nam — như MoMo, ZaloPay, VNPay, hay các đội Android tại Tiki, Shopee VN — Espresso là kỹ năng gần như bắt buộc nếu bạn muốn tham gia sâu vào automation phía dev. Đây cũng là framework mà đội developer Android thường yêu cầu QA phối hợp, vì nó nằm ngay trong dự án Android Studio. Hiểu Espresso giúp bạn "nói cùng ngôn ngữ" với lập trình viên và tham gia vào tầng test đáng tin cậy nhất của kim tự tháp kiểm thử di động.

Khái niệm cốt lõi

Espresso là gì và vì sao nó "không flaky"

Espresso là framework kiểm thử UI do Google phát triển, nằm trong bộ AndroidX Test. Nó dùng để viết các bài test UI tự động cho một app Android cụ thể — nghĩa là test white-box, chạy trên chính source code của app đó (khác với Appium là black-box, gắn ngoài).

Điểm mấu chốt làm nên tên tuổi Espresso là cơ chế auto-synchronization (tự đồng bộ). Espresso theo dõi hàng đợi công việc trên UI thread (main thread) và AsyncTask thread pool. Trước khi thực hiện bất kỳ hành động hay kiểm tra nào, Espresso chờ cho đến khi:

  • UI thread không còn message nào đang chờ xử lý,
  • Không còn AsyncTask nào đang chạy,
  • Các IdlingResource (nếu bạn khai báo) đều báo "idle".
Nói cách khác, Espresso không bao giờ thao tác lên một màn hình còn đang vẽ dở hay đang chờ animation. Đây chính là lý do bạn gần như không cần viết Thread.sleep() hay wait thủ công — thứ gây ra 80% test flaky khi dùng Appium.

Ba trụ cột: ViewMatchers, ViewActions, ViewAssertions

API của Espresso được xây quanh một câu tiếng Anh rất dễ đọc. Câu mẫu kinh điển:

onView(withId(R.id.login_button))   // ViewMatcher: tìm view nào?
    .perform(click());                // ViewAction: làm gì với nó?

onView(withText("Đăng nhập thành công"))
    .check(matches(isDisplayed()));   // ViewAssertion: kiểm tra điều gì?

Ba nhóm API này là toàn bộ nền tảng bạn cần nắm:

  • ViewMatchers — trả lời câu hỏi "tìm view nào?". Ví dụ: withId(), withText(), withHint(), withContentDescription(), isDisplayed(). Bạn có thể ghép nhiều matcher bằng allOf(...).
  • ViewActions — mô tả "làm gì". Ví dụ: click(), typeText("..."), scrollTo(), swipeUp(), closeSoftKeyboard().
  • ViewAssertions — kiểm tra "kết quả đúng chưa". Chủ yếu qua matches(...), ví dụ matches(isDisplayed()), matches(withText("...")), hoặc doesNotExist().

onData — làm việc với AdapterView

Với các danh sách dài render qua ListView, GridView hay adapter (không phải mọi phần tử đều nằm trong view hierarchy cùng lúc), bạn không dùng onView được mà phải dùng onData(). Đây là điểm nhiều bạn mới hay vấp:

onData(hasToString("Ví MoMo"))
    .inAdapterView(withId(R.id.wallet_list))
    .perform(click());

Lưu ý: RecyclerView không phải AdapterView, nên với RecyclerView bạn dùng module riêng espresso-contrib với RecyclerViewActions.

IdlingResource — mảnh ghép cho tác vụ bất đồng bộ

Auto-sync của Espresso chỉ biết về UI thread và AsyncTask. Nếu app của bạn gọi API qua Retrofit/OkHttp (chạy trên thread pool riêng), tải ảnh qua Glide, hay dùng RxJava/Coroutine, Espresso không tự biết khi nào chúng xong. Đây là lúc bạn cần IdlingResource — một "cảm biến" bạn tự cài để báo cho Espresso biết "tôi đang bận" / "tôi rảnh rồi". Đây là kiến thức phân biệt QA mới và QA có kinh nghiệm với Espresso.

Tình huống thực tế

Tình huống 1 — Đội ví điện tử và bài toán flaky test khi gọi API

Một đội QA tại một công ty ví điện tử ở TP.HCM (tạm gọi là công ty A, quy mô ~40 kỹ sư) ban đầu dùng Appium cho toàn bộ luồng nạp tiền. Suite 120 test chạy mất 55 phút và có tỷ lệ rớt "giả" khoảng 12% mỗi lần chạy CI — tức khoảng 14 test đỏ mà thực chất app không lỗi. Mỗi sáng, một QA phải mất gần một tiếng chỉ để chạy lại và xác nhận đâu là lỗi thật.

Đội quyết định port các luồng cốt lõi (đăng nhập, xem số dư, xác nhận giao dịch) sang Espresso. Kết quả sau hai sprint: 45 test Espresso chạy chỉ trong 6 phút, tỷ lệ flaky giảm xuống dưới 1%. Tuy nhiên họ vấp một chỗ: màn hình "Số dư" gọi API balance qua Retrofit, và test kiểm tra số dư liên tục rớt vì Espresso assert trước khi API trả về. Giải pháp là đăng ký một OkHttp3IdlingResource để Espresso chờ mọi request mạng hoàn tất.

Bài học: Espresso xử lý flaky cực tốt cho UI thuần, nhưng với tác vụ mạng bạn bắt buộc phải khai báo IdlingResource — nếu không sẽ tự tạo ra một loại flaky mới.

Tình huống 2 — Test ô nhập liệu tiếng Việt có dấu tại một app đặt xe

Một đội Android tại công ty đặt xe (công ty B) cần kiểm thử ô tìm kiếm địa chỉ, nơi người dùng gõ "Nguyễn Huệ", "Đường Cách Mạng Tháng Tám". Khi viết test Espresso đầu tiên, họ dùng typeText("Nguyễn Huệ") và test rớt vì bàn phím ảo che mất nút "Tìm", khiến click() sau đó bấm nhầm.

Bài học họ rút ra là chuỗi hành động phải đúng thứ tự và phải chủ động đóng bàn phím:

onView(withId(R.id.search_input))
    .perform(typeText("Nguyễn Huệ"), closeSoftKeyboard());
onView(withId(R.id.btn_search)).perform(click());

Họ cũng phát hiện một điểm tinh tế: với input tiếng Việt bỏ dấu bằng bộ gõ Telex/VNI, typeText gửi ký tự Unicode trực tiếp nên không mô phỏng đúng hành vi gõ Telex thật của người dùng. Với các test cần kiểm tra chính bộ gõ, họ chuyển sang test thủ công hoặc Appium — còn Espresso chỉ dùng để verify logic sau khi text đã có. Đây là ranh giới quan trọng: Espresso test logic app, không test bàn phím hệ thống.

Tình huống 3 — RecyclerView danh sách sản phẩm ở app thương mại điện tử

Một QA junior tại đội thương mại điện tử (công ty C) cần bấm vào sản phẩm thứ 15 trong danh sách. Bạn ấy dùng onView(withText("Áo thun")) và test rớt với lỗi NoMatchingViewException — vì sản phẩm thứ 15 chưa được render (nằm ngoài màn hình, RecyclerView chỉ giữ vài item hiển thị).

Sau khi được mentor hướng dẫn, bạn ấy dùng espresso-contrib:

onView(withId(R.id.product_list))
    .perform(RecyclerViewActions.actionOnItemAtPosition(15, click()));

Bài học: Với RecyclerView, đừng bao giờ giả định phần tử đã có trong view hierarchy. Phải cuộn tới nó bằng RecyclerViewActions (scrollToPosition, actionOnItemAtPosition). Đây là lỗi kinh điển của gần như mọi người mới học Espresso.

Hướng dẫn từng bước

Dưới đây là quy trình viết một test Espresso hoàn chỉnh cho luồng đăng nhập.

Bước 1 — Thêm dependency. Trong file build.gradle (module app):

androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1' // cho RecyclerView
androidTestImplementation 'androidx.test.ext:junit:1.1.5'

Bước 2 — Đặt test đúng thư mục. Espresso là instrumented test, phải nằm trong src/androidTest/java/... (không phải src/test/ — đó là unit test chạy trên JVM). Đây là lỗi cấu trúc thư mục rất hay gặp.

Bước 3 — Khai báo runner và launch Activity.

@RunWith(AndroidJUnit4.class)
public class LoginTest {

    @Rule
    public ActivityScenarioRule<LoginActivity> rule =
        new ActivityScenarioRule<>(LoginActivity.class);

ActivityScenarioRule sẽ tự động khởi chạy màn hình LoginActivity trước mỗi test và dọn dẹp sau đó.

Bước 4 — Viết test theo cấu trúc Given–When–Then.

    @Test
    public void loginThanhCong_hienThiTrangChu() {
        // When: nhập thông tin và bấm đăng nhập
        onView(withId(R.id.et_phone))
            .perform(typeText("0987654321"), closeSoftKeyboard());
        onView(withId(R.id.et_password))
            .perform(typeText("MatKhau@123"), closeSoftKeyboard());
        onView(withId(R.id.btn_login)).perform(click());

        // Then: màn hình trang chủ hiện ra
        onView(withId(R.id.home_container))
            .check(matches(isDisplayed()));
    }

Bước 5 — Xử lý bất đồng bộ (nếu có gọi API). Nếu nút login gọi server, đăng ký IdlingResource trước khi assert:

IdlingRegistry.getInstance().register(okHttpIdlingResource);
// ... thực hiện test ...
IdlingRegistry.getInstance().unregister(okHttpIdlingResource);

Bước 6 — Chạy test. Trên máy thật hoặc emulator:

./gradlew connectedAndroidTest

Hoặc bấm nút chạy ngay cạnh method test trong Android Studio. Kết quả (pass/fail, screenshot, stack trace) sẽ hiện trong tab Run và được xuất ra thư mục build/reports/androidTests.

Bước 7 — Thêm assertion tiêu cực. Đừng chỉ kiểm tra "thành công". Hãy thêm test cho trường hợp sai mật khẩu, kiểm tra thông báo lỗi xuất hiện đúng:

onView(withText("Sai mật khẩu")).check(matches(isDisplayed()));

Lỗi thường gặp & mẹo

Lỗi 1 — NoMatchingViewException. View không tồn tại trong hierarchy tại thời điểm tìm. Nguyên nhân phổ biến: view chưa render (RecyclerView), sai R.id, hoặc view nằm trong Dialog/Toast. Mẹo: đọc kỹ view hierarchy mà Espresso in ra trong log — nó liệt kê mọi view hiện có.

Lỗi 2 — AmbiguousViewMatcherException. Matcher khớp với nhiều view cùng lúc (ví dụ có 2 nút cùng text "OK"). Khắc phục bằng cách ghép matcher: allOf(withText("OK"), isDisplayed(), withParent(withId(R.id.dialog))).

Lỗi 3 — Dùng Thread.sleep() để "chờ". Đây là anti-pattern làm mất đi ưu điểm lớn nhất của Espresso và khiến test chậm, giòn. Với tác vụ async, hãy dùng IdlingResource thay vì sleep.

Lỗi 4 — Không tắt animation. Animation hệ thống có thể làm test flaky. Trên thiết bị test, tắt cả ba trong Developer Options: Window animation scale, Transition animation scale, Animator duration scale — đặt về 0. Nhiều đội CI cấu hình tắt tự động qua adb.

Lỗi 5 — Test không phụ thuộc bị coupling. Mỗi test nên độc lập, tự dựng state riêng. Đừng để test B phụ thuộc vào việc test A đã login. Dùng @Before để reset state.

Mẹo — Espresso Test Recorder. Android Studio có công cụ Record Espresso Test: bạn thao tác tay trên app, nó sinh code test tự động. Rất tốt để học cú pháp, nhưng code sinh ra thường thô, nên refactor lại trước khi đưa vào suite chính thức.

Mẹo — dùng resource id thay vì text. Ưu tiên withId() hơn withText() vì text hay đổi khi làm đa ngôn ngữ (bài l10n/i18n) và dễ gây ambiguous. Yêu cầu dev đặt android:id rõ ràng cho các view cần test.

Bài tập thực hành

  • Dựng dự án mẫu: Tạo một app Android đơn giản có màn hình đăng nhập (2 ô nhập + 1 nút) và màn hình chào. Thêm dependency Espresso, viết test cho luồng đăng nhập thành công như hướng dẫn ở Bước 4.
  • Viết test tiêu cực: Thêm một test cho trường hợp bỏ trống mật khẩu, kiểm tra thông báo lỗi "Vui lòng nhập mật khẩu" hiển thị. Dùng check(matches(isDisplayed())).
  • Xử lý RecyclerView: Nếu app của bạn có danh sách, viết test dùng RecyclerViewActions.actionOnItemAtPosition(10, click()) để bấm vào phần tử thứ 10 và verify màn hình chi tiết mở ra.
  • So sánh flaky: Thêm một Thread.sleep(2000) vào một test rồi bỏ nó ra, chạy mỗi phiên bản 10 lần và ghi lại thời gian tổng. Quan sát tại sao Espresso không cần sleep.
  • Tình huống mở rộng: Viết một test cho ô nhập tiếng Việt có dấu ("Trần Hưng Đạo"), nhớ chèn closeSoftKeyboard() đúng chỗ. Ghi lại điều gì xảy ra nếu bạn quên đóng bàn phím.

Tóm tắt

Espresso là framework kiểm thử UI native của Google cho Android, chạy in-process cùng app nên nhanh và đáng tin cậy hơn hẳn Appium nhờ cơ chế auto-synchronization — tự chờ UI thread rảnh trước khi thao tác. API xoay quanh ba trụ cột dễ nhớ: ViewMatchers (tìm view nào), ViewActions (làm gì) và ViewAssertions (kiểm tra gì), với câu mẫu onView(...).perform(...).check(...).

Những điểm cần khắc cốt: dùng onData/RecyclerViewActions cho danh sách; khai báo IdlingResource cho mọi tác vụ mạng/bất đồng bộ; đặt test trong androidTest; tắt animation; và tuyệt đối tránh Thread.sleep(). Qua ba tình huống thực tế từ đội ví điện tử, app đặt xe và thương mại điện tử, bạn thấy rõ Espresso tỏa sáng ở test logic app nhưng có ranh giới rõ với những thứ thuộc hệ thống (bàn phím, bộ gõ). Nắm vững Espresso, bạn sẽ trở thành cầu nối vững chắc giữa QA và đội developer Android — một lợi thế lớn trong nghề. Ở bài tiếp theo, chúng ta sẽ bước sang mảng Mobile Accessibility Testing để đảm bảo app dùng được cho mọi người.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi