Android的UI自動化測試(Espresso進階)

總目錄:Espresso從開始到…

onData

onView()方法採用hamcrest適配器,用於匹配當前視圖結構中有且僅有一個的控件。在大多數情況下onView()可以滿足我們在測試過程中對於控件定位的需求。但是,在處理AdapterView的時候,由於AdapterView的數據源可能很長,很多時候無法一次性將所有數據源顯示在屏幕上,對於沒有顯示在屏幕上的那部分數據,我們通過onView()是沒有辦法找到的。

常見的ListView、GridView、Spinner都屬於AdapterView
在使用方法上onData()onView()並沒有過大的差異,主要的區別是:

  • onView()匹配當前視圖結構中有且僅有一個的控件
  • onData()匹配AdapterView()數據源中有且僅有一個的數據

我舉個栗子

來看一下Espresso的官方文檔中例子,界面很簡單Activity只有一個ListView,而且根據官方的描述可以知道,當前ListView的每一行都是對應一個Map

List Activity

首先用onView()進行控件查找操作,由於onView()只能對視圖內的控件進行查找,這裏選擇第一條進行測試:

onView(allOf(withText("7"), hasSibling(withText("item 0"))))
    .perform(click());

這裏想要進行點擊操作的是第一個item中的“7”,所以使用匹配器withText("7")進行篩選,但是由於當前界面內有多個控件中text爲“7”,所以額外添加匹配器hasSibling(withText("item 0"))選擇相鄰控件中包含text“7”的控件,由於這裏使用了多個匹配器便增加了allOf()來將多個匹配器整合起來,當做一條完整的匹配條件使用。


現在用onData()進行控件查找操作,爲了彰顯onData()與上面的區別,這裏選擇第五十條進行測試:

onData(allOf( 
  is(instanceOf(Map.class)),
  hasEntry(equalTo("STR"), is("item: 50) ))          
  .perform(click());

allOf()的用法相同,用來將多個匹配條件整合成一條。
instanceOf(Map.class)就是字面意思,匹配的數據繼承自Map.class
hasEntry(equalTo("STR"), is("item: 50) ))可以簡單的理解爲查找Map<"STR","item:50">,其中is()equalTo()是將string封裝爲Matcher的方式,會在之後的文章中進行接收,這裏就不過多的贅述了。

還有一點,因爲onData()是對數據進行匹配的,所以當在同一個界面中有多個AdapterView時,就會報錯,因爲電腦不清楚你需要對哪一組數據進行操作,這裏可以使用inAdapterView()來確定當前需要的數據源。

onData(Matcher<object>)
    .inAdapterView(Matcher<View>)
    .perform(click());

注意:
onData()不適用於RecyleView和ScrollView,這兩者與AdapterView在表現形式上類似,但是工作原理是不同的,不能混淆。

  • ScrollView
    如果操作的視圖在 ScrollView (水平或垂直方向)中,需要考慮在對該視圖執行操作(如 click() )之前通過 scrollTo() 方法使其處於顯示狀態。這樣就保證了視圖在執行其他操作之前是在當前視圖範圍內。
    onView(…).perform(scrollTo(), click());
  • RecyleView
    如果想要與RecyleView交互進行自動化測試,需要引入“espresso-contrib”,裏邊包含一系列的Actions可以用於滾動和點擊。
/**
 *點擊position位置的item
 */
onView(withId(R.id.recycleview))
 .perform(RecyclerViewActions.actionOnItemAtPosition(position,click()));
    
/**
 *滑動到position位置的item
 */
onView(withId(R.id.recycleview))
    .perform(scrollToPosition(position));

Toast

Toast並沒有在我們的常規視圖中,Android支持多窗口,如果我們使用常規的方式是無法檢測到Toast的,所以這裏需要使用inRoot()來進行對Toast的匹配:

onView(withText("South China Sea"))
    .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
    .perform(click());

IdlingResource

IdlingResource本身只是一個簡單的接口

public interface IdlingResource {
    //標識異步資源佔用的名字,在日誌中會顯示
    public String getName();
    //返回目前資源是否可用(閒置),
    public boolean isIdleNow();
    //Espresso會註冊此回調
    public void registerIdleTransitionCallback(ResourceCallback callback);
    //回調接口
    public interface ResourceCallback {
        public void onTransitionToIdle();
    }
}

使用方法

  1. 使用時首先實現接口IdlingResourcepublic boolean isIdleNow();的返回值進行控制
public class SimpleIdlingResource implements IdlingResource {

    @Nullable private volatile ResourceCallback mCallback;
    /**
     * 管理阻塞狀態
     */
     private AtomicBoolean mIsIdleNow = new AtomicBoolean(true);

    @Override
    public String getName() {
        return this.getClass().getName();
    }

    @Override
    public boolean isIdleNow() {
        return mIsIdleNow.get();
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback callback) {
        mCallback = callback;
    }

    /**
     * 在耗時操作的前後分別調用此方法,來改變 isIdleNow() 的狀態,阻塞測試
     */
    public void setIdleState(boolean isIdleNow) {
        mIsIdleNow.set(isIdleNow);
        if (isIdleNow && mCallback != null) {
            mCallback.onTransitionToIdle();
        }
    }
}
  1. Activity中在耗時操作開始和結束時分別修改Idle狀態。
private void loadData() {

       //耗時操作開始變更爲忙碌狀態(false)阻塞測試線程
       mIdlingResource.setIdleState(false);
       
       loadData(new Callback{
         @Override
         void onCall(Data data){
         content.setText("finish data");
         
         //耗時操作完成變更爲空閒狀態(true)開啓測試線程
         mIdlingResource.setIdleState(true);
         }
    })
  1. 在Test的@Before@After中分別進行註冊和註銷操作:
Espresso.registerIdlingResources(mIdlingResource);

Espresso.unregisterIdlingResources(mIdlingResource);

注意:
Activity中與Test中爲相同mIdlingResource,需要在Test@Before中獲取Activity中的mIdlingResource,IdlingResource對象需要在Activity(業務邏輯代碼)中創建

IdlingResource的使用,會涉及到App中邏輯代碼的變化,爲了測試專門在業務代碼中增加額外的邏輯,需要測試人員對於代碼或者開發人員對於測試有一定的瞭解,亦或者兩者不分,才能真正良好的IdlingResource。

ActionBar

ActionBar有兩種不同的形式,普通的ActionBar直接通過onView(withId(R.id.xxx))訪問即可。
另一種就是菜單中的條目,常規的菜單條目可以通過以下代碼執行:
openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());
在帶有硬件懸浮菜單按鈕的設備上,可以通過通過下面的方式模擬硬件點擊:
(沒有遇到過這種設備,所以也沒有使用過)
openContextualActionModeOverflowMenu();

Espresso-Intent

當使用 Espresso-Intents 時,應當用IntentsTestRule替換ActivityTestRuleIntentsTestRule使得在 UI 功能測試中使用 Espresso-Intents API 變得簡單。該類是 ActivityTestRule 的擴展,它會在每一個被@Test註解的測試執行前初始化 Espresso-Intents,然後在測試執行完後釋放Espresso-Intents。

Espresso-Intents分別提供了Intent validationIntent stubbing的方法intended() 和intending().可以理解爲intended()是在Intent發出後進行檢查 ,而intending()是在Intend發出之前設置檢查項。這裏舉出官方Demo的栗子:

@Test
public void validateIntentSentToPackage() {
    // 用戶發出了“打電話”的 Intent
    user.clickOnView(system.getView(R.id.callButton));
    //檢查 Intent 已經被髮出
    intended(toPackage("com.android.phone"));
}


@Test
public void activityResult_IsHandledProperly() {
    /**
     * 新建 Intent 並封裝爲 ActivityResult
     */
    Intent resultData = new Intent();
    String phoneNumber = "123-345-6789";
    resultData.putExtra("phone", phoneNumber);
    ActivityResult result = new ActivityResult(Activity.RESULT_OK, resultData);
    
    //設置對Intent的檢查
    intending(toPackage("com.android.contacts")).respondWith(result));
    
    //執行發送Intent的操作
    onView(withId(R.id.pickButton)).perform(click());
    onView(withId(R.id.phoneNumber).check(matches(withText(phoneNumber)));
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章