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

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

如需查看完整示例,请参阅 GitHub 上 AdapterViewTest.java 类中的 testDataItemNotInAdapter() 方法。

使用自定义错误处理程序

通过用自定义的 FailureHandler 替换 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() 方法中。