Android Espresso(五) ——Custom Matcher
上一篇(Android Espresso(四)——RecyclerView)提到了使用自定义 ViewAction
来灵活应对 RecyclerView
中复杂Item测试。
这一篇讲下,在某种场景下,相同文字不同颜色或者其他属性,匹配其中一个组件进行操作,使用自定义ViewMatcher
。
自定义Matcher
在匹配一些简单的UI组件上,可以使用 BoundedMatcher
快速定义自己的 Matcher
。
static class CustomViewMatchers {
public static Matcher<View> withColoredText(final int expectedColor) {
return new BoundedMatcher<View, TextView>(TextView.class) {
@Override
public void describeTo(Description description) { // 为matcher添加描述信息
description.appendText("To find the textview with " + expectedColor + " color text");
}
// 主要实现这个matchesSafely()方法,来实现自己想要获取到的View。
// 这里需要注意参数类型 TextView 与 BoundedMatcher构造方法传入的 TextView在定义上是不同的。
// 见下面BoundedMatcher定义
@Override
protected boolean matchesSafely(TextView item) {
return expectedColor == item.getCurrentTextColor();
}
};
}
}
上述这段代码,可以快速看下如何使用 BoundedMatcher
来定义自己的 Matcher。
BoundedMatcher
先来看下 BoundedMatcher
定义。
// 在创建BoundedMatcher实例时,需要传入泛型类型T,S.
// T 表示Matcher要处理的类型,S 表示的操作实际的T的子类型
public abstract class BoundedMatcher<T, S extends T> extends BaseMatcher<T> {
private final Class<?> expectedType;
private final Class<?>[] interfaceTypes;
public BoundedMatcher(Class<? extends S> expectedType) {
this.expectedType = checkNotNull(expectedType);
this.interfaceTypes = new Class[0];
}
public BoundedMatcher(
Class<?> expectedType, Class<?> interfaceType1, Class<?>... otherInterfaces) {
this.expectedType = checkNotNull(expectedType);
checkNotNull(otherInterfaces);
int interfaceCount = otherInterfaces.length + 1;
this.interfaceTypes = new Class[interfaceCount];
interfaceTypes[0] = checkNotNull(interfaceType1);
checkArgument(interfaceType1.isInterface());
int interfaceTypeIdx = 1;
for (Class<?> intfType : otherInterfaces) {
interfaceTypes[interfaceTypeIdx] = checkNotNull(intfType);
checkArgument(intfType.isInterface());
interfaceTypeIdx++;
}
}
// 这里参数item的类型是S, 在创建BoundedMatcher实例时传入的泛型S
protected abstract boolean matchesSafely(S item);
// 真正的逻辑处理,判断传入的expectedType是否是同时传入的 interfaceTypes 中的类型,
// 满足条件之后,调用matchesSafely()方法进行匹配。
@Override
@SuppressWarnings({"unchecked"})
public final boolean matches(Object item) {
if (item == null) {
return false;
}
if (expectedType.isInstance(item)) {
for (Class<?> intfType : interfaceTypes) {
if (!intfType.isInstance(item)) {
return false;
}
}
return matchesSafely((S) item);
}
return false;
}
}
BoundedMatcher
本身的定义中包含了两个构造方法,第二个构造方法中可以同时传入多个需要进行匹配的类型。我们在上述实现使用的是单个参数的构造方法。
@Test
public void testCustomMatcher() throws InterruptedException {
Activity activity = activityTestRule.getActivity();
onView(withId(R.id.textview_red_text)).check(matches(withColoredText(activity.getColor(android.R.color.holo_red_dark)))).perform(click());
Thread.sleep(1000); // 为了执行看清UI变化
// 点击button,修改TextView文字颜色
onView(withId(R.id.button_change_color)).check(matches(isAssignableFrom(Button.class))).perform(click());
Thread.sleep(1000);
// 判断修改后的问题是否是预期的颜色
onView(withId(R.id.textview_common_text)).check(matches(withColoredText(Color.BLUE))).perform(click());
}
这里直接使用check()方法来检查传入的颜色是否与ID匹配的TextView
颜色形同。
看下运行效果。
RecyclerView Matcher
要在RecyclerView
上无法简单使用ViewMatcher
找到 Item,那么就需要自定义RecyclerView
使用的 Matcher。
结合 RecyclerViewActions
中定义的action方法(基于position, viewholder的参数),可以使用BuondedMatcher
来自定义Matcher。
可以先看如下定义:
static class CustomRecyclerViewMatchers {
public static Matcher<RecyclerView.ViewHolder> withTitle(final String expectedTitle) {
Checks.checkNotNull(expectedTitle);
return new BoundedMatcher<RecyclerView.ViewHolder, CustomAdapter.ViewHolder>(CustomAdapter.ViewHolder.class) {
@Override
public void describeTo(Description description) {
description.appendText("Find ViewHolder with title: " + expectedTitle);
}
@Override
protected boolean matchesSafely(CustomAdapter.ViewHolder item) {
String holderTitle = item.textView.getText().toString();
return TextUtils.equals(expectedTitle, holderTitle);
}
};
}
}
在构造BuondedMatcher
时,传入的类型分别是RecyclerView.ViewHolder
及CustomAdapter.ViewHolder
,参数传入的对比类型即是CustomAdapter.ViewHolder.class
。
在调用上,
@Test
public void testRecyclerViewCustomMatcher() {
onView(withId(R.id.recyclerView)).perform(scrollToHolder(CustomRecyclerViewMatchers.withTitle("This is the middle"))).check(matches(isDisplayed()));
}
上述调用中,首先在RecyclerView
中找到 Title 值是 This is the middle
的 Item, 并判断是否显示。
这样一个自定义的 RecyclerView
Matcher也就完成了。
总结
除了 BoundedMatcher
,Espresso还提供其他诸如CustomMatcher
,TypeSafeMatcher
等可用于自定义Matcher的定义。
这篇文章中只列举了BoundedMatcher
的使用,其他Matcher的使用或执行原理,感兴趣的同学可以自己再查看下定义。