Framework Test UI cho ứng dụng android

espresso_lockup111.jpg

Việc test ứng dụng trong quy trình phát triển phần mềm thường dành cho tester hay QA, nhưng để 1 sản phẩm có chất lượng đầu ra ở mức tốt nhất, ít lỗi nhất thì bản thân nhà phát triển (dev) cũng phải kiểm soát chặt chẽ code và nâng cao kĩ năng test của mình.

Một điều thường mắc phải đối với dev là khá chủ quan khi lấy kinh nghiệm dựa trên góc độ kĩ thuật để thực hiện việc test ứng dụng. Điều này đôi khi là 1 cản trở rất lớn khi các bugs phát sinh thường xảy ra bởi những thao tác hết sức bình thường hoặc rất vớ vẩn. Hơn nữa việc test thủ công bằng tay gây mất nhiều thời gian hoặc hiệu quả không cao, trong bài viết này chúng ta cùng nghiên cứu sử dụng 1 framework để test UI 1 cách tự động cho ứng dụng Android.

I, Giới Thiệu

Tên Framework: Espresso

Cung cấp: Trong gói thư viện Android Testing Support Library

Espresso cung cấp một tập hợp các API để viết các case cho việc test giao diện người dùng (UI) mô phỏng tương tác người dùng trong một ứng dụng đơn lẻ. Espresso có thể chạy trên các device dùng Android API 8 hoặc cao hơn. Một lợi ích quan trọng của việc sử dụng Espresso là nó cung cấp tự động việc đồng bộ hóa các hoạt động thử nghiệm với UI của ứng dụng bạn đang thử nghiệm.

Espresso phát hiện khi main thread trong trạng thái nhàn rỗi (idle), do đó nó chạy các lệnh kiểm tra vào thời điểm thích hợp, cải thiện độ tin cậy cho việc test của bạn.

Espresso testing framework dựa vào các API và làm việc với AndroidJUnitRunner test runner.

II, Cài Đặt

Trước khi cài đặt Espresso, bạn phải:

  • Cài đặt gói Android Testing Support Library: Các API cho Espresso được đặt trong gói com.android.support.test.espresso . Đây các lớp để sử dụng cho việc viết các case test sử dụng trong Espresso framework.

  • Cài đặt cấu trúc dự án: Trong dự án sử dụng Gradle, các mã nguồn cho các ứng dụng mục tiêu mà bạn muốn kiểm tra thường được đặt dưới thư mục app/src/main. Mã nguồn cho việc test sử dụng Espresso phải được đặt trong thư mục app/src/androidTest. Để tìm hiểu chi tiết hơn về cách quản lý và cài đặt các thư mục cho dự án, xem thêm: Managing Project

  • Cài đặt Android testing dependencies: Bạn phải mô tả các thư viện dưới đây trong file build.gradle cho việc chạy test sử dụng Espresso:

dependencies {
    androidTestCompile 'com.android.support:support-annotations:23.0.1'
    androidTestCompile 'com.android.support.test:runner:0.4.1'
    androidTestCompile 'com.android.support.test:rules:0.4.1'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
    // Set this dependency if you want to use Hamcrest matching
    // Hamcrest TUT: http://www.vogella.com/tutorials/Hamcrest/article.html
    androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
}
  • Tắt các hiệu ứng Animation trong máy: Các animaton trong hệ thống khi test có thể gây ra kết quả bất ngờ hoặc có thể dẫn đến thất bại thử nghiệm của bạn. Để tắt các Animation vào phần Settings > Developing Options > Và tắt bỏ các tính năng sau:
  1. Window animation scale
  2. Transition animation scale
  3. Animator duration scale

III, Tạo test class và sử dụng

