本文翻译自:Testing UI for a Single App
水平有限自己感觉很多地方表达的并不到位,但找不到更好的表达方式,如果您觉着有更好的表达方式,帮助我改进!
单个App的UI测试
测试单独的App的用户交互可以帮助你确保用户在和你的App交互时,不会遭遇一些非预期的结果或者遇到非常糟糕的用户体。如果你需要确保你的App的UI功能的正确性,你就应该养成创建用户交互界面测试的习惯。
Android Testing Support Library提供的Espresso测试框架,提供了编写UI测试的Api,用来模拟和单个目标App的交互。Espresso测试可以运行在Android2.2和更高的设备上。使用Espresso的一个关键好处是它提供了自动的同步测试动作,对你正在进行测试中的App。Espresso可以监测主线程什么时候是空闲的,所以可以在合适的时间运行你的测试命令,来提高你的测试的可靠性。这种能力也可以帮助你更灵活的安排你的工作,比如代码正在测试的时候,你可以去睡觉。
Espresso测试框架基于instrumentation Api并且依靠AndroidJUnitRunner测试器来工作的。
设置Espresso
在你使用Espresso构建你的UI测试之前,确保你的工程按照开始你的测试中描述的那样配置了你的测试代码目录和工程依赖。
在Android App module的build.gradle文件中,你必须设置Espresso 库的依赖:
dependencies {
...
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
}
在你的测试设备上关闭动画——测试设备上的动画打开的话可能会导致非预期的结果或者可能导致你的测试失败。在设置中的开发者选项中关闭如下动画:
- 窗口缩放动画
- 过渡缩放动画
- 动画程序时长缩放
如果你想设置你的工程使用Espresso的特性而不是核心的Api,参见 resource
创建一个Espresso测试类
为了创建一个Espresso测试,遵从如下编程规则来创建一个Java类:
- 寻找一个你想测试的activity的UI组件(比如,App的登录按钮)通过调用onView()方法,或者 onData()方法来为AdapterView的控制。
- 模拟和UI组件之间的用户交互,比如调用ViewInteraction.perform() 或者 DataInteraction.perform()方法 同时将用户的具体动作传入(比如,对登录按钮的点击)。为了让最UI组件的多个操作按照顺序执行,使用逗号分隔符在你的方法参数里将他们串联起来。
- 按需重复上述动作,来模拟用户和目标App的多个activity进行交互。
- 使用ViewAssertions方法来检查在进行了上述一系列的用户交互后,UI是否有正确的状态或者是表现。
这几个步骤将会在下面的章节中更详细的描述。
下面的代码片段向你展示了你的测试类如何进行这些最基本的工作流程:
onView(withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher
.perform(click()) // click() is a ViewAction
.check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
Espresso和ActivityTestRule一起来使用
下面的章节描述了如何按照JUnit 4的风格创建一个Espresso测试以及使用ActivityTestRule来减少你需要写的一大部分的模板代码。通过使用ActivityTestRule,测试框架加载正在测试中的App的activity,在每个被@Test、@Before注解的方法之前. 框架负责在测试完成后关闭这个activity 和调用所有被@After注解的方法来运行。
package com.example.android.testing.espresso.BasicSample;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
...
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {
private String mStringToBetyped;
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
MainActivity.class);
@Before
public void initValidString() {
// Specify a valid string.
mStringToBetyped = "Espresso";
}
@Test
public void changeText_sameActivity() {
// Type text and then press the button.
onView(withId(R.id.editTextUserInput))
.perform(typeText(mStringToBetyped), closeSoftKeyboard());
onView(withId(R.id.changeTextBt)).perform(click());
// Check that the text was changed.
onView(withId(R.id.textToBeChanged))
.check(matches(withText(mStringToBetyped)));
}
}
Espresso和ActivityInstrumentationTestCase2一起来使用
下面的章节告诉你如何迁移到Espresso,如果你已经有了ActivityInstrumentationTestCase2类的子类,并且你不想再用JUnit4来重写他们。
注意:对于新的UI测试,我们强烈建议你用JUnit4的风格来编写你的测试,并且使用ActivityTestRule类代替ActivityInstrumentationTestCase2.
如果你是用ActivityInstrumentationTestCase2的子类来创建你的Espresso 测试类,你必须在你的测试类中注入一个Instrumentation实例。为了你的Espresso测试借助AndroidJUnitRunner来运行,这步是必须的。
为了做到这些,调用injectInstrumentation()方法同时将InstrumentationRegistry.getInstrumentation()的结果作为参数传递它,如同下面的例子展示的那样:
import android.support.test.InstrumentationRegistry;
public class MyEspressoTest
extends ActivityInstrumentationTestCase2<MyActivity> {
private MyActivity mActivity;
public MyEspressoTest() {
super(MyActivity.class);
}
@Before
public void setUp() throws Exception {
super.setUp();
injectInstrumentation(InstrumentationRegistry.getInstrumentation());
mActivity = getActivity();
}
...
}
注意:前提,InstrumentationTestRunner 将注入 Instrumentation 实例,但是这个测试运行器已经过期了。
获取UI组件
在Espresso可以和你正在测试中的App交互之前,你必须首先具体声明对应的UI组件或者view。Espresso支持使用Hamcrest matchers来具体声明你App中的views 和 adapters.
为了查找对应的view,调用onView()方法同时传递一个view匹配器,他具体声明了你的目标view. 更加详细的内容将在Specifying a View Matcher中讲到。onView()方法返回一个ViewInteraction对象它允许你测试和目标view之间的交互。然而,调用onView()方法有可能也不会有效如果你想找到AdapterView中的View的话。这种情况下,按照Locating a view in an AdapterView中描述的来做。
注意,onView()方法不会检查你声明的view是否合法。取而代之的是,Espresso使用提供的匹配器,仅仅查找当前view层级下是否有该view。如果没有相匹配的View,这个方法将会抛出一个异常NoMatchingViewException.
下面的代码片段展示了你该如何来编写一个测试,它获取一个EditText字段,输入一个字符串,关闭虚拟键盘,然后接着对button进行一次点击。
public void testChangeText_sameActivity() {
// Type text and then press the button.
onView(withId(R.id.editTextUserInput))
.perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
onView(withId(R.id.changeTextButton)).perform(click());
// Check that the text was changed.
...
}
具体声明一个View匹配器
你可以使用这些方法具体声明一个view 匹配器:
调用ViewMatchers中的方法。比如,查找一个展现了指定字符串的view,你可以这样调用方法:
onView(withText("Sign-in"));
同样的你可以调用windId() 来提供该view的资源ID,就如同下面代码所述的那样:
onView(withId(R.id.button_signin));
非常不幸的是Android的资源id不是唯一的。如果你尝试去匹配的资源id 被多个view使用,Espresso将 会抛出一个AmbiguousViewMatcherException.
使用Hamcrest Matchers类。你可以使用allOf()方法来结合多个匹配器,比如 containsString() 和 instanceOf()方法。这些方法允许你更加严格的过滤匹配结果,就如同下面的例子中描述的那样。
onView(allOf(withId(R.id.button_signin), withText("Sign-in")));
你可以使用关键字 not 来过滤那些和matcher 不匹配的View,如同下面的例子所示:
onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))));
为了在你的测试中使用这些方法,你需要导入org.hamcrest.matchers 包。为了学习更多关于 Hamcrest Matching ,查阅Hamcrest site.
为了提高你的Espresso测试表现,具体声明你需要寻找的目标view的最小的匹配信息。比如,一个View通过它的描述信息被唯一的定义,你不需要具体声明它,因为他已经被分配了一个具体的TextView实例。
找出在AdapterView中的一个View
在一个AdapterView的视图中,view在运行时被动态的添加进来。如果你想测试的view在AdapterView内部(比如ListView,GridView,或者是一个 Spinner),onView()方法可能不会起作用,因为可能只有一部分view被加载进当前的view层级中。
取而代之的是,调用onData()方法获取一个 DataInteraction对象来获取目标view的元素。Espresso负责加载目标view元素进入当前view层级。Espresso也关心滚动至目标元素,然后给元素设置进焦点。
注意:onData()方法不会检查你声明的item是否能匹配出一个view。Espresso仅仅寻找当前层级下的view。 如果没有找到相匹配的,该方法就会抛出一个NoMatchingViewException.
下面的代码片段向你展示额如何使用onData()方法和 Hamcrest 匹配来寻找一个可持续滚动的list,他包含了给出的string。在这个例子中,LongListActivity类包含了了一个String列表,通过SimpleAdapter暴露出来。
onData(allOf(is(instanceOf(Map.class)),
hasEntry(equalTo(LongListActivity.ROW_TEXT), is(str))));
执行actions
调用 ViewInteraction.preform() 或者 DataInteraction.preform()方法来模拟用户在UI组件之间的交互。你必须传递一个或者更多的ViewAction对象作为参数。Espresso 按照给定的顺序来发射他们,并且在主线程执行他们。
ViewActions类提供了一系列的帮助方法来声明常用的动作。你可以使用这些方法做为捷径而不是创建和配置独立的ViewAction对象。你可以如下声明这些动作:
- ViewActions.click(): 对一个View的点击。
- ViewActions.typeText(): 点击一个view 然后输入特定的字符串。
- ViewActions.scrollTo(): 滚动至指定的view. 目标view 必须是 ScrollView的子类,同时它的android:visibility属性必须是可见的。对于继承自AdapterView的view,(比如,ListView),onData()方法 会为你滚动。
- ViewActions.pressKey(): 执行一个按键操作,按照给定的keycode
- ViewActions.clearText(): 清除目标view的text
如果目标view在ScrollView的内部,执行ViewActions.scrollTo( )动作首先会将该view展现在屏幕上,在其他action操作之前。如果目标view已经在在屏幕上展现了,那么ViewActions.scrollTo()方法将不会生效。
验证结果
调用 ViewInteraction.check() 或者 DataInteraction.check() 方法来断言目标view的反馈是否和预期状态相匹配。你必须传递一个ViewAssertion对象作为参数。如果断言失败,Espresso将会抛出一个 AssertionFailedError.
ViewAssertions类提供了一系列的帮助方法来声明常用的断言。你可以使用的断言包括:
- doesNotExist:断言在当前的view层级中没有符合标准的view相匹配。
- matches: 断言指定的view在当前view层级中存在同时他的状态和给定的Hamcrest匹配器相匹配。
- selectedDescendentsMatch : 断言指定的父view的子view存在,并且他们的状态和给定的Hamcrest 匹配器相匹配。
下面的代码片段向你展示了在UI上展示的text 和之前在EditText中输入的text相匹配。
public void testChangeText_sameActivity() {
// Type text and then press the button.
...
// Check that the text was changed.
onView(withId(R.id.textToBeChanged))
.check(matches(withText(STRING_TO_BE_TYPED)));
}
在具体设备上或者是模拟器上运行Espresso测试。
你可以运行Espresso测试从Android Studio或者是命令行上。确保在你的工程中声明了 AndroidJUnitRunner作为默认的instrumentation runner 。按照开始你的测试中描述的步骤运行你的Espresso测试。