(4.5.5.4)Espresso的进阶: OnView & onData & Matchers
来源:互联网 发布:蝴蝶战法指标公式源码 编辑:程序博客网 时间:2024/06/06 07:11
- 一源码分析
- 1 TypeSafeMatcher
- 2 BoundedMatcher
- 二工具集
- 1 ViewMatchers View匹配器适配类
- 2 RootMatchers 根视图匹配器的辅助类
- 3 Matchers 对Matcher的操作集合类
- 4 CursorMatchers
- 5 LayoutMatchers 匹配以检测典型的布局问题
- 6 PreferenceMatchers 匹配存储
- 三 AdapterView的OnData
- 1 简单类型
- 2 官方示例
- 21 isinstanceOfMapclass 类型校验
- 22 hasEntryequalToSTR isitem 50参数校验
- 3 自定义Adapter未打破继承约束
- 4 自定义Adapter打破继承约束 usingAdapterViewProtocol
- 5 OnData支持的筛选
- 6 匹配 ListView 的 footerheader 视图
- 四RecyleView
- 1 actionOnItem源码解析
- 11 viewHolderMatcher
- 12 ActionOnItemViewAction
- 1 actionOnItem源码解析
- 参考文献
- 一源码分析
Espresso编写自动化就做三件事情:找某些东西、做某些事情、检查某些东西
- 找到 并返回 XXInteraction交互类
- public static ViewInteraction onView(final Matcher< View > viewMatcher)
- public static DataInteraction onData(Matcher< ? extends Object> dataMatcher)
让我们先来看看Matchers 都有哪些API可供我们使用
- Classes:BoundedMatcher< T, S extends T >
Some matcher sugar that lets you create a matcher for a given type but only process items of a specific subtype of that matcher.
- Classes:CursorMatchers
A collection of Hamcrest matchers that matches a data row in a Cursor.
- Classes:CursorMatchers.CursorMatcher
A Matcher that matches Cursors based on values in their columns.
- Classes:LayoutMatchers
A collection of hamcrest matches to detect typical layout issues.
- Classes:PreferenceMatchers
A collection of hamcrest matchers that match Preferences.
- Classes:RootMatchers
A collection of matchers for Root objects.
- Classes:ViewMatchers
A collection of hamcrest matchers that match Views.
- Enums:ViewMatchers.Visibility
ViewMatchers.Visibility Enumerates the possible list of values for View.getVisibility()
一、源码分析
- 所有的校验都 实现了 interface Matcher
public interface Matcher extends SelfDescribing {
/** * 遍历当前视图中的Views,匹配到则返回true */ boolean matches(Object item); /** * 描述信息 */ void describeMismatch(Object item, Description mismatchDescription);}
当然在实际过程中,我们没有直接去实现Matcher,而是实现其子类BaseMatcher所派生的封装类。
public abstract class BaseMatcher<T> implements Matcher<T>
譬如:
//进行类型校验abstract class TypeSafeMatcher<T> extends BaseMatcher<T>
1.1 TypeSafeMatcher
直接上两个例子吧:
/*** 返回具有指定 Tag 的View*/ public static Matcher<View> withTag(final Object tag) { return new TypeSafeMatcher<View>() { @Override public void describeTo(Description description) { description.appendText("with key: " + tag); } @Override public boolean matchesSafely(View view) { return tag.equals(view.getTag()); } }; } /** * 在X中的 满足Y 的 X的直系子节点 * @param parentMatcher 目标view的直接父节点 * @param childtMatcher 目标view满足 * @return */ public static Matcher<View> childWithMatcher( final Matcher<View> parentMatcher, final Matcher<View> childtMatcher) { return new TypeSafeMatcher<View>() { @Override public void describeTo(Description description) { description.appendText("Child with childtMatcher in parent "); parentMatcher.describeTo(description); } @Override public boolean matchesSafely(View view) { ViewParent parent = view.getParent(); return parent instanceof ViewGroup && parentMatcher.matches(parent) && childtMatcher.matches(view); } }; }
1.2 BoundedMatcher
相对于 TypeSafeMatcher,BoundedMatcher 允许您为给定类型创建匹配器,但只能处理该匹配器的特定子类型项;
从而不需要关注类型项,而只关注 对象的具体业务
public static Matcher<View> withText(final Matcher<String> stringMatcher) { checkNotNull(stringMatcher); return new BoundedMatcher<View, TextView>(TextView.class) { @Override public void describeTo(Description description) { description.appendText("with text: "); stringMatcher.describeTo(description); } @Override public boolean matchesSafely(TextView textView) { return stringMatcher.matches(textView.getText().toString()); } }; }
- BoundedMatcher来确保匹配器只匹配TextView类及其子类
- 这使得很容易在BoundedMatcher.matchesSafely()中实现匹配逻辑本身:只需从TextView中获取getText()方法并将其送入下一个匹配器
- 有一个简单的describeTo()方法的实现,它只用于生成调试输出到控制台。
二、工具集
2.1 ViewMatchers View匹配器适配类
最重要也是应用最广的匹配器,通过一个或者多个来定位层级里面的控件。
包含了大量的常见的匹配操作,大部分都是通过TypeSafeMatcher 和 BoundedMatcher实现的..
2.2 RootMatchers 根视图匹配器的辅助类
匹配root装饰视图匹配给定的视图匹配器,也就是一系列静态方法,返回指定的 Matcher
onView(withText("Text")) .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView())))) .perform(click());
RootMatchers还有以下方法可以应用到其他场景:
下面给几个简单应用
//指定View的跟视图不在当前的Activity根布局层次中,例如ToastinRoot(withDecorView(not(mRules.getActivity().getWindow().getDecorView())))inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
2.3 Matchers 对Matcher的操作集合类
- allof(Matchers ) 满足Matchers 定义的所有Matcher
- anyof(Matchers ) 至少满足Matchers 定义的任意一个Matcher
- is(…)
- not(…)
- notNullValue()
- nullValue()
- endsWith(java.lang.String suffix)
- startsWith(java.lang.String prefix)
2.4 CursorMatchers
Hamcrest的集合匹配器,在Cursor匹配相应的数据行
/** * Returns a matcher that matches a {@link String} value at a given column index * in a {@link Cursor}s data row. * <br> * @param columnIndex int column index * @param value a {@link String} value to match */ public static CursorMatcher withRowString(int columnIndex, String value) { return withRowString(columnIndex, is(value)); }
大部分的场景,大多发生于表单或者滚动menu时:
onData( is(instanceOf(Cursor.class)), CursorMatchers.withRowString("job_title", is("Barista")));
2.5 LayoutMatchers 匹配以检测典型的布局问题
例如匹配具有椭圆形文本的TextView元素。
如果文本太长,无法适应TextView,它可以是椭圆形(’Too long’显示为’Too l …’或’… long’)或切断(’Too long“显示为”Too l“)。
虽然在某些情况下可以接受,但通常表示不好的用户体验
2.6 PreferenceMatchers 匹配存储
Preference组件其实就是Android常见UI组件与SharePreferences的组合封装实现
onData(Matchers.<Object>allOf(PreferenceMatchers.withKey("setting-name"))).perform(click());
PreferenceMatchers还有以下方法可以应用到其他场景
withSummary(final int resourceId)withSummaryText(String summary)withSummaryText(final Matcher<String> summaryMatcher)withTitle(final int resourceId)withTitleText(String title)withTitleText(final Matcher<String> titleMatcher)isEnabled()
onView是根据View的相关属性来找到Interaction交互类
OnData则是根据 Data的相关属性来找到Interaction交互类
三、 AdapterView的OnData
ViewInteraction: 关注于已经匹配到的目标控件。通过onView()方法我们可以找到符合匹配条件的唯一的目标控件,我们只需要针对这个控件进行我们需要的操作
- onView()
- Matcher< View>: 构造一个针对于View匹配的匹配规则
DataInteraction: 关注于AdapterView的数据。由于AdapterView的数据源可能很长,很多时候无法一次性将所有数据源显示在屏幕上,因此我们主要先关注AdapterView中包含的数据,而非一次性就进行View的匹配。
- onData
- Matcher< ? extends Object>: 构造一个针对于Object(数据)匹配的匹配规则
onData 只适用于 AdapterView及其派生类,不适用于 RecyleView。
AdapterView是一种通过Adapter来动态加载数据的界面元素。我们常用的ListView, GridView, Spinner等等都属于AdapterView。不同于我们之前提到的静态的控件,AdapterView在加载数据时,可能只有一部分显示在了屏幕上,对于没有显示在屏幕上的那部分数据,我们通过onView()是没有办法找到的。
- Matcher< ? extends Object>的构建规则:
- 类型校验 : 确认AdapterView
- 参数校验:确认item
注:初始化时就显示在屏幕上的adapter中的view你也可以不适用onData()因为他们已经被加载了。然而,使用onDta()会更加安全。
提醒:在打破了继承约束(尤其是getItem()的API)实现了AdatpterView的自定义view中onData()是有问题的。在这中情况下,做好的做法就是重构应用的代码。如果不重构代码,你也可以实现自定义的AdapterViewProtocol来实现。查看Espresso的AdapterViewProtocols 来查看更多信息。
- 更多AdapterViewProtocol
3.1 简单类型
- 类型校验
- 参数校验
onData(allOf(is(instanceOf(String.class)),is("Americano"))).perform(click());
3.2 官方示例
如上 activity 包含一个 ListView,它基于一个为每一行提供一个 Map
onData(allOf( is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50) )).perform(click());
3.2.1 is(instanceOf(Map.class)) 类型校验
限制搜索 AdapterView 中任意条目的条件为一个 Map。
在此例子中,ListView 的所有行都满足条件。但我们想要点击指定的条目 “item: 50”,所以我们需要继续缩小范围:
3.2.2 hasEntry(equalTo(“STR”), is(“item: 50)参数校验
hasEntry(equalTo("STR"), is("item: 50)
优化:
这个 Matcher< String, Object > 会匹配所有包含任意键,值=“item: 50” 的 Map 。鉴于查找此条目的代码较长,而且我们希望在其他地方重用它,我们可以写一个自定义的 matcher “withItemContent”:
public static Matcher<Object> withItemContent(String expectedText) { checkNotNull(expectedText); return withItemContent(equalTo(expectedText));}public static Matcher<Object> withItemContent(Matcher itemTextMatcher) { return new BoundedMatcher<Object, Map>(Map.class) { @Override public boolean matchesSafely(Map map) { return hasEntry(equalTo("STR"), itemTextMatcher).matches(map); } @Override public void describeTo(Description description) { description.appendText("with item content: "); itemTextMatcher.describeTo(description); } };}
现在点击该条目的代码很简单了:
onData(withItemContent("item: 50")).perform(click());
此测试的完整代码请查看 AdapterViewText#testClickOnItem50 和自定义匹配器。
3.3 自定义Adapter:未打破继承约束
使用BoundedMatcher实现:
- 类型校验
- 参数校验
其实没啥说的,直接给个示例吧:
//根据销售机会名称查找到List中的item,并点击onData(SaleOppMatcher.searchMainItemWithName(oppName)).perform(click());
/** * 查找指定搜索条件 销售机会匹配 * @param name 需要搜索的字 */ public static Matcher<Object> searchMainItemWithName(final String name) { return new BoundedMatcher<Object, SalesOpp>(SalesOpp.class) { @Override protected boolean matchesSafely(SalesOpp item) { return item != null && !TextUtils.isEmpty(item.content) && item.content.equals(name); } @Override public void describeTo(Description description) { description.appendText("SalesOpp has Name: " + name); } }; }
3.4 自定义Adapter:打破继承约束 usingAdapterViewProtocol
//根据客户拜访名称查找到List中的item,并点击onData(LegWorkMatcher.searchMainItemWithName(legName)).perform(click());
- (4.5.5.4#)Espresso的进阶: AdapterViewProtocol
3.5 OnData支持的筛选
onData我们使用了data进行了筛选,完全屏蔽了View,那么现在问题来了:
假设一个页面里有多个ListView,譬如“ViewPager+ListView的实现”
如果继续用上述方法,会出现:
android.support.test.espresso.AmbiguousViewMatcherException: ‘is assignable from class: class android.widget.AdapterView’ matches multiple views in the hierarchy. Problem views are marked with ‘*MATCHES*’ below.
大意就是说,有多个AdapterView在界面里,那么结局方案是:
- inAdapterView(Matcher adapterMatcher)
onData(...).inAdapterView(allOf(isAssignableFrom(AdapterView.class),isDisplayed())).perform(click());onData(...).inAdapterView(allOf(withId(R.id.list),isDisplayed())).perform(click());
现在我们详细的看看DataInteraction还支持那些筛选,来辅助onData:
- PullToRefreshListView 如何使用inAdapterView?
PullToRefreshListView里面的ListView实际上是嵌套了一个id为android.R.id.list的ListView,因此可以用这个ID来进行匹配。如果单一匹配规则还不够精细,可以再从其他方面构造复合匹配规则。比如针对这种情况我采用了:
onData(allOf(is(instanceOf(TeacherModel.class)),teacherHasName(VALUE_TEACHER_NAME_TARGET))).inAdapterView(allOf(withId(android.R.id.list), isDisplayed())).perform(click());
3.6 匹配 ListView 的 footer/header 视图
header 和 footer 通过 addHeaderView/addFooterView API 添加到 ListView 中。为了能够使用 Espresso.onData 加载它们,确保使用预置的值来设置数据对象(第二个参数)。
public static final String FOOTER = "FOOTER";...View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);((TextView) footerView.findViewById(R.id.item_content)).setText("count:");((TextView) footerView.findViewById(R.id.item_size)).setText(String.valueOf(data.size()));listView.addFooterView(footerView, FOOTER, true);
然后,你可以写一个匹配器来匹配此对象:
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));}
在测试中很轻易就能加载该视图:
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;import static com.google.android.apps.common.testing.ui.espresso.sample.LongListMatchers.isFooter;public void testClickFooter() { onData(isFooter()) .perform(click()); ...}
请在 AdapterViewtest#testClickFooter 中查看完整示例代码。
四、RecyleView
RecyclerViews 和 AdapterViews 的工作原理不同,因此onData()不适用于RecyleView
如果想要与RecyleView交互,请引入“espresso-contrib”,里边包含一系列的Actions可以用于滚动和点击
//TODO 根据android.support.test.espresso.contrib.RecyclerViewActions揣摩RecyclerView的macher规则
4.1 actionOnItem源码解析
public static <VH extends ViewHolder> PositionableRecyclerViewAction actionOnItem( final Matcher<View> itemViewMatcher, final ViewAction viewAction) { Matcher<VH> viewHolderMatcher = viewHolderMatcher(itemViewMatcher); return new ActionOnItemViewAction<VH>(viewHolderMatcher, viewAction); }
4.1.1 viewHolderMatcher
/** * Creates matcher for view holder with given item view matcher. * * @param itemViewMatcher a item view matcher which is used to match item. * @return a matcher which matches a view holder containing item matching itemViewMatcher. */ private static <VH extends ViewHolder> Matcher<VH> viewHolderMatcher( final Matcher<View> itemViewMatcher) { return new TypeSafeMatcher<VH>() { @Override public boolean matchesSafely(RecyclerView.ViewHolder viewHolder) { return itemViewMatcher.matches(viewHolder.itemView); } @Override public void describeTo(Description description) { description.appendText("holder with view: "); itemViewMatcher.describeTo(description); } }; }
4.1.2 ActionOnItemViewAction
private static final class ActionOnItemViewAction<VH extends ViewHolder> implements PositionableRecyclerViewAction { private final Matcher<VH> viewHolderMatcher; private final ViewAction viewAction; private final int atPosition; private final ScrollToViewAction<VH> scroller; private ActionOnItemViewAction(Matcher<VH> viewHolderMatcher, ViewAction viewAction) { this(viewHolderMatcher, viewAction, NO_POSITION); } private ActionOnItemViewAction(Matcher<VH> viewHolderMatcher, ViewAction viewAction, int atPosition) { this.viewHolderMatcher = checkNotNull(viewHolderMatcher); this.viewAction = checkNotNull(viewAction); this.atPosition = atPosition; this.scroller = new ScrollToViewAction<VH>(viewHolderMatcher, atPosition); } @SuppressWarnings("unchecked") @Override public Matcher<View> getConstraints() { return allOf(isAssignableFrom(RecyclerView.class), isDisplayed()); } @Override public PositionableRecyclerViewAction atPosition(int position) { checkArgument(position >= 0, "%d is used as an index - must be >= 0", position); return new ActionOnItemViewAction<VH>(viewHolderMatcher, viewAction, position); } @Override public String getDescription() { if (atPosition == NO_POSITION) { return String.format("performing ViewAction: %s on item matching: %s", viewAction.getDescription(), viewHolderMatcher); } else { return String.format("performing ViewAction: %s on %d-th item matching: %s", viewAction.getDescription(), atPosition, viewHolderMatcher); } } @Override public void perform(UiController uiController, View root) { RecyclerView recyclerView = (RecyclerView) root; try { scroller.perform(uiController, root); uiController.loopMainThreadUntilIdle(); // the above scroller has checked bounds, dupes (maybe) and brought the element into screen. int max = atPosition == NO_POSITION ? 2 : atPosition + 1; int selectIndex = atPosition == NO_POSITION ? 0 : atPosition; List<MatchedItem> matchedItems = itemsMatching(recyclerView, viewHolderMatcher, max); actionOnItemAtPosition(matchedItems.get(selectIndex).position, viewAction).perform( uiController, root); uiController.loopMainThreadUntilIdle(); } catch (RuntimeException e) { throw new PerformException.Builder().withActionDescription(this.getDescription()) .withViewDescription(HumanReadables.describe(root)).withCause(e).build(); } } }
参考文献
- Android ViewMatchers
- http://blog.csdn.net/qq744746842/article/category/6088564
- (4.5.5.4)Espresso的进阶: OnView & onData & Matchers
- Espresso 自动化测试(五)- onData() 的使用
- (4.5.5.4)Espresso的进阶: AdapterViewProtocol
- (4.5.5.3)Espresso的进阶: ViewAction
- (4.5.5.5)Espresso的进阶: ViewAssertions
- (4.5.5.6)Espresso的进阶: IdlingResource
- Espresso 自动化测试 (六) - onData()的使用
- (4.5.5.9)Espresso之UiAutomator2与Espresso的结合
- (4.5.5.10)Espresso之Robotium与Espresso的结合
- (4.5.5.2)Espresso的基础
- Matchers
- Espresso自动化测试(十三)- UiAutomator2与Espresso的结合
- (4.5.5.1) Espresso的简介、下载和安装
- mokito matchers when的坑
- 大牛们的浓咖啡(Espresso)简单介绍
- (4.5.5.7)Espresso之Intent测试
- Javascript测试框架Jasmine(二):Matchers
- Espresso指南二(Espresso意图)
- part-1 输入偏置电流和输入失调电流
- C++11中std::addressof的使用
- java线程中yield(),sleep(),wait()区别详解
- 题目1203:IP地址
- BigDecimal使用方式
- (4.5.5.4)Espresso的进阶: OnView & onData & Matchers
- android去掉振动相关选项
- PDO中事物详解
- fragment入门
- cin和scanf
- 关于function(e)中的e
- jquery获取图片的宽度
- c_简单的文件加解密
- 题目1204:农夫、羊、菜和狼的故事