在以前的文章中,咱们简单介绍了Espresso
的使用。经过onView()
方法咱们能够快速定位到界面上咱们须要测试的目标元素。总体来讲,onView()
比较适用于UI比较简单的状况,在不须要过于复杂的匹配条件的状况下是很方便的。可是,对于相似ListView
这种有UI复用的元素来讲,只是经过onView()
就显得复杂了一点,咱们来看一下针对这种状况应有何种方案。segmentfault
AdapterView
是一种经过Adapter
来动态加载数据的界面元素。咱们经常使用的ListView
, GridView
, Spinner
等等都属于AdapterView
。不一样于咱们以前提到的静态的控件,AdapterView
在加载数据时,可能只有一部分显示在了屏幕上,对于没有显示在屏幕上的那部分数据,咱们经过onView()
是没有办法找到的。app
对于AdapterView
,Espresso
提供了以下方法用来查找元素:ide
/** * Creates an {@link DataInteraction} for a data object displayed by the application. Use this * method to load (into the view hierarchy) items from AdapterView widgets (e.g. ListView). * * @param dataMatcher a matcher used to find the data object. */ public static DataInteraction onData(Matcher<? extends Object> dataMatcher) {}
咱们首先来研究一下这个方法的返回值。从以上定义能够看出,该方法返回了一个DataInteraction
对象,还记得onView()
方法返回的ViewInteraction
对象么?这二者的区别能够大概描述为:工具
ViewInteraction
: 关注于已经匹配到的目标控件。经过onView()
方法咱们能够找到符合匹配条件的惟一的目标控件,咱们只须要针对这个控件进行咱们须要的操做。布局
DataInteraction
: 关注于AdapterView
的数据。因为AdapterView
的数据源可能很长,不少时候没法一次性将全部数据源显示在屏幕上,所以咱们主要先关注AdapterView
中包含的数据,而非一次性就进行View的匹配。学习
咱们再来研究一下这个方法的入参。从以上定义看出,该方法接收了一个Matcher<? extends Object>
的参数,该参数用来指定一个匹配规则。还记得onView()
的入参么?是一个Matcher<View>
对象。从类型上来看,这二者的区别也不言而喻:测试
Matcher<View>
: 构造一个针对于View
匹配的匹配规则;ui
Matcher<? extends Object>
: 构造一个针对于Object
(数据)匹配的匹配规则。this
从以上对比能够看出,咱们在使用onData()
方法对AdapterView
进行测试的时候,咱们的思路就转变成了首先关注这个AdapterView
的具体数据,而不是UI上呈现的内容。固然,咱们最终的目标仍是要找到目标的UI元素,可是咱们是经过其数据源来进行入手的。设计
那么,接下来,咱们就要学习如何去寻找咱们须要的数据了!显然,要想找到咱们须要的数据,就须要构造一个onData()
所使用的Matcher
对象,而这个对象的构造和使用实际上和以前咱们所用的针对于View
的Matcher
大概雷同。好比,咱们能够指定单一条件:
onData(is(instanceOf(MyObject.class)))
表示咱们须要找一个AdapterView
,其数据源的类型是MyObject
(这是一个自定义的类)。固然了,咱们确定仍是须要更加精确地去寻找一个AdapterView
中的指定条目,因而咱们能够采用allOf()
来构造一个符合匹配条件:
onData(allOf(is(instanceOf(MyObject.class)), myCustomMatcher()))
如上代码便使用allOf()
方法构造了一个符合匹配规则(allOf()
方法能够参考第三篇文章Espresso入门里的介绍)。而上面的myCustomMatcher()
方法构造了一个自定义的Matcher
,咱们能够采用本身的自定义Matcher
来更加精准地进行数据的匹配。
接下来咱们要感觉一下自定义Matcher
的强大之处了!为了更好地给你们介绍自定义Matcher
,我举一个答疑君APP里面的例子来进行说明。
在答疑君APP的老师页面,有一个老师搜索的功能。当我点击搜索框时,界面上便会显示以前的搜索关键字历史。如今,我须要在这个搜索关键字列表中点击相应的关键字来触发搜索。
简单来讲,个人目的就是:在搜索历史ListView
中点击搜索关键字为TEXT
的条目。
首先,个人ListView
的数据源类型为List<SearchItem>
,因而我先构造一个数据类型匹配条件:
is(instanceOf(SearchItem.class))
这个构造条件就指定了列表的数据源为SearchItem
类型。请注意,Espresso
在根据onData()
进行类型匹配时,是根据咱们的Adapter.getItem()
方法返回的数据类型进行匹配的。若是咱们本身实现了一个自定义的Adapter
,请注意咱们构造的匹配规则要和getItem()
方法返回的数据类型相统一。
接下来,我就须要去找那个含有TEXT
关键字的数据项了。个人SearchItem
类的定义极其简单:
public class SearchItem { private String keyword; public SearchItem() {} public void setKeyword(String keyword) { this.keyword = keyword; } public String getKeyword() { return keyword; } }
接下来我只要找到那个keyword
为TEXT
的SearchItem
数据项就能够了。为此,我构造了以下的一个自定义Matcher
:
/** * 查找指定关键字的搜索条件 * @param name 须要搜索的关键字 */ public static Matcher<Object> teacherSearchItemWithName(final String name) { return new BoundedMatcher<Object, SearchItem>(SearchItem.class) { @Override protected boolean matchesSafely(SearchItem item) { return item != null && !TextUtils.isEmpty(item.getKeyword()) && item.getKeyword().equals(name); } @Override public void describeTo(Description description) { description.appendText("SearchItem has Name: " + name); } }; }
接下来对该方法作一些说明,以助于帮助你们构造本身的Matcher:
1. @return Matcher<Object>
很显然,返回值必须是一个Matcher<Object>
对象,表明一个针对于Object
数据的匹配规则。这也是onData()
方法入参的要求。
2. BoundedMatcher
以上方法其实是构造了一个BoundedMatcher
,咱们先来看一下BoundedMatcher
的定义:
/** * Some matcher sugar that lets you create a matcher for a given type * but only process items of a specific subtype of that matcher. * * @param <T> The desired type of the Matcher. * @param <S> the subtype of T that your matcher applies safely to. */ public abstract class BoundedMatcher<T, S extends T> extends BaseMatcher<T> { // ... protected abstract boolean matchesSafely(S item); // ... }
由以上定义咱们能够看到,BoundedMatcher
为咱们指定了一个针对目标类型的子类型进行匹配的匹配规则。好比,咱们如今须要一个Matcher<Object
>对象,但实际上咱们须要考察的目标类型是SearchItem
,而SearchItem
又是Object
的子类,所以,咱们能够经过BoundedMatcher
来构造这个Matcher<Object>
对象,只不过咱们实际上进行检查的转变成了SearchItem
类型,只要采用以下写法:
return new BoundedMatcher<Object, SearchItem>(SearchItem.class) {...}
3. matchesSafely()
上述复写的matchesSafely()
方法即是真正执行匹配的地方了!你们能够看到,我由BoundedMatcher
指定了SearchItem
类型,所以matchesSafely()
方法也接收了SearchItem
类型的入参,咱们只要去考察入参提供的这个SearchItem
对象是否符合咱们的匹配条件便可:
return item != null && !TextUtils.isEmpty(item.getKeyword()) && item.getKeyword().equals(name);
在如上代码中,我作了三步检查:
指定SearchItem
自己不为null
;
指定SearchItem
的keyword
不为空;
指定SearchItem
的keyword
和咱们须要匹配的name
相同。
只有符合这三个条件,咱们才会认为当前的SearchItem
数据项符合咱们的预期。
综合以上,将以前的两个Matcher
复合一下,我即可以构造以下的符合匹配规则了:
onData(allOf(is(instanceOf(SearchItem.class)), teacherSearchItemWithName(TEXT)))
这样一来,我就可以成功地在个人搜索历史列表中找到关键字为TEXT
的数据项了。
这样就完了嘛?是的,针对自定义Matcher
就已经讲完了。实际上我在运行以上脚本的时候,Espresso
仍是给我报了个AmbiguousViewMatcherException
的异常。这是由于,答疑君APP的布局比较复杂,在当前的View Hierarchy
中有好几个AdapterView
,我须要指定我要进行匹配的AdapterView
究竟是哪个。
Espresso
提供了以下方法来完成这件事情:
/** * Selects a particular adapter view to operate on, by default we operate on any adapter view * on the screen. */ public DataInteraction inAdapterView(Matcher<View> adapterMatcher){}
inAdapterView()
可让咱们指定咱们须要匹配哪一个AdapterView
。个人搜索历史列表的id为teacher_page_search_history_list
,所以,我只要在上面的基础上增长以下一行:
onData(allOf(is(instanceOf(SearchItem.class)), teacherSearchItemWithName(TEXT))) .inAdapterView(withId(R.id.teacher_page_search_history_list))
便解决了问题。如今,Espresso
只会针对个人搜索历史列表进行数据匹配了!
到目前为止,咱们介绍了onView()
和onData()
的使用。从以上的文章中,相信你们也可以感觉到这两种匹配思路的设计目的与区别。在咱们平时的测试脚本编写的过程当中,我我的仍是建议,一切都要按照咱们本身的实际状况来进行方法的选择。
实际上,虽然onData()
方法是针对AdapterView
来进行测试的,可是在答疑君的测试脚本中,有时针对AdapterView
我也会采用onView()
方法直接去进行匹配,由于有些简单的场景实际上是不须要那么复杂的数据分析的,只关注于UI上的显示我也可以找到ListView
中的某个控件。话说回来,Espresso
只是一个工具,至于具体如何去用,就看咱们本身的发挥啦!
Android自动化测试-从入门到入门(1) Hello Testing!
Android自动化测试-从入门到入门(2) Testing APIs
Android自动化测试-从入门到入门(3) Espresso入门
Android自动化测试-从入门到入门(4) uiautomatorviewer
Android自动化测试-从入门到入门(5) AdapterView的测试
Android自动化测试-从入门到入门(6) 会玩的Espresso
Android自动化测试-从入门到入门(7) UI Automator