Espresso 是在 2013 年的 GTAC 上首次提出,目的是让开发人员能够快速地写出简洁,美观,可靠的 Android UI 测试。
其实说再多还是先动手实操才是最实际的。
// 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 的依赖关系可能会出现冲突,所以要制定它的版本
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
Espresso 的主要组建
Espresso 由 3 个主要的组件构成。
这些组件是:
更简单的可以用下面的短语来表述它们:
下面是使用 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 是直接显示结果的,所以有时候你连操作都没看清,用例就已经跑完了。