【Android測試】【第十九節】Espresso——API詳解


 版權聲明:本文出自胖喵~的博客,轉載必須注明出處。

  轉載請注明出處:http://www.cnblogs.com/by-dream/p/5997557.html 

 

 

前言


  Espresso的提供了不少API支持使用者來和界面元素進行交互,但同時它又阻止使用者直接獲取Activity和View,它為的就是想保持讓這些對象在UI線程中執行,以防發生線程不安全的情況。因此在Espresso中我們看不到getView、getCurrentActivity類似這樣的方法。但是我們可以通過實行自己的ViewAction和ViewAssertion來安全的操作View。這就是Espresso的思想。

 

 

認識組件


  Espresso:與視圖交互的入口(通過onView和onData),還包含一些不綁定到任何元素上的API(例如pressBack)。

  ViewMatchers: 實現Matcher<? super View>接口對象的集合,可以將一個或者多個傳遞給onView,從而定位當前視圖中的元素。

  ViewActions:可以傳遞到ViewInteraction.perform方法的集合(例如click)。

  ViewAssertions:可以傳遞到ViewInteraction.check方法的集合。 大多數時候需要matches斷言,即使用View Matcher來和當前選擇的視圖的狀態進行斷言。

  舉例:

  

 

 

定位元素onView


  onView使用的是一個hamcrest匹配器,該匹配器只匹配當前視圖層次結構中的一個(且只有一個)視圖。如果你不熟悉hamcrest匹配器,建議先看看這個。通常情況下一個控件的id是唯一的,但是有些特定的視圖是無法通過R.id拿到,因此我們就需要訪問Activity或者Fragment的私有成員找到擁有R.id的容器。有的時候也需要使用ViewMatchers來縮小定位的范圍。

  最簡單的onView就是這樣的形式:

onView(withId(R.id.my_view))

  有的時候多個視圖之間共享R.id值,當這種情況下,我們調用系統會拋出這樣的異常AmbiguousViewMatcherException:

java.lang.RuntimeException:
com.google.android.apps.common.testing.ui.espresso.AmbiguousViewMatcherException:
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

  當然系統給出你詳細的信息,讓你進行排查:

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button, visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true, window-focus=true,
is-focused=false, is-focusable=false, enabled=true, selected=false, is-layout-requested=false, text=, root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button, visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true, window-focus=true,
is-focused=false, is-focusable=true, enabled=true, selected=false, is-layout-requested=false, text=Hello!, root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

  通過上面的信息對比,我們可以發現text字段是不一致的,因此我們就可以根據這個組合匹配來縮小定位范圍,方法如下:

onView(allOf(withId(R.id.my_view), withText("Hello!")))

  你也可以使用這樣的方法:

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

  對於大部分的控件,使用上述的方法就可以搞定了,如果你發現使用“withText”或“withContentDescription”都無法定位到元素的時候,谷歌建議你可以給開發提一個可訪問性的bug了。

  下面這種情況,出現了很多同樣的數字,但是它旁邊有可以識別出的唯一元素,這個時候我們就也可以使用hasSibling來進行篩選:

  

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

   另外說兩個常用的menu,如果是 overflow menu也就是下面這種情況的下:

  

  需要使用:

openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());

  如果是下面這樣的:

  

  使用:

openContextualActionModeOverflowMenu();

  注意:如果目標視圖在AdapterView(例如ListView,GridView,Spinner)中,onView方法可能無法正常工作,這個時候需要用到onData方法。 

  

 

定位元素onData


   假設一個Spinner的控件,我們要點擊“Americano”,我們使用默認的Adaptor,它的字段默認是String的,因此當我們要進行點擊的時候,就可以使用如下方法: 

onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());

  假設是一個Listview,我們需要點擊Listview中第二個item的按鈕,那么我們需要這樣寫:

onData(Matchers.allOf())
        .inAdapterView(withId(R.id.photo_gridview)) // listview的id
        .atPosition(1)                              // 所在位置
        .onChildView(withId(R.id.imageview_photo))  // item中子控件id
        .perform(click());

 

 

執行操作


  當你獲取到了目標的控件后,就可以使用perform來執行操作了。例如一個點擊操作:

onView(...).perform(click());

  也可以通過一個命令執行多個操作:

// 輸入hello,並且點擊
onView(...).perform(typeText("Hello"), click());

  當你操作的對象如果是ScrollView,在執行其他操作(例如click、typeText)之前,必須確保當前的控件是出現在當前的可視范圍的,若沒有可以使用scrollTo的方法:

onView(...).perform(scrollTo(), click());

  如果可視范圍已經出現了該元素,scrollTo將不起作用,因此當你的屏幕分辨率大或小的時候,都可以放心安全地使用它。

 

 

校驗


  使用check方法可以斷言當前選擇的界面, 常用的斷言是matches,它使用ViewMatcher來斷言當前選定視圖的狀態。例如,要檢查視圖中是否包含“Hello”這個字符串:

onView(...).check(matches(withText("Hello")));

  千萬不要使用下面這樣的方法做斷言,谷歌是不推薦這樣的:

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));

  所以當我們需要斷言一個指定的內容是否在AdapterView當中的時候,我們需要做一些特殊的處理。做法就是找到AdapterView,然后訪問它的內部元素,這里不適用onData,而是使用onView和我們自己寫的matcher來進行處理。我們自定義一個matcher叫withAdaptedData,實現如下:

private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
  return new TypeSafeMatcher<View>() {

    @Override
    public void describeTo(Description description) {
      description.appendText("with class name: ");
      dataMatcher.describeTo(description);
    }

    @Override
    public boolean matchesSafely(View view) {
      if (!(view instanceof AdapterView)) {
        return false;
      }
      @SuppressWarnings("rawtypes")
      Adapter adapter = ((AdapterView) view).getAdapter();
      for (int i = 0; i < adapter.getCount(); i++) {
        if (dataMatcher.matches(adapter.getItem(i))) {
          return true;
        }
      }
      return false;
    }
  };
}

  然后我們就可以使用它來進行斷言了:

// 當list當中是否存在一個bryan的item,就斷言失敗
onView(withId(R.id.list)).check(matches(not(withAdaptedData(withItemContent("bryan")))));

  因為里面還用到了一個withItemContent,我們也需要實現它:

public static Matcher<Object> withItemContent(final Matcher<String> itemTextMatcher) {
    // use preconditions to fail fast when a test is creating an invalid matcher.
    checkNotNull(itemTextMatcher);
    return new BoundedMatcher<Object, Map>(Map.class) {
      @Override
      public boolean matchesSafely(Map map) {
        return hasEntry(equalTo("STR"), itemTextMatcher).matches(map);
      }
      @Override
      public void describeTo(Description description) {
        description.appendText("with item content: ");
        itemTextMatcher.describeTo(description);
      }
    };
  }

  所以當我們要斷言時,如果遇到了一些沒有實現的內容,就需要我們重寫matcher了。

 

 

參考圖


  下面這幅圖,我們在寫代碼中可以快速查看,這里面包含了大部分我們經常用的API:

 

  細心的人應該能看到圖中有intent相關的內容,這部分內容我目前沒有用到,因此也沒有深入的了解。有興趣的可以自己看看。

 

  參考鏈接:https://google.github.io/android-testing-support-library/docs/espresso/intents/index.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM