Espresso 的学习整理 (一)

saii · March 07, 2016 · Last by saii replied at March 20, 2016 · Last modified by admin 恒温 · 1845 hits
本帖已被设为精华帖!

简介


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 #2 · March 07, 2016 作者

#1楼 @neyo 谢谢。

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

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

不错,赞一个

感谢分享

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

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

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

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up