Android自动化测试-从入门到入门(5)AdapterView的测试
来自: http://segmentfault.com/a/1190000004392396
在之前的文章中,我们简单介绍了 Espresso 的使用。通过 onView() 方法我们可以快速定位到界面上我们需要测试的目标元素。整体来说, onView() 比较适用于UI比较简单的情况,在不需要过于复杂的匹配条件的情况下是很方便的。但是,对于类似 ListView 这种有UI复用的元素来说,只是通过 onView() 就显得复杂了一点,我们来看一下针对这种情况应有何种方案。
AdapterView
AdapterView 是一种通过 Adapter 来动态加载数据的界面元素。我们常用的 ListView , GridView , Spinner 等等都属于 AdapterView 。不同于我们之前提到的静态的控件, AdapterView 在加载数据时,可能只有一部分显示在了屏幕上,对于没有显示在屏幕上的那部分数据,我们通过 onView() 是没有办法找到的。
对于 AdapterView , Espresso 提供了如下方法用来查找元素:
/** * 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 匹配的匹配规则;
-
Matcher<? extends Object> : 构造一个针对于 Object (数据)匹配的匹配规则。
从以上对比可以看出,我们在使用 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 的强大之处了!为了更好地给大家介绍自定义 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 的数据项了。
指定AdapterView
这样就完了嘛?是的,针对自定义 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 只是一个工具,至于具体如何去用,就看我们自己的发挥啦!