Espresso 食谱

本文档介绍如何设置各种常见的 Espresso 测试。

匹配另一个视图旁边的视图

布局可能包含某些本身不唯一的视图。例如,联系人表格中重复的呼叫按钮可能具有相同的 R.id,包含相同的文本,并且与视图层次结构中的其他呼叫按钮具有相同的属性。

例如,在这个活动中,带有文本 "7" 的视图在多行中重复

A list activity showing 3 copies of the same view element
     inside a 3-item list

通常,非唯一视图将与位于其旁边的某个唯一标签配对,例如呼叫按钮旁边的联系人姓名。在这种情况下,您可以使用 hasSibling() 匹配器缩小选择范围

Kotlin

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

Java

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

匹配操作栏中的视图

ActionBarTestActivity 具有两个不同的操作栏:一个普通操作栏和一个从选项菜单创建的上下文操作栏。这两个操作栏都具有一个始终可见的项目和两个仅在溢出菜单中可见的项目。单击项目后,它会将 TextView 更改为所单击项目的內容。

匹配两个操作栏上的可见图标很简单,如下面的代码片段所示

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")));
}

The save button is on the action bar, at the top of the activity

上下文操作栏的代码看起来相同

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")));
}

The lock button is on the action bar, at the top of the activity

单击溢出菜单中的项目对于普通操作栏来说有点棘手,因为某些设备具有硬件溢出菜单按钮,它将在选项菜单中打开溢出项目,而某些设备具有软件溢出菜单按钮,它将打开一个普通的溢出菜单。幸运的是,Espresso 为我们处理了这个问题。

对于普通操作栏

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")));
}

The overflow menu button is visible, and a list appears beneath the
          action bar near the top of the screen

这是它在具有硬件溢出菜单按钮的设备上的显示方式

There is no overflow menu button, and a list appears near the bottom
          of the screen

对于上下文操作栏,它再次非常容易

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")));
    }
}

The overflow menu button appears in the action bar, and the list of
          options appear underneath the action bar, near the top of the screen

要查看这些示例的完整代码,请查看 GitHub 上的 ActionBarTest.java 示例。

断言视图未显示

执行一系列操作后,您肯定希望断言被测 UI 的状态。有时,这可能是一个负面情况,例如当某些事情没有发生时。请记住,您可以使用 ViewAssertions.matches() 将任何 hamcrest 视图匹配器转换为 ViewAssertion

在下面的示例中,我们使用 isDisplayed() 匹配器并使用标准 not() 匹配器将其反转。

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())));

如果视图仍然是层次结构的一部分,则上述方法有效。如果不是,您将收到 NoMatchingViewException,并且您需要使用 ViewAssertions.doesNotExist()

断言视图不存在。

如果视图已从视图层次结构中消失,这可能发生在操作导致过渡到另一个活动时,您应该使用 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());

断言适配器中不存在数据项。

要证明特定数据项不在 AdapterView 中,您需要做一些不同的事情。我们必须找到我们感兴趣的 AdapterView 并询问它所保存的数据。我们不需要使用 onData()。相反,我们使用 onView() 查找 AdapterView,然后使用另一个匹配器来处理视图内部的数据。

首先是匹配器。

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;
        }
    };
}

然后,我们只需要 onView() 来查找 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")))));
    }
}

我们有一个断言,如果适配器视图中存在等于 "item: 168" 的项,则断言将失败,其 ID 为 list。

有关完整示例,请查看 GitHub 上 AdapterViewTest.java 类中的 testDataItemNotInAdapter() 方法。

使用自定义失败处理程序。

用自定义处理程序替换 Espresso 中的默认 FailureHandler 允许进行额外的或不同的错误处理,例如截取屏幕截图或传递额外的调试信息。

示例 CustomFailureHandlerTest 演示了如何实现自定义失败处理程序。

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);
        }
    }
}

此失败处理程序抛出 MySpecialException 而不是 NoMatchingViewException,并将所有其他失败委托给 DefaultFailureHandler。可以在测试的 setUp() 方法中将 CustomFailureHandler 注册到 Espresso。

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()));
}

有关更多信息,请参阅 FailureHandler 接口和 Espresso.setFailureHandler()

目标非默认窗口。

Android 支持多个窗口。通常,这对用户和应用程序开发人员来说是透明的,但在某些情况下,多个窗口是可见的,例如当自动完成窗口在搜索小部件中绘制到主应用程序窗口之上时。为了简化操作,默认情况下,Espresso 使用启发式方法来猜测您要交互的 Window。这种启发式方法几乎总是足够好的;但是,在极少数情况下,您需要指定交互的目标窗口。您可以通过提供自己的根窗口匹配器或 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());

ViewMatchers 一样,我们提供了一组预先提供的 RootMatchers。当然,您始终可以实现自己的 Matcher 对象。

请查看 GitHub 上的 MultipleWindowTest 示例

使用 addHeaderView()addFooterView() 方法将页眉和页脚添加到 ListViews 中。为了确保 Espresso.onData() 知道要匹配哪个数据对象,请确保将预设数据对象值作为第二个参数传递给 addHeaderView()addFooterView()。例如

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);

然后,您可以为页脚编写匹配器。

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));
}

在测试中加载视图很简单。

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());

    // ...
}

请查看完整的代码示例,该示例位于 GitHub 上 AdapterViewTest.javatestClickFooter() 方法中。