Android自动化测试框架---Espresso(2)

上一篇文章介绍了有关android自动化测试的环境搭建,以及Espresso的简单用法,这篇文章主要针对一些比较复杂的控件,不易通过查找元素的方式寻找。
原文可见:https://segmentfault.com/a/1190000004392396

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()方法构造了一个符合匹配规则。而上面的myCustomMatcher()方法构造了一个自定义的Matcher,我们可以采用自己的自定义Matcher来更加精准地进行数据的匹配。

自定义Matcher

接下来我们要感受一下自定义Matcher的强大之处了!为了更好地给大家介绍自定义Matcher,我举一个答疑君APP里面的例子来进行说明。

在答疑君APP的老师页面,有一个老师搜索的功能。当我点击搜索框时,界面上便会显示之前的搜索关键字历史。现在,我需要在这个搜索关键字列表中点击相应的关键字来触发搜索。

简单来说,我的目的就是:在搜索历史ListView中点击搜索关键字为TEXT的条目。

首先,我的ListView的数据源类型为List,于是我先构造一个数据类型匹配条件:

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只会针对我的搜索历史列表进行数据匹配了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值