【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

posted @ 2016-10-25 17:40  胖喵~  Views(3390)  Comments(0Edit  收藏  举报