Android自動化測試-從入門到入門(5)AdapterView的測試

jshwinlove 9年前發布 | 11K 次閱讀 自動化測試 Android開發 移動開發

來自: 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&lt;Object&gt;

很顯然,返回值必須是一個 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 只是一個工具,至于具體如何去用,就看我們自己的發揮啦!

附錄

 本文由用戶 jshwinlove 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!