Android自动化测试

Instrumentation介绍

  • Instrumentation是个什么东西?
  • Instrumentation测试
  • Instrumentation原理介绍

一、Instrumentation是个什么东西?

  • Instrumentation是位于android.app包下,与Activity处于同一级目录,它是Android系统中一系列控制方法的集合(俗称hook)。这些hook可以在正常的生命周期之外控制Android控件的运行,也可控制Android如何加载应用程序。
  • 可以说Instrumentation就是AndroidSDK在Junit上的扩展,提供了AndroidTestCase类及其系列子类,其中最重要的一个类是ActivityInstrumentationTestCase2。
  • Instrumentation将在任何应用程序启动之前初始化,通过它来检测系统与应用程序之间的所有的交互。
  • 通过Instrumentation启动时,测试程序与被测应用运行在同一进程的不同线程中。
  • application package 与 test package 处于同一个进程。
  • Android测试框架基于JUnit,因此可以直接使用JUnit来测试一些与Android平台不相关的类,或者使用Android的JUnit扩展来测试Android组件;
  • Android JUnit扩展提供了对Android特定组件(Activity,Service等)的测试支持,也就是对mock的支持;

二、如何使用Instrumentation进行单元测试?

  • 环境配置:

在AndroidMainfast.xml文件中加入一下代码:

<instrumentation  
    android:name="android.test.InstrumentationTestRunner"

    android:targetPackage="com.xxx.xxx" />

注意:targetPackage是被测应用的包名

或者在App module gradle中增加以下下配置:

defaultConfig {
    testInstrumentationRunner "android.support.test.InstrumentationTestRunner"
}
  • 使用Instrumentation进行自动化测试步骤:

1、 启动应用

Intent intent = new Intent();
intent.setClassName(“packageName”,”className”);
intent,setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MainActivity activity = (MainActivity)getInstrumentation().startActivitySync(intent);

2、编辑界面:

Button button = activity.findViewById(R.id.btn);

3、结果提交:

button.performClick();

4、界面跳转:

ActivityMonitor monitor = getInstrumentation().addMonitor(String cls, ActivityResult result, boolean block);
ChangeActivity ca = (ChangeActivity)getInstrumentation().waitForMonitor(monitor);

5、验证结果:

assertTrue(ca != null);

三、Instrumentation源码分析?

1)、测试框架如何运行?

想要了解测试框架运行过程需从InstrumentationTestRunner类入手,此类为Android测试框架启动入口。
整个运行流程如下:

  • 创建线程:通过InstrumentationTestRunner创建Instrumentation专属线程InstrumentationThread。
  • 获取线程:通过InstrumentationTestRunner获取线程
  • 执行用例:通过AndroidTestRunner对象循环执行测试用例

通过Instrumentation框架可以直接操作activity的生命周期函数。
测试activity的基本测试类为InstrumentationTestCase,它提供了Instrumentation接口给TestCase的子类,为了支持activity的测试,InstrumentationtTestCase提供了下面功能:

  • 生命周期的控制:使用Instrumentation可以启动,暂停,终止被测试的activity。
  • Dependency Injection(依赖注入):Instrumentation允许创建一些mock对象,如Context,Application来帮助测试activity,从而帮助你控制测试环境并和实际应用的其他部分隔离开来。
  • 用户界面交互:你可以使用Instrumentation向UI发送按键触摸事件。

以下是由TestCase派生而来的测试类:

  • ActivityInstrumationTestCase2 : 通常用于多个activity的功能测试,它使用正常的系统框架来运行activity(使用应用程序本身),并使用正常系统Context(非Mock)来测试activity的功能,允许你创建一些mock Intent用来测试activity的响应,这种case不允许使用mock的Context和Application对象测试,也就是说你必须使用和应用程序实际运行的环境测试。

  • ActivityUnitTestCase :通常用来测试单独的activity,在启动被测试的activity之前,你可以Inject一个假的Context或是Application,使用这个mock的Context中一个隔离环境中运行被测试activity。通常用于activity的单元测试,而不和Android系统进行交互。

  • SingleLaunchActivityTestCase:用于测试单个activity,和ActivityUnitTestCase不同的是,它只运行setUp和tearDown一次,而不是在运行testCase中每个test Method前后运行setUp和tearDown,它可以保证运行多个测试之间fixture不会被重置,从而可以用来测试一些有关联的方法。

