Công thức Espresso

Tài liệu này mô tả cách thiết lập nhiều quy trình kiểm thử Espresso phổ biến.

Khớp một chế độ xem với một chế độ xem khác

Một bố cục có thể chứa một số khung hiển thị nhất định không phải là duy nhất. Ví dụ: nút gọi lặp lại trong một bảng danh bạ có thể có cùng R.id, chứa cùng văn bản và có cùng thuộc tính như các nút gọi khác trong hệ phân cấp khung hiển thị.

Ví dụ: trong hoạt động này, thành phần hiển thị có văn bản "7" lặp lại trên nhiều hàng:

Một hoạt động trên danh sách cho thấy 3 bản sao của cùng một phần tử thành phần hiển thị bên trong một danh sách gồm 3 mục

Thông thường, thành phần hiển thị không phải duy nhất sẽ được ghép nối với một số nhãn duy nhất ở bên cạnh, chẳng hạn như tên của người liên hệ bên cạnh nút gọi. Trong trường hợp này, bạn có thể sử dụng trình so khớp hasSibling() để thu hẹp lựa chọn:

Kotlin

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
    .perform(click())

Java

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
    .perform(click());

Khớp với khung hiển thị bên trong thanh thao tác

ActionBarTestActivity có hai thanh thao tác khác nhau: thanh thao tác thông thường và thanh thao tác theo ngữ cảnh được tạo từ trình đơn tuỳ chọn. Cả hai thanh thao tác đều có một mục luôn hiển thị và hai mục chỉ hiển thị trong trình đơn mục bổ sung. Khi được nhấp vào một mục, mục đó sẽ thay đổi TextView thành nội dung của mục đã nhấp.

Việc so khớp các biểu tượng hiển thị trên cả hai thanh thao tác rất đơn giản, như minh hoạ trong đoạn mã sau đây:

Kotlin

fun testClickActionBarItem() {
    // We make sure the contextual action bar is hidden.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click())

    // Click on the icon - we can find it by the r.Id.
    onView(withId(R.id.action_save))
        .perform(click())

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Save")))
}

Java

public void testClickActionBarItem() {
    // We make sure the contextual action bar is hidden.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());

    // Click on the icon - we can find it by the r.Id.
    onView(withId(R.id.action_save))
        .perform(click());

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Save")));
}

Nút lưu nằm trên thanh tác vụ, ở đầu hoạt động

Mã này trông giống hệt với thanh thao tác theo ngữ cảnh:

Kotlin

fun testClickActionModeItem() {
    // Make sure we show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click())

    // Click on the icon.
    onView((withId(R.id.action_lock)))
        .perform(click())

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Lock")))
}

Java

public void testClickActionModeItem() {
    // Make sure we show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click());

    // Click on the icon.
    onView((withId(R.id.action_lock)))
        .perform(click());

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Lock")));
}

Nút khoá nằm trên thanh thao tác, ở đầu hoạt động

Thao tác nhấp vào các mục trong trình đơn mục bổ sung sẽ phức tạp hơn một chút đối với thanh thao tác thông thường vì một số thiết bị có nút trình đơn mục bổ sung phần cứng giúp mở các mục mục bổ sung trong trình đơn tuỳ chọn, và một số thiết bị có nút trình đơn mục bổ sung phần mềm để mở trình đơn mục bổ sung thông thường. Thật may là Espresso xử lý việc đó cho chúng tôi.

Đối với thanh thao tác thông thường:

Kotlin

fun testActionBarOverflow() {
    // Make sure we hide the contextual action bar.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click())

    // Open the options menu OR open the overflow menu, depending on whether
    // the device has a hardware or software overflow menu button.
    openActionBarOverflowOrOptionsMenu(
            ApplicationProvider.getApplicationContext<Context>())

    // Click the item.
    onView(withText("World"))
        .perform(click())

    // Verify that we have really clicked on the icon by checking
    // the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("World")))
}

Java

