Espresso 的学习整理 (一)

saii · 2016年03月07日 · 最后由 saii 回复于 2016年03月20日 · 3693 次阅读
本帖已被设为精华帖!

简介


Espresso 是在 2013 年的 GTAC 上首次提出,目的是让开发人员能够快速地写出简洁,美观,可靠的 Android UI 测试。

在你的项目中添加 Espresso


其实说再多还是先动手实操才是最实际的。

  1. 首先保证你的 Android Support Repository 已经成功安装

这里写图片描述

  1. 在你程序的 build.gradle 文件中添加依赖
// Force usage of support annotations in the test app, since it is internally used by the runner module.
    androidTestCompile 'com.android.support:support-annotations:23.1.1'
    androidTestCompile 'com.android.support.test:runner:0.4.1'
    androidTestCompile 'com.android.support.test:rules:0.4.1'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
    androidTestCompile 'com.android.support:support-annotations:23.1.1'
    androidTestCompile 'com.android.support.test:runner:0.4.1'
    androidTestCompile 'com.android.support.test:rules:0.4.1'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'

由于 annotations 的依赖关系可能会出现冲突,所以要制定它的版本

  1. 在默认配置中指定 test instrumentation runner
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

Espresso 的主要组建


Espresso 由 3 个主要的组件构成。

这些组件是:

  • ViewMatchers - 在当前的 view 层级中定位一个 view
  • ViewActions - 跟你的 view 交互
  • ViewAssertions - 给你的 view 设置断言

更简单的可以用下面的短语来表述它们:

  • ViewMatchers – “ 找 某些东西 “
  • ViewActions – “ 做 某些事情 “
  • ViewAssertions – “ 检查 某些东西 “

下面是使用 Espresso 的例子,你会看到那些主要的组件将会在哪里出现使用。

这里写图片描述

简单入门

我们先看看下面这张图

这里写图片描述

找某些东西

Espresso 提供了 onView() 方法用来查看 UI 上指定的元素,该方法如下:

public static ViewInteraction onView(final Matcher<View> viewMatcher) {}

该方法接受一个 Matcher 类型的参数并且返回一个 ViewInteraction 的对象,这里的 matcher 是使用了一个 hamcrest 的测试框架,它提供了一套通用的匹配符 matcher,并且我们还可以去自定义 matcher。具体可以去这里了解下。其实 onView 就可以理解成通过一个指定的条件在当前的 UI 界面查找符合条件的 View,并且将该 View 返回回来。

例如:

onView(withId(id));

我现在要找一个 R.id 为指定 id 的控件,那么我就从我的这个 id 出发,先生成一个查找匹配条件:withId(id)。然后把这个条件传给 onView() 方法:onView(withId(id)),让 onView() 方法根据这个条件找到我们想要的那个控件!

Espresso 提供了很多的方法,具体可以看上边图片中的 view matchers。

还是上面的例子,有时候 id 这个值在多个视图中都被使用了,这个时候当你再试图去使用这个 id 的时候,系统就会报一个 AmbiguousViewMatcherException 的异常了。这个异常信息会以文本的形式提供当前视图的层次结构。让你查看到你查找的 id 值并不是唯一的

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****

所以你可能需要找到一些唯一的属性来查找对应的控件,如:

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

或者:

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

做某些事情

Espresso 提供了如下方法来对相应的元素做操作:

public ViewInteraction perform(final ViewAction... viewActions) {}

perform 是 ViewInteraction 类中的方法, 还记得 onView() 方法的返回值么?yes,正是一个 ViewInteraction 对象。因此,我们可以在 onView() 方法找到的元素上直接调用 perform() 方法进行一系列操作:

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

当然你还可以执行多个操作在一个 perform 中类似于:

onView(...).perform(typeText("Hello"), click());

检查某些东西

接下来我们需要检查一下这些操作的结果是否符合我们的预期了。
Espresso 提供了一个 check() 方法用来检测结果:

public ViewInteraction check(final ViewAssertion viewAssert) {}