说明:以上为Instrumentation单元测试框架的内容,通过配置InstrumentationTestRunner启动器来执行自动化测试脚本,But在最新SDK中与InstrumentationTestRunner相关的所有类都被废弃了,Google推荐使用最新的测试框架来编写测试代码。(AndroidJUnitRunner + Espresso + UIAutomater)

AndroidJUnitRunner

AndroidJUnitRunner是一个可以用来运行JUnit 3和JUnit 4样式的测试类的Test Runner,并且同时支持Espresso和UI Automator。这是对于之前的InstrumentationTestRunner的一个升级,如果你去查看Gradle文档中对于Testing配置的说明,会发现推荐的Test Runner为AndroidJUnitRunner。InstrumentationTestRunner只支持JUnit 3样式的测试用例,而我们在写Android测试用例时应该尽可能使用JUnit 4样式来实现。

相对于Junit 3,JUnit 4有如下改进:

  • 测试类不需要再继承junit.framework.TestCase类;

  • 测试方法名不再需要以test开头;

  • 可以使用类似@Test,@Before,@After等注解来管理自己的测试方法;

  • 增加了一些Assert方法;

  • 支持对assert方法的static导入。

下面来看一个例子。如下的代码段采用了JUnit 4风格进行编写,并且调用了Espresso的API来进行了一些测试:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityInstrumentationTest {

    @Rule
    public ActivityTestRule mActivityRule = new ActivityTestRule<>(
    MainActivity.class);

    @Test
    public void sayHello(){
        onView(withText("Say hello!")).perform(click());
        onView(withId(R.id.textView)).check(matches(withText("Hello, World!")));
    }
}

从以上代码可以看到,JUnit 4支持使用如下注解来管理整个测试用例:

  • @Before: 标识在运行测试方法之前运行的代码。可以支持同一个Class中有多个@Before,但是这些方法的执行顺序是随机的。该注解替代了JUnit 3中的setUp()方法。

  • @After: 标识在运行测试方法结束之后运行的代码。可以在其中做一些释放资源的操作。该注解替代了JUnit 3中的tearDown()方法。

  • @Test: 标识一个测试方法。一个测试类中可以有多个测试方法,每个测试方法需要用一个@Test注解来标识。

  • @Rule: 简单来说,是为各个测试方法提供一些支持。具体来说,比如我需要测试一个Activity,那么我可以在@Rule注解下面采用一个ActivityTestRule,该类提供了对相应Activity的功能测试的支持。该类可以在@Before和@Test标识的方法执行之前确保将Activity运行起来,并且在所有@Test和@After方法执行结束之后将Activity杀死。在整个测试期间,每个测试方法都可以直接对相应Activity进行修改和访问。

  • @BeforeClass: 为测试类标识一个static方法,在测试之前只执行一次。

  • @AfterClass: 为测试类标识一个static方法,在所有测试方法结束之后只执行一次。

  • @Test(timeout=<milliseconds>): 为测试方法设定超时时间。

Espresso详解

Espresso是一个新工具,相对于其他工具,API更加精确。并且规模更小、更简洁并且容易学习。它最初是2013年GTAC大会上推出的,目标是让开发者写出更简洁的针对APP的UI测试代码。

优点:

  • 代码快速上手
  • 容易扩展
  • 无需考虑复杂的多线程
  • 有Google做靠山

缺点:

  • 不支持跨应用的UI测试

Espresso 的主要组件:

onView(ViewMatchers).perform(ViewActions).check(ViewAssertions)

  • Espresso – 与视图(views)交互的入口,并暴露了一些视图(views)无关的API(例如回退按钮)。
  • ViewMatchers – 实现匹配器的一组对象。允许可以通过多次的onView方法,在层次图中找到目标视图(views)。
  • ViewActions – 对视图触发动作(例如点击)。
  • ViewAssertions – 用于插入测试关键点的一组断言,可用于判断某视图(view)的状态。

可以看出,与其他框架相比,Espresso代码集成度更高,功能分块更加集中:onView用于定位视图,perform用于产生事件,check用于检测checkpoint。

Espresso环境搭建

  1. 在Android Studio中新建一个Project;
  2. 修改Project中App/build.gradle脚本(Android studio2.2默认集成了Espresso) <br />

主要修改3处:

  • 在defaultConfig内增加
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
  • 增加packagingOptions,避免编译时候Liscens的冲突;
  • 在dependencies中增加Espresso相关的引用;