public void testActionBarOverflow() {
    // Make sure we hide the contextual action bar.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());

    // Open the options menu OR open the overflow menu, depending on whether
    // the device has a hardware or software overflow menu button.
    openActionBarOverflowOrOptionsMenu(
            ApplicationProvider.getApplicationContext());

    // Click the item.
    onView(withText("World"))
        .perform(click());

    // Verify that we have really clicked on the icon by checking
    // the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("World")));
}

Nút trình đơn mục bổ sung sẽ xuất hiện và một danh sách sẽ xuất hiện bên dưới thanh thao tác gần đầu màn hình

Đây là giao diện của giao diện trên các thiết bị có nút trình đơn mục bổ sung phần cứng:

Không có nút trình đơn mục bổ sung và một danh sách sẽ xuất hiện ở gần cuối màn hình

Đối với thanh thao tác theo ngữ cảnh, việc này thực sự dễ dàng:

Kotlin

fun testActionModeOverflow() {
    // Show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click())

    // Open the overflow menu from contextual action mode.
    openContextualActionModeOverflowMenu()

    // Click on the item.
    onView(withText("Key"))
        .perform(click())

    // Verify that we have really clicked on the icon by
    // checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Key")))
    }
}

Java

public void testActionModeOverflow() {
    // Show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click());

    // Open the overflow menu from contextual action mode.
    openContextualActionModeOverflowMenu();

    // Click on the item.
    onView(withText("Key"))
        .perform(click());

    // Verify that we have really clicked on the icon by
    // checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Key")));
    }
}

Nút trình đơn mục bổ sung xuất hiện trong thanh thao tác và danh sách tuỳ chọn xuất hiện bên dưới thanh thao tác, gần đầu màn hình

Để xem mã đầy đủ cho các mẫu này, hãy xem mẫu ActionBarTest.java trên GitHub.

Xác nhận rằng một thành phần hiển thị không hiển thị

Sau khi thực hiện một loạt hành động, chắc chắn bạn sẽ muốn xác nhận trạng thái của giao diện người dùng đang được kiểm thử. Đôi khi, đây có thể là trường hợp tiêu cực, chẳng hạn như khi điều gì đó không xảy ra. Xin lưu ý rằng bạn có thể chuyển mọi trình so khớp khung hiển thị hamcrest thành ViewAssertion bằng cách sử dụng ViewAssertions.matches().

Trong ví dụ bên dưới, chúng tôi sẽ lấy trình so khớp isDisplayed() và đảo ngược trình so khớp này bằng trình so khớp not() tiêu chuẩn:

Kotlin

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.hamcrest.Matchers.not

onView(withId(R.id.bottom_left))
    .check(matches(not(isDisplayed())))

Java

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.not;

onView(withId(R.id.bottom_left))
    .check(matches(not(isDisplayed())));

Phương pháp trên sẽ có hiệu quả nếu khung hiển thị vẫn là một phần của hệ phân cấp. Nếu không, bạn sẽ nhận được NoMatchingViewException và cần sử dụng ViewAssertions.doesNotExist().

Xác nhận rằng không có thành phần hiển thị

Nếu khung hiển thị đã biến mất khỏi hệ phân cấp khung hiển thị (có thể xảy ra khi một hành động dẫn đến việc chuyển đổi sang một hoạt động khác), bạn nên sử dụng ViewAssertions.doesNotExist():

Kotlin

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.matcher.ViewMatchers.withId

onView(withId(R.id.bottom_left))
    .check(doesNotExist())

Java

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

onView(withId(R.id.bottom_left))
    .check(doesNotExist());

Xác nhận rằng một mục dữ liệu không nằm trong bộ chuyển đổi

Để chứng minh một mục dữ liệu cụ thể không nằm trong AdapterView, bạn phải thực hiện các bước khác một chút. Chúng tôi phải tìm AdapterView mà chúng tôi quan tâm và truy vấn dữ liệu mà công cụ đó nắm giữ. Chúng ta không cần phải sử dụng onData(). Thay vào đó, chúng ta sử dụng onView() để tìm AdapterView rồi sử dụng một trình so khớp khác để xử lý dữ liệu bên trong khung hiển thị này.

