本文档介绍如何设置各种常见的 Espresso 测试。
匹配另一个视图旁边的视图
布局可能会包含某些本身不具唯一性的视图。例如,联系人列表中的某个重复通话按钮可能会与视图层次结构中的其他通话按钮具有相同的 R.id
、包含相同的文本,并且具有相同的属性。
例如,在下面的 Activity 中,包含文本 "7"
的视图在多行间重复:
通常,非唯一视图会与位于其旁边的某个唯一标签配对,如通话按钮旁边的联系人的姓名。在这种情况下,您可以使用 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"))); }
上下文操作栏的代码看起来完全相同:
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"))); }
点击溢出菜单中的项目对于普通操作栏来说稍微复杂一些,因为某些设备具有硬件溢出菜单按钮,点击该按钮会在选项菜单中打开溢出项目,而某些设备具有软件溢出菜单按钮,点击该按钮会打开普通溢出菜单。不过请放心,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"))); }
在具有硬件溢出菜单按钮的设备上,如下所示:
对于上下文操作栏,同样非常简单:
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"))); } }
如需查看这些示例的完整代码,请查看 GitHub 上的 ActionBarTest.java
示例。
断言视图未显示
执行一系列操作后,您肯定想要断言被测界面的状态。有时,这可能是否定的情况,例如当不会发生某件事时。请注意,您可以使用 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 列表的适配器视图中存在等于“item: 168”的项目,我们的断言就会失败。
如需查看完整示例,请在 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()
方法中向 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 示例。
匹配列表视图中的页眉或页脚
您可以使用 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.java
的 testClickFooter()
方法中找到。