defaultConfig {
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
packagingOptions {
    exclude 'LICENSE.txt' }
}

dependencies {
    testCompile 'junit:junit:4.12'
    androidTestCompile ('com.android.support.test:runner:0.5'){
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test:rules:0.5') {
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test.espresso:espresso-core:2.2.2'){
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test.espresso:espresso-idling-resource:2.2.2'){
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test.espresso:espresso-intents:2.2.2'){
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2.2') {
        exclude group: 'com.android.support',module: 'support-annotations'
        exclude group: 'com.android.support', module: 'appcompat'
        exclude group: 'com.android.support', module: 'appcompat-v7'
        exclude group: 'com.android.support', module: 'design'
        exclude group: 'com.android.support', module: 'support-v4'
        exclude module: 'recyclerview-v7'
    }
}

说明:

espresso-core:espresso的基础库

espresso-idling-resource:异步任务相关的库

espresso-intents:提供对intent的支持库

espresso-contrib:提供对特定组件(如:recycleView等)的支持库

espresso-web:提供对webView测试的支持库

Espresso API 鼓励测试者以用户会怎样与应用交互的方式进行思考来定位 UI 元素并与它们交互。同时,框架不允许直接使用应用的活动和视图,因为在非 UI 线程持有此类对象并对它们操作是造成测试花屏的主要原因。因此,你不会在 Espresso API 中看到诸如 getView 或 getCurrentActivity 等方法。但你仍然可以通过实现 ViewAction 和 ViewAssertion 来对视图进行安全操作。

例如:

onView(withText("test")).check(matches(isDisplayed())).perform(click());

查找显示文本为Test所对应的View显示在界面上,并点击该View

使用 onView 查找视图

多数情况下,onView 方法使用 hamcrest 匹配器以期望在当前视图结构里匹配一个(唯一的)视图。该匹配器十分强大而且对用过 Mockito 或 JUnit 的人而言并不陌生。
想要查找的视图一般会有唯一的 ​R.id​ 值,使用简单的 ​withId​ 匹配器可以缩小搜索范围。然而,当你在测试开发阶段,无法确定 ​R.id值是合理的​。例如,指定的视图可能没有 R.id​ 值或该值不唯一。这将使一般的 instrumentation 测试变得脆弱而复杂,因为通用的获取视图方式(通过 findViewById())已经不适用了。因此,你可能需要获取持有视图的私有对象 Activity 或 Fragment,或者找到一个已知其 ​R.id​ 值的父容器,然后在其中定位到特定的视图。

Espresso 处理该问题的方式很干脆,它允许你使用已存在的或自定义的 ViewMatcher 来限定视图查找。
通过 ​R.id​ 查找视图:

onView(withId(R.id.my_view))

有时,​R.id​值会被多个视图共享,此时你需要找到一个能唯一确定的属性,你可以通过使用组合匹配器结合该属性来缩小搜索范围:

onView(allOf(withId(R.id.my_view), withText("Hello!")))

你也可以使用 ​not​ 反转匹配:

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

你可以在 ViewMatchers 类中查看 Espresso 提供的视图匹配器。

注意:在一个良态的应用中,所有用户可与之交互的视图都应该包含说明文字或有一个内容描述。如果你不能通过使用 ‘withText’ 或 ‘withContentDescripiton’ 来缩小 onView 的搜索范围,可以认为这是一个很明显的 bug。
如果目标视图在一个 ​AdapterView​(如 ​ListView​,​GridView​,​Spinner​)中,将不能使用 onView​ 方法,推荐使用 ​onData​ 方法。

在视图上执行操作

当为目标视图找到了合适的适配器后,你将可以通过 ​perform​ 方法在该视图上执行 ​ViewAction​。
例如,点击该视图:

onView(…).perform(click());

如果操作的视图在 ​ScrollView​(水平或垂直方向)中,需要考虑在对该视图执行操作(如 ​click()​ 或 ​typeText()​)之前通过 ​scrollTo()​ 方法使其处于显示状态。这样就保证了视图在执行其他操作之前是显示着的。

onView(…).perform(scrollTo(), click());

注意:如果视图已经是显示状态, ​scrollTo()​ 将不会对界面有影响。因此,当视图的可见性取决于屏幕的大小时(例如,同时在大屏和小屏上执行测试时),你可以安全的使用该方法。
你可以在 ViewActions 类中产看 Espresso 提供的视图操作。