Đầu tiên là trình so khớp:

Kotlin

private fun withAdaptedData(dataMatcher: Matcher<Any>): Matcher<View> {
    return object : TypeSafeMatcher<View>() {

        override fun describeTo(description: Description) {
            description.appendText("with class name: ")
            dataMatcher.describeTo(description)
        }

        public override fun matchesSafely(view: View) : Boolean {
            if (view !is AdapterView<*>) {
                return false
            }

            val adapter = view.adapter
            for (i in 0 until adapter.count) {
                if (dataMatcher.matches(adapter.getItem(i))) {
                    return true
                }
            }

            return false
        }
    }
}

Java

private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
    return new TypeSafeMatcher<View>() {

        @Override
        public void describeTo(Description description) {
            description.appendText("with class name: ");
            dataMatcher.describeTo(description);
        }

        @Override
        public boolean matchesSafely(View view) {
            if (!(view instanceof AdapterView)) {
                return false;
            }

            @SuppressWarnings("rawtypes")
            Adapter adapter = ((AdapterView) view).getAdapter();
            for (int i = 0; i < adapter.getCount(); i++) {
                if (dataMatcher.matches(adapter.getItem(i))) {
                    return true;
                }
            }

            return false;
        }
    };
}

Sau đó, chúng ta chỉ cần onView() để tìm AdapterView:

Kotlin

fun testDataItemNotInAdapter() {
    onView(withId(R.id.list))
          .check(matches(not(withAdaptedData(withItemContent("item: 168")))))
    }
}

Java

@SuppressWarnings("unchecked")
public void testDataItemNotInAdapter() {
    onView(withId(R.id.list))
          .check(matches(not(withAdaptedData(withItemContent("item: 168")))));
    }
}

Ngoài ra, chúng tôi có một khẳng định sẽ không thành công nếu một mục bằng "item: 168" tồn tại trong khung hiển thị bộ chuyển đổi với danh sách mã nhận dạng.

Để xem mẫu đầy đủ, hãy xem phương thức testDataItemNotInAdapter() trong lớp AdapterViewTest.java trên GitHub.

Sử dụng trình xử lý lỗi tuỳ chỉnh

Việc thay thế FailureHandler mặc định trong Espresso bằng một thành phần tuỳ chỉnh cho phép xử lý thêm hoặc xử lý lỗi khác, chẳng hạn như chụp ảnh màn hình hoặc truyền thêm thông tin gỡ lỗi.

Ví dụ CustomFailureHandlerTest minh hoạ cách triển khai một trình xử lý lỗi tuỳ chỉnh:

Kotlin

private class CustomFailureHandler(targetContext: Context) : FailureHandler {
    private val delegate: FailureHandler

    init {
        delegate = DefaultFailureHandler(targetContext)
    }

    override fun handle(error: Throwable, viewMatcher: Matcher<View>) {
        try {
            delegate.handle(error, viewMatcher)
        } catch (e: NoMatchingViewException) {
            throw MySpecialException(e)
        }

    }
}

Java

private static class CustomFailureHandler implements FailureHandler {
    private final FailureHandler delegate;

    public CustomFailureHandler(Context targetContext) {
        delegate = new DefaultFailureHandler(targetContext);
    }

    @Override
    public void handle(Throwable error, Matcher<View> viewMatcher) {
        try {
            delegate.handle(error, viewMatcher);
        } catch (NoMatchingViewException e) {
            throw new MySpecialException(e);
        }
    }
}

Trình xử lý lỗi này sẽ gửi một MySpecialException thay vì NoMatchingViewException và uỷ quyền tất cả các lỗi khác cho DefaultFailureHandler. Bạn có thể đăng ký CustomFailureHandler với Espresso trong phương thức setUp() của quy trình kiểm thử:

Kotlin