Để tạo 1 lớp test Espresso, bạn tạo 1 class Java hoặc subclass của lớp ActivityInstrumentationTestCase2 và tuân theo mô hình dưới đây:

  1. Tìm đối tượng UI mà bạn muốn test trong Activity (Ví dụ 1 Button Đăng Nhập) bởi việc gọi phương thức onView hoặc onData cho AdapterView.

  2. Mô phỏng một tương tác của người dùng cụ thể tới thành phần UI đó, bằng việc gọi phương thức ViewInteraction.perform() hoặc DataInteraction.perform() và đưa vào các hành động người dùng (ví dụ mô phỏng thao tác click và button). Để trình tự làm nhiều thao tác hành động mô phỏng người dùng trong cùng 1 thành phần giao diện bằng cách thêm các dấu chấm (.) để ngăn cách các hoạt động của bạn trong tham số truyền vào.

  3. Lặp lại các bước trên nếu cần thiết, để mô phỏng một lưu lượng người dùng trên nhiều Activity khác nhau trong ứng dụng.

  4. Sử dụng các phương thức ViewAssertions để kiểm tra xem giao diện người dùng phản ánh có đúng tình trạng hoặc hành vi dự kiến, sau khi các tương tác người dùng được thực hiện.

Các bước sẽ được trình bày chi tiết hơn trong các phần dưới đây:

Đoạn code sau đây mô tả cho bạn các method sử dụng cho các luồng cơ bản khi test:

onView(withId(R.id.my_view))            // withId(R.id.my_view) is a ViewMatcher
        .perform(click())               // click() is a ViewAction
        .check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion

Sử dụng Espresso với ActivityInstrumentationTestCase2

Nếu bạn tạo subclass của ActivityInstrumentationTestCase2 cho việc tạo class test Espresso, bạn phải inject 1 thể hiện của Instrumentation trong lớp test của bạn. Bước này là yêu cầu để cho thử nghiệm Espresso của bạn chạy được với các thử nghiệm runner AndroidJUnitRunner.

Để làm được điều này, hãy gọi phương thức injectInstrumentation () và đưa vào kết quả của InstrumentationRegistry.getInstrumentation (), như trong ví dụ mã sau:

import android.support.test.InstrumentationRegistry;

public class MyEspressoTest
        extends ActivityInstrumentationTestCase2<MyActivity> {

    private MyActivity mActivity;

    public MyEspressoTest() {
        super(MyActivity.class);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();
        // Inject
        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
        mActivity = getActivity();
    }

   ...
}

Truy cập các thành phần UI

Trước khi Espresso có thể tương tác với ứng dụng, đầu tiên bạn phải xác định các thành phần UI hoặc các view. Espresso hỗ trợ sử dung Framwork Hamscret matchers để xác định các views và adapter trong ứng dụng của bạn.

Để xác định 1 view, sử dụng phương thức onView() và đưa vào 1 view khớp (view matcher) để xác định mục tiêu cần test. Điều này sẽ được mô tả nhiều hơn ở phần "Xác định 1 View Matcher". Phương thức này trả về 1 đối tượng ViewInteraction mà cho phép bạn giả lập các tương tác người dùng.

Tuy nhiên, gọi phương thức onView () có thể không hoạt động nếu bạn muốn xác định vị trí một view trong một AdapterView (Ví dụ: Adapter dùng cho List, GridView, ...). Trong trường hợp này xem phần "Đặt một view trong AdapterView" bên dưới để tìm hiểu cách làm.

Chú ý: Phương thức onView () sẽ không kiểm tra nếu bạn đã xác định 1 views là hợp lệ. Thay vào đó, Espresso chỉ tìm kiếm sự phân cấp của view hiện tại, sử dụng các matchers. Nếu ko có tìm thấy sự trùng khớp nào (no match) phương thức này sẽ bắn ra ngoại lệ: NoMatchingViewExeception

Đoạn code dưới đây hướng dẫn bạn làm thế nào để truy cập tới 1 EditText. Điễn chữ vão, tắt bàn phím ảo, và sau đó click vào 1 button.

public void testChangeText_sameActivity() {
    // Type text and then press the button.
    onView(withId(R.id.editTextUserInput))
            .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
    onView(withId(R.id.changeTextButton)).perform(click());

    // Check that the text was changed.
    ...
}

Xác đinh 1 View Matchers

Bạn có thể xác định 1 view matcher bằng các cách thức sau đây:

  • Gọi phương thức trong lớp ViewMatcher. Ví dụ, để tìm một View bằng cách tìm một chuỗi nó sẽ hiển thị (Sign-in button), bạn có thể gọi một phương thức như sau:
onView(withText("Sign-in"));

Tương tự, bạn có thể gọi withId() và cung cấp resource ID (R.id) như trong ví dụ sau:

onView(withId(R.id.button_signin));

Android resource ID không đảm bảo là duy nhất. Nếu bạn cố gắng tìm 1 View sử dụng resourceId mà được sử dụng bởi nhiều hơn 1 view. Khi đó Espresso sẽ bắn ra ngoại lệ AmbiguousViewMatcherException.

  • Sử dụng các phương thức trong lớp Matchers của Hamscret. Bạn có thể sử dụng phương thức allOf() để kết hợp nhiều lệnh khớp, như là containsString()instanceOf(). Cách tiếp cận này cho phép bạn lọc (đưa ra các điều kiện) các kết quả trả về, như trong ví dụ sau:
// Tìm kiếm tất cả các view có Id là button_signin và text hiển thị là "Sign-in"
onView(allOf(withId(R.id.button_signin), withText("Sign-in")));

Bạn có thể sử dụng từ khoá "not" để lọc các View không mong muốn như trong ví dụ sau đây:

// Tìm tất các các View có ID button_signin ngoại trừ các View có text Sign-out
onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))));

Để sử dụng các phương thức trên cho việc test bạn phải import gói: org.hamcrest.Matchers. Để học nhiều hơn về Hamscret matching , xem trang Hamscret.

Để tăng hiệu suất cho Espresso trong việc Test, xác định tối thiểu các thông tin để bạn tìm thấy View mục tiêu. Ví dụ nếu 1 View chỉ hiển thị duy nhất Text, vì vậy bạn không cần xác định View này là 1 thể hiện của TextView sẽ là giảm hiệu suất.

Đặt một view trong AdapterView

Trong 1 AdapterView. Các View là được hiển thị động trong thời gian thực thi. Nếu các View của bạn muốn test nằm trong AdapterView (như: ListView, GridView hoặc Spinner), phương thức onView() có thể sẽ không hoạt động được vì chỉ có 1 phần nhỏ các view đang được hiển thị .

Thay vào đó bạn sử dụng phương thức onData() để trả về 1 đối tượng DataInteraction mà có thể truy cập tới các view mục tiêu của bạn. Espresso sẽ xử lý nạp các View mục tiêu vào trong cấu trúc View hiện tại. Espresso cũng sẽ cuộn (scrolling) tới các thành phần mục tiêu và đưa chúng vào trọng tâm (focus item).

Lưu ý: Phương thức onData() sẽ không kiểm tra nếu các matcher đưa tới mục tiêu là 1 View. Espresso chỉ tìm kiếm trên cấu trúc phân cấp view hiện tại. Nếu ko tìm thấy mục nào phù hợp, nó sẽ bắn ra ngoại lệ NoMatchingViewException.

Đoạn mã sau đây chỉ cho bạn làm thế nào bạn có thể sử dụng phương thức onData() cùng với các matcher của Hamcrest để tìm kiếm một dòng (row) cụ thể trong một danh sách từ một chuỗi cho trước. Trong ví dụ này, lớp LongListActivity chưa đựng 1 danh sách các chuỗi thông qua SimpleAdapter .

onData(allOf(is(instanceOf(Map.class)),
        hasEntry(equalTo(LongListActivity.ROW_TEXT), is(str))));

Thực hiện các Hành Động (Actions)

Gọi các phương thức ViewInteraction.perform () hoặc DataInteraction.perform () để mô phỏng các tương tác của người dùng trên các thành phần giao diện. Bạn phải đưa vào 1 hoặc nhiều các đối tượng ViewAction như là 1 tham số. Espresso bắn mỗi hành động một cách tuần tự theo thứ tự nhất định, và thực thi chúng trong các luồng chính (main Thread).