检查一个视图是否满足断言

断言可以通过 ​check()​ 方法应用在当前选中的视图上。最常用的是 ​matches()​ 断言,它使用一个 ​ViewMatcher​ 来判断当前选中视图的状态。
例如,检查一个视图拥有 “Hello!”文本:

onView(…).check(matches(withText("Hello!")));

注意:不要将 “assertions” 作为 onView 的参数传入,而要在检查代码块中明确指定你检查的内容,
例如:如果你想要断言视图的内容是 “Hello!” ,以下做法是错误的:

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));
//use it
onView(withId(...)).check(matches(withText("Hello!")));

从另一个角度讲,如果你想要断言一个包含 “Hello!” 文本的视图是可见的(例如,在修改了该视图的可见性标志之后),这段代码是正确的。

注意:请留意断言一个视图没有显示和断言一个视图不在当前视图结构之间的区别。

在 ​AdapterView​ 控制器(ListView, GridView, ...)中使用 onData

AdapterView​ 是一个从适配器中动态加载数据的特殊控件。最常见的 ​AdapterView​ 是 ListView​。与像 ​LinearLayout​ 这样的静态控件相反,在当前视图结构中,可能只加载了 ​AdapterView​ 子控件的一部分, 简单的 ​onview()​ 搜索不能找到当前没有被加载的视图。Espresso 通过提供单独的 onData()​ 切入点处理此问题,它可以在操作适配器中有该问题的条目或该条目的子项之前将其加载(使其获取焦点)。

注意:
你可能不会对初始状态就显示在屏幕上的适配器条目执行 ​onData()​ 加载操作,因为它们已经被加载了。然而,一直使用 ​onData()​ 会更安全。

使用 onData 编写一个简单的测试

SimpleActivity​ 包含一个 ​Spinner​ ,该 Spinner​ 中有几个条目——代表咖啡类型的字符串。当选中其中一个条目时,​TextView​ 内容会变成 ​“One %s a day!”​,其中 %s 代表选中的条目。此测试的目标是打开 ​Spinner​,选中一个条目然后验证 ​TextView​ 中包含该条目。由于 ​Spinner​ 类基于 ​AdapterView​,建议使用 ​onData()​ 而不是 ​onView()​ 来匹配条目。

  1. 点击 Spinner 打开条目选择框
onView(withId(R.id.spinner_simple)).perform(click());
  1. 点击 “Americano” 条目
    为了条目可供选择,Spinner 用它的内容创建了一个 ​ListView​。该 ListView 可能会很长,而且它的元素不会出现在视图结构中。通过使用 ​onData()​ 我们强制将想要得到的元素加入到视图结构中。Spinner 中的元素是字符串,我们想要匹配的条目是字符串类型并且值是 “Americano”。
onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());
  1. 验证 TextView​ 包含 “Americano” 字符串