该方法接收了一个 ViewAssertion 的入参,该入参的作用就是检查结果是否符合我们的预期。一般来说,我们可以调用如下的方法来自定义一个 ViewAssertion:

public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {}

例如:检查一个视图的文本是否有 hello

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

注意:不要把你要” assertions“的内容放到 onView 的参数中,相反的你应该清楚的在你的检查处说明你需要检查哪些内容如:

onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));

常用的操作

获取文本内容

其实从刚才的内容的 API 中实际上没有相应的方法,很明显我们还是需要依赖 Android 原生的 getText 方法。那如何才能够得到 TextView 的对象,从而获取到文本内容来,其实我们从第一章就应该知道。

ViewActions 代表的就是做某些事。

所以我们就从这个入手,我们重写来 ViewAction 方法,看看下面的代码吧

public String getText(final Matcher<View> matcher) {
        final String[] text = {null};
        onView(matcher).perform(new ViewAction() {
            //识别所操作的对象类型
            @Override
            public Matcher<View> getConstraints() {
                return isAssignableFrom(TextView.class);
            }
            //视图操作的一个描述
            @Override
            public String getDescription() {
                return "getting text from a TextView";
            }
            //实际的一个操作,在之类我们就可以获取到操作的对象了。
            @Override
            public void perform(UiController uiController, View view) {
                TextView textView = (TextView)view;
                text[0] = textView.getText().toString();
            }
        });
        return text[0];
    }

再来就是我们的测试代码了


//获取文本的内容输入到文本输入框中
String tv = getText(withId(R.id.textToBeChanged));
onView(withId(R.id.editTextUserInput)).perform(typeText(tv));

以上的 demo 使用的均为android-testing 的测试代码。

中文的输入

由于 Espresso 也是用于做 UI 自动化测试的,所以我们难免要拿它来跟 UiAutomator 进行比较了。 使用过 UiAutomator 的都应该知道,它不支持中文的输入,为此 Appium 引入了专门的 appium 的输入法来解决这个问题,那我们来试试看 Espresso 是否能够支持中文呢。

中文的支持

onView(withId(R.id.editTextUserInput)).perform(typeText("你好"));

代码很简单就是对一个文本框进行内容的输入。我们看看运行的结果

java.lang.RuntimeException: Failed to get key events for string 你好 (i.e. current IME does not understand how to translate the string into key events). As a workaround, you can use replaceText action to set the text directly in the EditText field.
at android.support.test.espresso.base.UiControllerImpl.injectString(UiControllerImpl.java:265)
at android.support.test.espresso.action.TypeTextAction.perform(TypeTextAction.java:105)
.....

好吧失败来,不开森了,难道 Espresso 也跟 UiAutomator 一样不支持中文的输入吗? 等等!! 我们好好的阅读下错误的信息

As a workaround, you can use replaceText action to set the text directly in the EditText field

原来这里已经给出了措施,直接使用 replaceText 好的,我们重新试试看。

@Override
    onView(withId(R.id.editTextUserInput)).perform(replaceText("你好"));

!!! 真的运行通过了,成功的输入了中文了。

不过问题还没完, 我们得搞懂 typeText 与 replaceText 的区别到底在哪里呢

查看源代码我们终于发现两者的区别了,typeText 与 replaceText 方法分别是实例化了一个 TypeTextAction 以及 ReplaceTextAction 的对象,并且这两个类都实现了 ViewAction 的接口。我们首先看看 ReplaceTextAction 的 perform 方法的实现

@Override
  public void perform(UiController uiController, View view) {
    ((EditText) view).setText(stringToBeSet);
  }

原来如此一个直接是使用 EditText 的 setText 方法。

那下来我们再看看 TypeTextAction 的 perform 的方法实现吧。

