Danh sách Espresso

Espresso cung cấp các cơ chế để cuộn đến hoặc thao tác trên một mục cụ thể cho 2 loại danh sách: khung hiển thị trình chuyển đổi và khung hiển thị tái chế (recycler view).

Khi xử lý các danh sách, đặc biệt là những khung hiển thị được tạo bằng RecyclerView hoặc đối tượng AdapterView, khung hiển thị mà bạn quan tâm thậm chí có thể không xuất hiện trên màn hình vì chỉ có một số ít phần tử con xuất hiện và được tái chế khi bạn cuộn. Bạn không thể sử dụng phương thức scrollTo() trong trường hợp này vì phương thức này yêu cầu khung hiển thị hiện có.

Tương tác với các mục trong danh sách khung hiển thị bộ chuyển đổi

Thay vì sử dụng phương thức onView(), hãy bắt đầu tìm kiếm bằng onData() và cung cấp trình so khớp dựa trên dữ liệu đang sao lưu khung hiển thị mà bạn muốn so khớp. Espresso sẽ thực hiện mọi công việc để tìm hàng trong đối tượng Adapter và hiển thị mục đó trong khung nhìn.

So khớp dữ liệu bằng trình so khớp khung hiển thị tuỳ chỉnh

Hoạt động bên dưới chứa ListView, được hỗ trợ bởi SimpleAdapter chứa dữ liệu cho từng hàng trong đối tượng Map<String, Object>.

Hoạt động trong danh sách đang hiển thị trên màn hình chứa một danh sách có 23 mục. Mỗi mục có một số, được lưu trữ dưới dạng Chuỗi, được ánh xạ tới một số khác, được lưu trữ dưới dạng Đối tượng.

Mỗi bản đồ có hai mục nhập: một khoá "STR" chứa Chuỗi (chẳng hạn như "item: x") và một khoá "LEN" chứa Integer (thể hiện độ dài của nội dung). Ví dụ:

{"STR" : "item: 0", "LEN": 7}

Mã cho một lượt nhấp vào hàng có "item: 50" sẽ có dạng như sau:

Kotlin

onData(allOf(`is`(instanceOf(Map::class.java)), hasEntry(equalTo("STR"),
        `is`("item: 50")))).perform(click())

Java

onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50"))))
    .perform(click());

Lưu ý: Espresso sẽ tự động cuộn qua danh sách khi cần.

Hãy tách Matcher<Object> bên trong onData(). Phương thức is(instanceOf(Map.class)) thu hẹp phạm vi tìm kiếm ở bất kỳ mục nào của AdapterView, được hỗ trợ bởi một đối tượng Map.

Trong trường hợp này, khía cạnh này của truy vấn khớp với mọi hàng của chế độ xem danh sách, nhưng chúng ta muốn nhấp cụ thể vào một mục, vì vậy chúng ta thu hẹp phạm vi tìm kiếm thêm bằng cách:

Kotlin

hasEntry(equalTo("STR"), `is`("item: 50"))

Java

hasEntry(equalTo("STR"), is("item: 50"))

Matcher<String, Object> này sẽ khớp với mọi mục ánh xạ có chứa mục nhập với khoá "STR" và giá trị "item: 50". Vì mã để tra cứu đoạn mã này mất nhiều thời gian và chúng ta muốn sử dụng lại ở các vị trí khác, hãy viết trình so khớp withItemContent tuỳ chỉnh cho mã này:

Kotlin

return object : BoundedMatcher<Object, Map>(Map::class.java) {
    override fun matchesSafely(map: Map): Boolean {
        return hasEntry(equalTo("STR"), itemTextMatcher).matches(map)
    }

    override fun describeTo(description: Description) {
        description.appendText("with item content: ")
        itemTextMatcher.describeTo(description)
    }
}

Java

return new BoundedMatcher<Object, Map>(Map.class) {
    @Override
    public boolean matchesSafely(Map map) {
        return hasEntry(equalTo("STR"), itemTextMatcher).matches(map);
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("with item content: ");
        itemTextMatcher.describeTo(description);
    }
};

Bạn dùng BoundedMatcher làm cơ sở vì để chỉ so khớp các đối tượng thuộc loại Map. Ghi đè phương thức matchesSafely(), đưa vào trình so khớp đã tìm thấy trước đó rồi so khớp phương thức đó với Matcher<String> mà bạn có thể truyền dưới dạng đối số. Điều này cho phép bạn gọi cho withItemContent(equalTo("foo")). Để rút ngắn mã, bạn có thể tạo một trình so khớp khác đã gọi equalTo() và chấp nhận đối tượng String:

Kotlin

fun withItemContent(expectedText: String): Matcher<Object> {
    checkNotNull(expectedText)
    return withItemContent(equalTo(expectedText))
}

Java

public static Matcher<Object> withItemContent(String expectedText) {
    checkNotNull(expectedText);
    return withItemContent(equalTo(expectedText));
}

Giờ đây, mã để nhấp vào mục này trở nên đơn giản:

Kotlin

onData(withItemContent("item: 50")).perform(click())

Java

onData(withItemContent("item: 50")).perform(click());