onView(withId(R.id.spinnertext_simple).check(matches(withText(containsString("Americano"))));

自定义ListView匹配器事例:

public static Matcher<? super Object> withContactTitle(final String title) {
        return new BoundedMatcher(Contact.class) {

            @Override
            public void describeTo(Description description) {
                description.appendText("with id:"+title);
            }

            @Override
            protected boolean matchesSafely(Object item) {
                if(item instanceof Contact){
                    return title.equals(((Contact)item).getFullName());
                }
                return false;
            }
        };
    }

测试Menu菜单

分两种情况:1、菜单按钮显示在title上,2、菜单隐藏在pop中

1、菜单显示在titlebar中的测试方法:

onView(allOf(withId(R.id.action_create),withContentDescription("创建")))
        .check(matches(isDisplayed())).perform(click());
onView(withText("创建")).check(matches(isDisplayed()));
onView(allOf(withId(R.id.action_search),withContentDescription("搜索")))
        .check(matches(isDisplayed())).perform(click());
onView(withText("搜索")).check(matches(isDisplayed()));

说明:不论菜单在titleBar上显示icon,还是显示文字,菜单布局中都必须包含title,用作菜单描述,否则没法定位对应的菜单项。
当菜单只显示icon时,请使用withContentDescription(“title”)定位对应的菜单,withText(“title")不生效,原因是菜单中压根没有设置title
详情查看ActionMenuItemView类

2、 菜单隐藏在pop中:
首先需要打开pop

//打开menu
openContextualActionModeOverflowMenu();
onView(allOf(withId(R.id.title),withText("创建"))).check(matches(isDisplayed())).perform(click());

注明:首先需要打开menu菜单框,title为菜单文本对应的TextView的id,此处固定为title,详情请查看ListMenuItemView类

针对RecycleView测试方法

RecyclerView 是一个像 ListView、GridVIew 那样呈现数据集合的 UI 组件,实际上它的目的是要替换掉这两个组件。从测试的角度上来看我们感兴趣的就是 RecyclerView 不是一个 AdapterView,这意味着你不能使用 onData() 去跟你的 list items 交互。

幸运的是,有一个叫 RecyclerViewActions 的类提供了简单的 API 给我们操作 RecyclerView。RecyclerViewActions 是 espresso-contrib库的一部分,这个库的依赖可以在 build.gradle 中添加:

androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.0');

这个时候就需要引用到一个 RecyclerViewActions ,RecyclerViewActions就是为了针对RecyclerView才出来的。 我们主要还是看看如何进行测试吧。

  • 点击RecyclerView列表中第1个item
onView(withId(R.id.pull_refresh_list)).perform(RecyclerViewActions.actionOnItemAtPosition(1,click()));
  • 点击带有 “Effective Java ” 字符串的item
onView(withId(R.id.pull_refresh_list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(taskName)),click()));

这里的hasDescendant 指代的是对应的item的后代中有包含对应文本的内容的。不过使用这个需要小心 因为很有可能会出现两个同样内容的。

  • actionOnHolderItem 的用法
@Test
public void testItemSelect() {
    onView(withId(R.id.pull_refresh_list))
            .perform(RecyclerViewActions.actionOnHolderItem(
                    new CustomViewHolderMatcher(hasDescendant(withText("Effective Java "))), click()));

}

private static class CustomViewHolderMatcher extends TypeSafeMatcher<RecyclerView.ViewHolder> {
    private Matcher<View> itemMatcher = any(View.class);

    public CustomViewHolderMatcher() { }

    public CustomViewHolderMatcher(Matcher<View> itemMatcher) {
        this.itemMatcher = itemMatcher;
    }

    @Override
    public boolean matchesSafely(RecyclerView.ViewHolder viewHolder) {
        return TaskListAdapter.ViewHolder.class.isAssignableFrom(viewHolder.getClass())
                && itemMatcher.matches(viewHolder.itemView);
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("is assignable from CustomViewHolder");
    }
}

<br />

Espress线程同步问题

使用 registerIdlingResource 与自定义资源同步

Espresso 的核心是它可以与待测应用无缝同步测试操作的能力。默认情况下,Espresso 会等待当前消息队列中的 UI 事件执行(默认是 AsyncTask)完毕再进行下一个测试操作。这应该能解决大部分应用与测试同步的问题。

然而,应用中有一些执行后台操作的对象(比如与网络服务交互)通过非标准方式实现;例如:直接创建和管理线程,以及使用自定义服务。

此种情况,我们建议你首先提出可测试性的概念,然后询问使用非标准后台操作是否必要。某些情况下,可能是由于对 Android 理解太少造成的,并且应用也会受益于重构(例如,将自定义创建的线程改为 AsyncTask)。然而,某些时候重构并不现实。庆幸的是 Espresso 仍然可以同步测试操作与你的自定义资源。

以下是我们需要完成的:

  • 实现 ​IdlingResource​ 接口并暴露给测试。
  • 通过在 setUp 中调用 ​Espresso.registerIdlingResource​ 注册一个或多个 IdlingResource 给 Espresso。

需要注意的是 IdlingResource 接口是在待测应用中实现的,所以你需要谨慎的添加依赖:

// IdlingResource is used in the app under test
compile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2'

例如:项目使用OkHttp库,此时需要定制针对OKHttp的同步测试,代码如下:

public final class OkHttp3IdlingResource implements IdlingResource {
    /**
     * Create a new {@link IdlingResource} from {@code client} as {@code name}. You must register
     * this instance using {@code Espresso.registerIdlingResources}.
     */
    @CheckResult @NonNull
    @SuppressWarnings("ConstantConditions") // Extra guards as a library.
    public static OkHttp3IdlingResource create(@NonNull String name, @NonNull OkHttpClient client) {
        if (name == null) throw new NullPointerException("name == null");
        if (client == null) throw new NullPointerException("client == null");
        return new OkHttp3IdlingResource(name, client.dispatcher());
    }

    private final String name;
    private final Dispatcher dispatcher;
    volatile ResourceCallback callback;

    private OkHttp3IdlingResource(String name, Dispatcher dispatcher) {
        this.name = name;
        this.dispatcher = dispatcher;
        dispatcher.setIdleCallback(new Runnable() {
            @Override
            public void run() {
                ResourceCallback callback = OkHttp3IdlingResource.this.callback;
                if (callback != null) {
                    callback.onTransitionToIdle();
                }
            }
        });
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public boolean isIdleNow() {
        return dispatcher.runningCallsCount() == 0;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback callback) {
        this.callback = callback;
    }
}

//测试代码中加入以下代码块
@Before
public void registerIdlingResource(){
    okHttp3IdlingResource = OkHttp3IdlingResource.create("okhttp",HaizhiRestClient.getHttpClient());
    Espresso.registerIdlingResources(okHttp3IdlingResource);
       
}
@After
public void unregisterIdlingResource(){
    Espresso.unregisterIdlingResources(okHttp3IdlingResource);
}

Espresso-Intents

Espresso-Intents 是 Espresso 的一个扩展,它使验证和存根待测应用向外发出的意图成为可能。它类似于 Mockito,但是针对的是 Android 的意图(专门针对Android的intent的扩展)。
Espresso-Intents 只兼容 Espresso 2.1+ 和 testing support library 0.3

在应用的build.gradle文件中添加以下配置

androidTestCompile ('com.android.support.test.espresso:espresso-intents:2.2.2'){
    exclude group: 'com.android.support', module: 'support-annotations'
}

IntentsTestRule

使用 Espresso-Intents 时,应当用 ​IntentsTestRule​ 替换 ​ActivityTestRule​。IntentsTestRule​ 使得在 UI 功能测试中使用 Espresso-Intents API 变得简单。该类是 ​ActivityTestRule​ 的扩展,它会在每一个被 ​@Test​ 注解的测试执行前初始化 Espresso-Intents,然后在测试执行完后释放 Espresso-Intents。被启动的 activity 会在每个测试执行完后被终止掉,此规则也适用于 ​ActivityTestRule​。

验证意图(Intent validation)

Espresso-Intents 会记录待测应用里所有尝试启动 Activity 意图。使用 intended API(与 ​Mockito.verify​ 类似)你可以断言特定的意图是否被发出。

验证外发意图的简单示例:

onView(withText("send")).perform(click());
//验证发送短信界面成功调用
Uri smsToUri = Uri.parse("smsto:10086");
intended(hasData(smsToUri));

意图存根(Intent stubbing)

使用 intending API(与 ​Mockito.when​ 类似)你可以为通过 startActivityForResult 启动的 Activity 提供一个响应结果(尤其是外部的 Activity,因为我们不能操作外部 activity 的用户界面,也不能控制 ​ActivityResult​ 返回给待测 Activity)。

使用意图存根的示例:

@Test
public void startSecondActivity(){
     Intent intent = new Intent();
     intent.putExtra("test","test");
     Instrumentation.ActivityResult result =
                new Instrumentation.ActivityResult(Activity.RESULT_OK,intent);
//        intending(anyIntent()).respondWith(result);
        //必须使用完整类名(包名+类名)
    intending(hasComponent(InstrumentationRegistry.getTargetContext().getPackageName()+"."+
                SecondActivity.class.getSimpleName())).respondWith(result);
        onView(withText("start")).perform(click());
        onView(withText("test")).check(matches(isDisplayed()));
}

说明:从MainActivity中点击按钮start跳转到SecondActivity中,返回时带回参数Test 显示到MainActivity的界面中。

意图匹配器(Intent Matchers)
intending​ 和 ​intended​ 方法用一个 hamcrest ​Matcher<Intent>​ 作为参数。 Hamcrest 是匹配器对象(也称为约束或断言)库。有以下选项:

  • 使用现有的意图匹配器:最简单的选择,绝大多数情况的首选。
  • 自己实现意图匹配器,最灵活的选择(参考 Hamcrest 教程 的 “Writing custom matchers” 章节)

以下是一个使用现有的意图匹配器验证意图的示例:

intended(allOf(
    hasAction(equalTo(Intent.ACTION_VIEW)),
    hasCategories(hasItem(equalTo(Intent.CATEGORY_BROWSABLE))),
    hasData(hasHost(equalTo("www.google.com"))),
    hasExtras(allOf(
        hasEntry(equalTo("key1"), equalTo("value1")),
        hasEntry(equalTo("key2"), equalTo("value2")))),
        toPackage("com.android.browser")));

Mock

mock的概念其实很简单,所谓的mock就是创建一个类的虚拟对象,在测试环境中用来替换掉真是的对象,以达到两个目的:

  • 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等。
  • 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作。

Mockito

要是用mock一般需要使用到mock框架,Mockito框架是java界使用最广泛的一个mock框架。

1、申明依赖

testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.+'
// 如果你要使用Mockito 用于 Android instrumentation tests,那么需要你添加以下三条依赖库
androidTestCompile 'org.mockito:mockito-core:1.+'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"

2、创建Mock对象

在 Mockito 中你可以使用 mock() 方法来创建一个模拟对象,也可以使用注解的方式 @Mock 来创建 ,这里推荐使用注解。需要注意的是,如果是使用注解方式,需要在使用前进行初始化。

使用注解方式创建有三种初始化方式:

1)、使用 MockitoAnnotations.initMocks(this) 方式

public class MockitoAnnotationsTest {
    @Mock
    AccountData accountData;
    @Before
    public void setupAccountData(){
        MockitoAnnotations.initMocks(this);
    }
    @Test
    public void testIsNotNull(){
        assertNotNull(accountData);
    }
}

2)、使用 @RunWith(MockitoJUnitRunner.class) 方式

