Android Google官方UI自动化测试的框架Espresso

  • Android Testing Support Library
  • Espresso docs地址,包含官方测试例子

如何做UI测试呢?

大概的流程就是找到某个UI元素,做一些操作,再检查结果
具体点来讲就是这3个步骤:

  1. 找到UI上测试所针对的元素;
  2. 给这个元素做一些操作;
  3. 操作之后出现了程序预期的结果。

举个例子,我们测试向EditText输入一段文字这个过程。

  1. 找到EditText;
  2. 向EditText输入字符串;
  3. 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 which currently causes Dagger issues
public static ViewInteraction onView(final Matcher viewMatcher) {  
    return BASE.plus(new ViewInteractionModule(viewMatcher)).viewInteraction();
}

这个方法接收一个Matcher类型的参数,返回一个ViewInteraction对象,其所做的事情就是根据Matcher所指定的条件,在当前UI页面上寻找符合条件的View,并且把相应的View返回出来。

这里要注意这个方法的注释,它并不适用于AdapterView (e.g. ListView)的查找,我们可以使用另一个方法onData来加载。

这里的Matcher是使用了一个hamcrest的测试框架,它提供了一套通用的匹配符matcher,并且我们还可以去自定义matcher。

当我们在实现布局的时候,每个控件都会有一些特殊的属性来确定其唯一性,比如最常用的R.id。Matcher支持通过控件的唯一ID来从当前页面上寻找目标控件,对应的方法为withId(),该方法定义如下:

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 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 viewMatcher) {}

这个方法接收了一个匹配规则,然后根据这个规则为我们生成了一个ViewAssertion对象!还记得Matcher这个类型吧?是的,这就是onView()方法的入参!实际上他们是同一个类型,其使用方法也是完全一致的。

比如,我想检查一下指定id的TextView是否按照我的预期显示了一段text文本,那么我就可以这样写:

onView(withId(id)).check(matches(withText(text)))

ViewAssertion的支持也可以参照这个Espresso cheat sheet。

Espresso拓展

  1. 中文字符的输入
    我们试着按照先前的做法
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测试。

你可能感兴趣的:(Android Google官方UI自动化测试的框架Espresso)