@Throws(Exception::class)
override fun setUp() {
    super.setUp()
    getActivity()
    setFailureHandler(CustomFailureHandler(
            ApplicationProvider.getApplicationContext<Context>()))
}

Java

@Override
public void setUp() throws Exception {
    super.setUp();
    getActivity();
    setFailureHandler(new CustomFailureHandler(
            ApplicationProvider.getApplicationContext()));
}

Để biết thêm thông tin, hãy xem giao diện FailureHandler Espresso.setFailureHandler().

Nhắm mục tiêu cửa sổ không mặc định

Android hỗ trợ nhiều cửa sổ. Thông thường, giá trị này minh bạch đối với người dùng và nhà phát triển ứng dụng. Tuy nhiên, trong một số trường hợp, nhiều cửa sổ sẽ hiển thị, chẳng hạn như khi cửa sổ tự động hoàn thành được vẽ trên cửa sổ chính của ứng dụng trong tiện ích tìm kiếm. Để đơn giản hoá mọi thứ, theo mặc định, Espresso sử dụng phương pháp phỏng đoán để đoán ra Window mà bạn định tương tác. Phương pháp phỏng đoán này hầu như luôn đủ tốt; tuy nhiên, trong một số ít trường hợp, bạn sẽ cần chỉ định cửa sổ mà tương tác nên nhắm đến. Bạn có thể thực hiện việc này bằng cách cung cấp trình so khớp cửa sổ gốc riêng hoặc trình so khớp Root:

Kotlin

onView(withText("South China Sea"))
    .inRoot(withDecorView(not(`is`(getActivity().getWindow().getDecorView()))))
    .perform(click())

Java

onView(withText("South China Sea"))
    .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
    .perform(click());

Tương tự như trường hợp với ViewMatchers, chúng tôi cung cấp một tập hợp RootMatchers được cung cấp trước. Tất nhiên, bạn luôn có thể triển khai đối tượng Matcher của riêng mình.

Hãy xem mẫu MultiplexWindowTest trên GitHub.

Đầu trang và chân trang được thêm vào ListViews bằng phương thức addHeaderView()addFooterView(). Để đảm bảo Espresso.onData() biết cần so khớp đối tượng dữ liệu nào, hãy nhớ truyền một giá trị đối tượng dữ liệu đặt trước ở dạng tham số thứ hai đến addHeaderView()addFooterView(). Ví dụ:

Kotlin

const val FOOTER = "FOOTER"
...
val footerView = layoutInflater.inflate(R.layout.list_item, listView, false)
footerView.findViewById<TextView>(R.id.item_content).text = "count:"
footerView.findViewById<TextView>(R.id.item_size).text
        = data.size.toString
listView.addFooterView(footerView, FOOTER, true)

Java

public static final String FOOTER = "FOOTER";
...
View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
footerView.findViewById<TextView>(R.id.item_content).setText("count:");
footerView.findViewById<TextView>(R.id.item_size).setText(String.valueOf(data.size()));
listView.addFooterView(footerView, FOOTER, true);

Sau đó, bạn có thể viết trình so khớp cho chân trang:

Kotlin

import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.instanceOf
import org.hamcrest.Matchers.`is`

fun isFooter(): Matcher<Any> {
    return allOf(`is`(instanceOf(String::class.java)),
            `is`(LongListActivity.FOOTER))
}

Java

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;

@SuppressWarnings("unchecked")
public static Matcher<Object> isFooter() {
    return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
}

Việc tải khung hiển thị trong một chương trình kiểm thử không đơn giản:

Kotlin

import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.sample.LongListMatchers.isFooter

fun testClickFooter() {
    onData(isFooter())
        .perform(click())

    // ...
}

Java

import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.sample.LongListMatchers.isFooter;

public void testClickFooter() {
    onData(isFooter())
        .perform(click());

    // ...
}

Hãy xem mã mẫu đầy đủ trong phương thức testClickFooter() của AdapterViewTest.java trên GitHub.