@RunWith(MockitoJUnitRunner.class)
public class MockitoJUnitRunnerTest {
    @Mock
    AccountData accountData;

    @Test
    public void testIsNotNull() {
        assertNotNull(accountData);
    }
}

3)、使用 MockitoRule 方式

public class MockitoRuleTest {
    @Mock
    AccountData accountData;
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testIsNotNull(){
        assertNotNull(accountData);
    }
}

使用mock方式创建mock对象

public class MockitoTest {

    AccountData accountData;
    @Before
    public void setup(){
       accountData = Mockito.mock(AccountData.class);
    }
    @Test
    public void testIsNotNull(){
        assertNotNull(accountData);
    }
}

说明:

  • Mockito.mock() 并不是mock一整个类,而是根据传进去的一个类,mock出属于这个类的一个对象,并且返回这个 mock对象;而传进去的这个类本身并没有改变,用这个类new出来的对象也没有受到任何改变!
  • Mockito.verify() 的参数必须是mock对象,也就是说,Mockito只能验证mock对象的方法调用情况

Mockito的使用

1、验证方法的调用

前面我们讲了验证一个对象的某个method得到调用的方法:

Mockito.verify(accountData).isLogin();

这行代码验证的是, accountData 的 isLogin() 方法得到了 一次 调用。因为这行代码其实是:

Mockito.verify(accountData, Mockito.times(1)).isLogin();

因此,如果你想验证一个对象的某个方法得到了多次调用,只需要将次数传给 Mockito.times() 就好了。

Mockito.verify(accountData, Mockito.times(3)).isLogin(); //accountData的isLogin方法调用了3次。

对于调用次数的验证,除了可以验证固定的多少次,还可以验证最多,最少从来没有等等,方法分别是:

  • Mockito.verify() : 验证Mock对象的方法是否被调用。
  • Mockito.times() : 调用mock对象的次数
  • Mockito.atMost(count) , Mockito.atLeast(count) , Mockito.never() :最多次数,最少次数,永远调用。
  • Mockito.anyInt() , Mockito.anyLong() , Mockito.anyDouble()等等 : 参数设置-任意的Int类型,任意的Long类型。。。等。
  • anyCollection,anyCollectionOf(clazz), anyList(Map, set), anyListOf(clazz)

2、指定mock对象的某些方法的行为

到目前为止,我们介绍了mock的一大作用:验证方法调用。我们说mock主要有两大作用,第二个大作用是:指定某个方法的返回值,或者是执行特定的动作。
那么接下来,我们就来介绍mock的第二大作用,先介绍其中的第一点:指定mock对象的某个方法返回特定的值。

//希望 isLogin() 方法被调用时返回true,那么你可以这样写:

when(accountData.isLogin()).thenReturn(true);
//验证结果
boolean islogin = accountData.isLogin();
assertTrue(islogin);

//如果你希望 getUserName() 被调用返回Jack
when(accountData.getUserName()).thenReturn("Jack");
assertEquals("Jack",accountData.getUserName());

