Espresso 食谱

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

匹配相邻视图

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

例如,在此 activity 中,文本为 "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()

断言视图不存在

如果视图从视图层次结构中消失(当某个操作导致转换到另一个 activity 时可能会发生这种情况),则应使用 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() 方法中向 Espresso 注册 CustomFailureHandler

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 示例

页眉和页脚通过使用 ListViewsaddHeaderView()addFooterView() 方法添加到视图中。为确保 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.java 方法中的 testClickFooter() 完整代码示例。