- Android Testing Support Library
- Espresso docs地址,包含官方测试例子
如何做UI测试呢?
大概的流程就是找到某个UI元素,做一些操作,再检查结果。
具体点来讲就是这3个步骤:
- 找到UI上测试所针对的元素;
- 给这个元素做一些操作;
- 操作之后出现了程序预期的结果。
举个例子,我们测试向EditText输入一段文字这个过程。
- 找到EditText;
- 向EditText输入字符串;
- EditText显示了我们输入的字符串。
下面,我们采用Espresso来进行简单的UI测试。
Espresso入门
它由三部分组成:
- ViewMachers: 在当前的 view 层级中定位一个 view,寻找用来测试的View
- ViewActions:跟界面上的View交互,发送交互事件
- ViewAssertions:对View设置断言,检验测试结果。
使用onView()方法找元素
该方法定义如下:
/**
* Creates a {@link ViewInteraction} for a given view. Note: the view has
* to be part of the view hierarchy. This may not be the case if it is rendered as part of
* an AdapterView (e.g. ListView). If this is the case, use Espresso.onData to load the view
* first.
* * @param viewMatcher used to select the view.
* * @see #onData(org.hamcrest.Matcher)
*/
// TODO change parameter to type to Matcher extends View> which currently causes Dagger issues
public static ViewInteraction onView(final Matcher viewMatcher) {
return BASE.plus(new ViewInteractionModule(viewMatcher)).viewInteraction();
}
这个方法接收一个Matcher
这里要注意这个方法的注释,它并不适用于AdapterView (e.g. ListView)的查找,我们可以使用另一个方法onData来加载。
这里的Matcher是使用了一个hamcrest的测试框架,它提供了一套通用的匹配符matcher,并且我们还可以去自定义matcher。
当我们在实现布局的时候,每个控件都会有一些特殊的属性来确定其唯一性,比如最常用的R.id。Matcher
public static Matcher withId(final int id) {}
该方法接收了一个int类型的参数,返回了一个Matcher
onView(withId(id));
就能在当前页面找到指定ID所对应的目标控件了。
Espresso提供了很多方法来让我们自定义我们的查找条件。比如我们可以通过withText()方法来寻找显示了指定文案的控件等等。具体支持的Matcher类型可以参考Espresso cheat sheet。
onView()方法在根据匹配条件进行查找时,它的目标是找到唯一的一个目标控件。如果我们制定的匹配条件有多个控件可以匹配(比如复用了layout的布局,或者显示相同文字的TextView等),该方法会抛出一个AmbiguousViewMatcherException异常,因此我们在构造匹配条件时,一定要确保能查找到的目标控件是唯一的。如果单一的匹配条件无法精确地匹配出来唯一的控件,我们可能还需要额外的匹配条件,此时可以用allOf()方法来进行复合匹配条件的构造:
//以下代码可以查找ID为tv_hello同时显示的文字内容为"Hello World!"的控件。
// 这里需要注意的是,为了保证自动化测试的效率,我们应尽可能减少匹配条件的数量。
onView(allOf(withId(R.id.tv_hello),withText("Hello World!")));
如果用一个匹配条件能够满足我们的需求,我们也就没有必要再用allOf()来构造复合匹配条件了。
使用onData()方法加载AdapaterView视图
AdapterView是一种通过Adapter来动态加载数据的界面元素。我们常用的ListView, GridView, Spinner等等都属于AdapterView。不同于我们之前提到的静态的控件,AdapterView在加载数据时,可能只有一部分显示在了屏幕上,对于没有显示在屏幕上的那部分数据,我们通过onView()是没有办法找到的。来看看onData方法的实现:
/**
* Creates an {@link DataInteraction} for a data object displayed by the application. Use this
* method to load (into the view hierarchy) items from AdapterView widgets (e.g. ListView).
*
* @param dataMatcher a matcher used to find the data object.
*/
public static DataInteraction onData(Matcher extends Object> dataMatcher) {
return new DataInteraction(dataMatcher);
}
使用perform()方法操作元素
perform方法定义如下:
public ViewInteraction perform(final ViewAction... viewActions) {}
该方法定义在ViewInteraction类里面。还记得onView()方法的返回值么?它是一个ViewInteraction对象。因此,我们可以在onView()方法找到的元素上直接调用perform()方法进行一系列操作:
onView(withId(R.id.tv_hello)).perform(click());
上面的代码对onView()查询到的元素做了一次点击的操作。
perform()方法的参数是变长参数,也就意味着,我们可以依次对某个元素(比如editText)做多个操作:
onView(withId(id)).perform(click(), replaceText(text), closeSoftKeyboard())
以上代码对目标元素editText依次做了点击、输入文本、关闭输入法键盘的操作。
使用check()方法来检查结果
该方法定义如下:
public ViewInteraction check(final ViewAssertion viewAssert) {}
该方法接收了一个ViewAssertion的参数,该参数的作用就是检查结果是否符合我们的预期。一般来说,我们可以调用下面的matches()方法来自定义一个ViewAssertion:
public static ViewAssertion matches(final Matcher super View> viewMatcher) {}
这个方法接收了一个匹配规则,然后根据这个规则为我们生成了一个ViewAssertion对象!还记得Matcher这个类型吧?是的,这就是onView()方法的入参!实际上他们是同一个类型,其使用方法也是完全一致的。
比如,我想检查一下指定id的TextView是否按照我的预期显示了一段text文本,那么我就可以这样写:
onView(withId(id)).check(matches(withText(text)))
ViewAssertion的支持也可以参照这个Espresso cheat sheet。
Espresso拓展
- 中文字符的输入
我们试着按照先前的做法
onView(withId(R.id.editText)).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:26
at android.support.test.espresso.action.TypeTextAction.perform(TypeTextAction.java:105).....
根据以上错误信息,我们需要使用replaceText来输入中文
onView(withId(R.id.editTex)).perform(replaceText("你好"));
结果通过了测试。
typeText与replaceText方法分别是实例化了一个TypeTextAction以及ReplaceTextAction的对象,并且这两个类都实现了ViewAction 的接口。
public static ViewAction typeText(String stringToBeTyped) {
return actionWithAssertions(new TypeTextAction(stringToBeTyped));
}
public static ViewAction replaceText(@Nonnull String stringToBeSet) {
return actionWithAssertions(new ReplaceTextAction(stringToBeSet));
}
我们首先看看ReplaceTextAction 的perform方法的实现
@Override
public void perform(UiController uiController, View view) {
((EditText) view).setText(stringToBeSet);
}
这里直接使用了EditText的setText方法。
再来看看TypeTextAction的perform方法,
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(); }}
可以看到,typeText是通过模拟事件注入的方式,它将传入的字符串转成字符数组,再分别获取到对应的KeyEvent后直接进行注入。因此,也就能理解它不支持中文输入。
我们在查看typeText以及replaceText的操作现象的时候,也能够发现 typeText的内容是一个个输进去的,但是replaceText是直接显示结果的。
新建工程
了解了espresso的相关知识之后,我们就可以开始一步步地展开测试了。首先建立新的工程,其MainActivity代码如下
package mumubin.espressodemo;import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;
import butterknife.BindView;
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_hello) TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
activity_main.xml文件配置如下
Espresso的引入
在build.gradle的dependencies中增加了如下依赖:
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'})
testCompile 'junit:junit:4.12'
在defaultConfig增加代码:
defaultConfig {
...
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
建一个Test Case
新建对应的测试文件
在app/src/androidTest/项目对应的packageName/此目录中新建一个类。目前用到的类的名字为:ExampleInstrumentedTest.java
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Rule
public ActivityTestRule mActivityRule = new ActivityTestRule<>(
MainActivity.class);
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("mumubin.espressodemo", appContext.getPackageName());
onView(withId(R.id.tv_hello)).check(matches(withText("Hello World!")));
//这段代码主要是测试Hello world!这段文字是否显示到了界面上。
onView(withText("Hello World!")).check(matches(isDisplayed()));
//以下代码可以查找ID为tv_hello同时显示的文字内容为"Hello World!"的控件。
// 这里需要注意的是,为了保证自动化测试的效率,我们应尽可能减少匹配条件的数量。
onView(allOf(withId(R.id.tv_hello),withText("Hello World!")));
}
}
运行测试
测试机的设置
关闭测试机三个选项,具体操作步骤如下:
On your device, under Settings->Developer options disable the following 3 settings
- Window animation scale
- Transition animation scale
- Animator duration scale
运行测试文件
选择ExampleInstrumentedTest .java文件,右键点击,在弹出的菜单中选Run 'ExampleInstrumentedTest '。运行后,观察结果,分析结果。至此,我们完成了一个基本的UI测试。