Để biết mã đầy đủ của bài kiểm thử này, hãy xem phương thức testClickOnItem50() trong lớp AdapterViewTest và trình so khớp LongListMatchers tuỳ chỉnh này trên GitHub.

Khớp với một chế độ xem con cụ thể

Mẫu ở trên đưa ra một lượt nhấp vào giữa toàn bộ hàng của ListView. Nhưng nếu chúng ta muốn thao tác trên một phần tử con cụ thể của hàng thì sao? Ví dụ: chúng ta muốn nhấp vào cột thứ hai trong hàng của LongListActivity, cột này hiển thị String.length của nội dung trong cột đầu tiên:

Trong ví dụ này, bạn nên chỉ trích xuất độ dài của một nội dung cụ thể. Quy trình này bao gồm việc xác định giá trị của cột thứ hai trong một hàng.

Bạn chỉ cần thêm thông số kỹ thuật onChildView() vào quá trình triển khai DataInteraction:

Kotlin

onData(withItemContent("item: 60"))
    .onChildView(withId(R.id.item_size))
    .perform(click())

Java

onData(withItemContent("item: 60"))
    .onChildView(withId(R.id.item_size))
    .perform(click());

Tương tác với các mục trong danh sách thành phần hiển thị tái chế

Đối tượng RecyclerView hoạt động khác với đối tượng AdapterView nên bạn không thể dùng onData() để tương tác với đối tượng đó.

Để tương tác với RecyclerViews bằng Espresso, bạn có thể dùng gói espresso-contrib. Gói này có một tập hợp RecyclerViewActions có thể dùng để cuộn đến các vị trí hoặc thực hiện thao tác trên các mục:

  • scrollTo() – Cuộn đến Chế độ xem phù hợp, nếu có.
  • scrollToHolder() – Cuộn đến Trình giữ khung hiển thị trùng khớp, nếu có.
  • scrollToPosition() – Cuộn đến một vị trí cụ thể.
  • actionOnHolderItem() – Thực hiện Hành động đối với khung hiển thị trên một Chủ sở hữu khung hiển thị trùng khớp.
  • actionOnItem() – Thực hiện Hành động đối với thành phần hiển thị trên một Thành phần hiển thị trùng khớp.
  • actionOnItemAtPosition() – Thực hiện một ViewAction trên khung hiển thị tại một vị trí cụ thể.

Các đoạn mã sau đây nêu bật một số ví dụ từ mẫu RecyclerViewSample:

Kotlin

@Test(expected = PerformException::class)
fun itemWithText_doesNotExist() {
    // Attempt to scroll to an item that contains the special text.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(
            // scrollTo will fail the test if no item matches.
            RecyclerViewActions.scrollTo(
                hasDescendant(withText("not in the list"))
            )
        )
}

Java

@Test(expected = PerformException.class)
public void itemWithText_doesNotExist() {
    // Attempt to scroll to an item that contains the special text.
    onView(ViewMatchers.withId(R.id.recyclerView))
            // scrollTo will fail the test if no item matches.
            .perform(RecyclerViewActions.scrollTo(
                    hasDescendant(withText("not in the list"))
            ));
}

Kotlin

@Test fun scrollToItemBelowFold_checkItsText() {
    // First, scroll to the position that needs to be matched and click on it.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(
            RecyclerViewActions.actionOnItemAtPosition(
                ITEM_BELOW_THE_FOLD,
                click()
            )
        )

    // Match the text in an item below the fold and check that it's displayed.
    val itemElementText = "${activityRule.activity.resources
        .getString(R.string.item_element_text)} ${ITEM_BELOW_THE_FOLD.toString()}"
    onView(withText(itemElementText)).check(matches(isDisplayed()))
}

Java

@Test
public void scrollToItemBelowFold_checkItsText() {
    // First, scroll to the position that needs to be matched and click on it.
    onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(RecyclerViewActions.actionOnItemAtPosition(ITEM_BELOW_THE_FOLD,
            click()));

    // Match the text in an item below the fold and check that it's displayed.
    String itemElementText = activityRule.getActivity().getResources()
            .getString(R.string.item_element_text)
            + String.valueOf(ITEM_BELOW_THE_FOLD);
    onView(withText(itemElementText)).check(matches(isDisplayed()));
}

Kotlin

@Test fun itemInMiddleOfList_hasSpecialText() {
    // First, scroll to the view holder using the isInTheMiddle() matcher.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()))

    // Check that the item has the special text.
    val middleElementText = activityRule.activity.resources
            .getString(R.string.middle)
    onView(withText(middleElementText)).check(matches(isDisplayed()))
}

Java

@Test
public void itemInMiddleOfList_hasSpecialText() {
    // First, scroll to the view holder using the isInTheMiddle() matcher.
    onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()));

    // Check that the item has the special text.
    String middleElementText =
            activityRule.getActivity().getResources()
            .getString(R.string.middle);
    onView(withText(middleElementText)).check(matches(isDisplayed()));
}

Tài nguyên khác

Để biết thêm thông tin về cách sử dụng danh sách Espresso trong kiểm thử Android, hãy tham khảo các tài nguyên sau.

Mẫu

  • DataAdapterSample: Cho thấy điểm truy cập onData() cho Espresso, cho các danh sách và đối tượng AdapterView.