Lớp ViewAction cung cấp 1 danh sách các phương thức để mô tả các hành động (action) chung. Bạn có thể sử dụng những phương thức này như 1 cách nhanh chong tiện lợi thay vì tự tạo ra và cấu hình đối tượng ViewAction. Bạn có thể chỉ định các hành động như:

  • ViewActions.click(): Mô phỏng người dùng click vào 1 View.
  • ViewActions.typeText(): Click vào 1 View, và điền text đã được quy định
  • ViewActions.scrollTo() Cuộn tới 1 View. View mục tiêu phải là view con (subclass) của lớp ScrollView và phải được gán giá trị VISBLE cho android:visibility. Các View mà được chứa trong AdapterView (như là ListView), phương thức onData() sẽ cuộn cho bạn (Xem phần trên).
  • ViewActions.pressKey(): Thực hiện việc nhấn sử dụng các mã phím (keycode) được quy định
  • ViewActions.clearText(): Thực hiện xoá text trong View mục tiêu

Nếu View mục tiêu của bạn nằm trong ScrollView, thực hiện hành động ViewActions.scrollTo() đầu tiên để hiển thị các View trên màn hình trước khi tiến hành các hành động khác. Cáchành động ViewActions.scrollTo() sẽ không có hiệu lực nếu View đã được hiển thị.

Xác minh các kết quả trả về

Gọi phương thức ViewInteraction.check () hoặc DataInteraction.check () để khẳng định (asset that) View mục tiêu trả về đúng các trạng thái mong muốn. Bạn phải đưa vào ViewAssertion như là tham số cho các phương thức. Nếu khẳng định thất bại (fails), Espresso sẽ ném ra 1 ngoại lệ: AssertionFailedError.

Lớp ViewAssertions cung cấp một danh sách các phương thức trợ giúp cho việc xác định khẳng định (assert) chung. Các khẳng định bạn có thể sử dụng bao gồm:

  • doesNotExist: Khẳng định rằng không có View nào phù hợp với các tiêu chí quy định trong cấu trúc View hiện tại.
  • matches: Khẳng định rằng View được định tồn tại trong hệ thống cấu trúc View hiện tại và trạng thái của nó phù hợp với các điều kiện (matcher) của Hamcrest.
  • selectedDescendentsMatch: Khẳng định rằng View con được mô tả cho parent View là tồn tại, và các trạng thái của chúng phù hợp với một số matcher của hamcrest

Đoạn mã sau đây cho bạn biết làm thế nàokiểm tra xem các text hiển thị trong UI có giá trị giống như văn bảnđã nhập trong EditText.

public void testChangeText_sameActivity() {
    // Type text and then press the button.
    ...

    // Check that the text was changed.
    onView(withId(R.id.textToBeChanged))
            .check(matches(withText(STRING_TO_BE_TYPED)));
}

Chạy Espresso trên Thiết Bị hoặc Emulator

Để chạy Espresso, bạn phải sử dụng các class trong AndroidJUnitRunner cung cấp trong gói Android Testing Support Library như một mặc định cho việc tự động hoá các quá trình (runner).

Trong Gradle project cung cấp 1 thư mục mặc đinh: src/androidTest/java cho bạn lưu trữ các lớp khế ước (instrumented ) cho việc test và các hành động test cho bạn thực hiện trên device. Các plugin sẽ biên dịch các class đặt trong thư mục này và sau đó thực thi việc test App sử dụng các cấu hình trong class runner.

Để chạy Espresso trong Gradle:

  1. Mô tả AndroidJUnitRunner như là lớp mặc định cho việc tự động hoá các hành động trong file build.gradle :
 android {
  defaultConfig {
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
 }
  1. Chạy test bằng dòng lệnh bằng việc gọi nhiệm vụ connectedAndroidTest (hay cAT):
./gradlew cAT

IV, Kết luân

Việc tự động hoá cho các công việc test ứng dụng thực sự mang lại nhưng hiệu quả lớn vì cường độ cao các thao tác mà con người khó có thể đạt được, vì vậy sử dụng chúng giúp giảm thiểu rủi ro trong qua trình sử dụng app thực tế và dev sẽ phải cải thiện code của mình hơn, để hạn chế bugs cũng như tăng performance làm tăng chất lượng của sản phẩm.

Tham khảo