@Override
  public void perform(UiController uiController, View view) {
    // No-op if string is empty.
    if (stringToBeTyped.length() == 0) {
      Log.w(TAG, "Supplied string is empty resulting in no-op (nothing is typed).");
      return;
    }

    if (tapToFocus) {
      // Perform a click.
      new GeneralClickAction(Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER)
          .perform(uiController, view);
      uiController.loopMainThreadUntilIdle();
    }

    try {
      if (!uiController.injectString(stringToBeTyped)) {
        Log.e(TAG, "Failed to type text: " + stringToBeTyped);
        throw new PerformException.Builder()
          .withActionDescription(this.getDescription())
          .withViewDescription(HumanReadables.describe(view))
          .withCause(new RuntimeException("Failed to type text: " + stringToBeTyped))
          .build();
      }
    } catch (InjectEventSecurityException e) {
      Log.e(TAG, "Failed to type text: " + stringToBeTyped);
      throw new PerformException.Builder()
        .withActionDescription(this.getDescription())
        .withViewDescription(HumanReadables.describe(view))
        .withCause(e)
        .build();
    }
  }

typeTextAction 相比来说就复杂了很多了,我们重点看到

uiController.injectString(stringToBeTyped)
这里才是真正的注入字符串的地方,我们再细细一瞧

@Override
  public boolean injectString(String str) throws InjectEventSecurityException {
    checkNotNull(str);
    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
    initialize();

    // No-op if string is empty.
    if (str.length() == 0) {
      Log.w(TAG, "Supplied string is empty resulting in no-op (nothing is typed).");
      return true;
    }

    boolean eventInjected = false;
    KeyCharacterMap keyCharacterMap = getKeyCharacterMap();

    // TODO(user): Investigate why not use (as suggested in javadoc of keyCharacterMap.getEvents):
    // http://developer.android.com/reference/android/view/KeyEvent.html#KeyEvent(long,
    // java.lang.String, int, int)
    KeyEvent[] events = keyCharacterMap.getEvents(str.toCharArray());
    if (events == null) {
     throw new RuntimeException(String.format(
         "Failed to get key events for string %s (i.e. current IME does not understand how to "
          + "translate the string into key events). As a workaround, you can use replaceText action"
          + " to set the text directly in the EditText field.", str));
    }

    Log.d(TAG, String.format("Injecting string: \"%s\"", str));

    for (KeyEvent event : events) {
      checkNotNull(event, String.format("Failed to get event for character (%c) with key code (%s)",
          event.getKeyCode(), event.getUnicodeChar()));

      eventInjected = false;
      for (int attempts = 0; !eventInjected && attempts < 4; attempts++) {
        attempts++;

        // We have to change the time of an event before injecting it because
        // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
        // time stamp and the system rejects too old events. Hence, it is
        // possible for an event to become stale before it is injected if it
        // takes too long to inject the preceding ones.
        event = KeyEvent.changeTimeRepeat(event, SystemClock.uptimeMillis(), 0);
        eventInjected = injectKeyEvent(event);
      }

      if (!eventInjected) {
        Log.e(TAG, String.format("Failed to inject event for character (%c) with key code (%s)",
            event.getUnicodeChar(), event.getKeyCode()));
        break;
      }
    }

    return eventInjected;
  }

其实看到这里我们大概就能明白了,typeText 是通过模拟事件注入的方式,它将传入的字符串转成字符数组,再分别获取到对应的 KeyEvent 后直接进行注入。

实际上我们在查看 typeText 以及 replaceText 的操作现象的时候就能够发现 typeText 的内容是一个个输进去的,但是 replaceText 是直接显示结果的,所以有时候你连操作都没看清,用例就已经跑完了。

参考文档

Android 自动化测试 - 从入门到入门(3)Espresso 入门
Espresso basics

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 9 条回复 时间 点赞

这个很棒,写得很详细啊

saii #8 · 2016年03月07日 Author

#1 楼 @neyo 谢谢。

谢谢分享,分享详细的说明。

最近正想研究这个来着,感谢!!

不错,赞一个

感谢分享

怎么没有源码的情况下,找到需要测试的 apk

怎么没有源码的情况下,找到需要测试的 apk

#8 楼 @michaelian 我之前 google 了很久,espresso 还是推荐有源码的情况下进行测试。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册