//如果你希望对 setUserName(String userName) 方法中参数进行测试
accountData.setUserName("haha");
verify(accountData).setUserName(Matchers.eq("haha"));

同样的,你可以用 any 系列方法来指定"无论传入任何参数值,都返回xxx":

//当调用accountData的setUserName1方法时,返回haha,无论参数是什么
when(accountData.setUserName1(anyString())).thenReturn("haha");

在这里,我们想进一步测试传给 accountData.login() 的 NetworkCallback 里面的代码,验证view得到了更新等等。在测试环境下,我们并不想依赖 accountData.login() 的真实逻辑,而是让 accountData.login 直接调用传入的 NetworkCallback 的 onSuccess 或 onFailure 方法。这种指定mock对象执行特定的动作的写法如下:

Mockito.doAnswer(desiredAnswer).when(mockObject).targetMethod(args);

传给 doAnswer() 的是一个 Answer 对象,我们想要执行什么样的动作,就在这里面实现。结合上面的

@Test
public void test_login(){
    activityTestRule.getActivity().setAccountData(accountData);
    doAnswer(new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {

            //这里可以获取传给login的参数
            Object[] args = invocation.getArguments();

            //callback是第三个参数
            NetWorkCallBack callBack = (NetWorkCallBack) args[2];

            callBack.onFailure(500,"Server error");
            return 500;
        }
    }).when(accountData).login(anyString(),anyString(),any(NetWorkCallBack.class));

    onView(withText("button")).perform(click());
    onView(withText("code=500:errorMsg=Server error")).check(matches(isDisplayed()));
}

Spy

前面我们讲了mock对象的两大功能,对于第二大功能: 指定方法的特定行为,不知道你会不会好奇,如果我不指定的话,它会怎么样呢?那么现在补充一下,如果不指定的话,一个mock对象的所有非void方法都将返回默认值:int、long类型方法将返回0,boolean方法将返回false,对象方法将返回null等等;而void方法将什么都不做。然而很多时候,你希望达到这样的效果:除非指定,否者调用这个对象的默认实现,同时又能拥有验证方法调用的功能。这正好是spy对象所能实现的效果。创建一个spy对象,以及spy对象的用法介绍如下:

//假设目标类的实现是这样的
public class PasswordValidator {
    public boolean verifyPassword(String password) {
        return "test_spy".equals(password);
    }
}

@RunWith(MockitoJUnitRunner.class)
public class SpyTest {
    @Spy
    PasswordValidator passwordValidator;

    @Test
    public void test_verifyPassword(){
        //跟创建mock类似,只不过调用的是spy方法,而不是mock方法。spy的用法
        Assert.assertTrue(passwordValidator.verifyPassword("test_spy"));
        Assert.assertFalse(passwordValidator.verifyPassword("test_spy1"));

        //spy对象的方法也可以指定特定的行为
        when(passwordValidator.verifyPassword(anyString())).thenReturn(true);

        Assert.assertTrue(passwordValidator.verifyPassword("test_spy12"));
        //同样的,可以验证spy对象的方法调用情况
        verify(passwordValidator).verifyPassword("test_spy12");
    }
}

总之,spy与mock的唯一区别就是默认行为不一样:spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值。

Android真机使用Mockito-1.10.19+Dexmaker-1.2在Mock继承抽象父类的子类时报告错误“java.lang.AbstractMethodError: abstract method not implemented”

在项目的 build.gradle中的声明如下:

androidTestCompile 'org.mockito:mockito-core:1.+'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"

这个问题只在 Dalvik虚拟机下面发生异常,相同的代码在 ART下面是完全正常的。
导致问题发生的原因是 Google提供的 dexmaker库存在 BUG导致的,而这个库,从 Maven Center上看,自从 2012年开始就没有提供过任何的更新了。
解决方法是不使用 Google提供的 dexmaker,而是使用com.crittercism.dexmaker修正过这个 BUG的版本。

androidTestCompile 'org.mockito:mockito-core:1.10.19'
androidTestCompile 'com.crittercism.dexmaker:dexmaker:1.4'
androidTestCompile "com.crittercism.dexmaker:dexmaker-dx:1.4"
androidTestCompile 'com.crittercism.dexmaker:dexmaker-mockito:1.4'

参考资料

官方文档
Espresso 自动化测试框架介绍
测试与基本规范
使用MVP+Dagger2+Espresso+Mockito构建的项目样板

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容