单元测试框架:Mockito

什么是 Mock

mock 的中文译为: 仿制的,模拟的,虚假的。对于测试框架来说,即构造出一个模拟/虚假的对象,使我们的测试能顺利进行下去。

为什么要使用 Mock 测试框架

单元测试是为了验证我们的代码运行正确性,我们注重的是代码的流程以及结果的正确与否。而对比于真实运行代码,可能其中有一些外部依赖的构建步骤相对麻烦,如果我们还是按照真实代码的构建规则构造出外部依赖,会大大增加单元测试的工作,代码也会参杂太多非测试部分的内容,测试用例显得复杂难懂,而采用 Mock 框架,我们可以虚拟出一个外部依赖,只注重代码的流程与结果,真正地实现测试目的。

Mock 测试框架好处

  • 可以很简单的虚拟出一个复杂对象(比如虚拟出一个接口的实现类)
  • 可以配置 mock 对象的行为
  • 可以使测试用例只注重测试流程与结果
  • 减少外部类或系统带来的副作用
    ······

Mockito 简介

Most popular Mocking framework for unit tests written in Java

Mockito 是当前最流行的单元测试 mock 框架。

使用

在 Module 的 build.gradle中添加如下内容:

dependencies {
    //Mockito for unit tests
    testImplementation "org.mockito:mockito-core:2.+"
    //Mockito for Android tests
    androidTestImplementation 'org.mockito:mockito-android:2.+'
}

这里稍微解释下:
mockito-core用于本地单元测试,其测试代码路径位于:module-name/src/test/java/
mockiot-android用于仪器测试,即需要运行android设备进行测试,其测试代码路径位于: module-name/src/androidTest/java/
更多详细信息,请查看官网:测试应用

ps:
mockito-core最新版本可以在 Maven 中查询:mockito-core
mockito-android最新版本可以在 Maven 中查询:mockito-android

示例

  1. 普通单元测试使用 mockitomockito-core),路径:module-name/src/test/java/

这里摘用官网的Demo:

now you can verify interactions -- 检验调对象相关行为是否被调用

import static org.mockito.Mockito.*;

// mock creation
List mockedList = mock(List.class);

// using mock object - it does not throw any "unexpected interaction" exception
mockedList.add("one"); //调用了add("one")行为
mockedList.clear();//调用了clear()行为

// selective, explicit, highly readable verification
verify(mockedList).add("one");//检验add("one")是否已被调用
verify(mockedList).clear();//检验clear()是否已被调用

这里 mock 了一个 List(这里只是为了用作 Demo 示例,通常对于 List 这种简单的类对象创建而言,直接 new 一个真实的对象即可,无需进行 mock),verify 会检验对象是否在前面已经执行了相关行为,这里mockedListverify之前已经执行了add("one")clear()行为,所以verify会通过。

stub method calls -- 配置/方法行为

// you can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);

// stubbing appears before the actual execution
when(mockedList.get(0)).thenReturn("first");

// the following prints "first"
System.out.println(mockedList.get(0));

// the following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

这里对几个比较重要的点进行解析:

  • when(mockedList.get(0)).thenReturn("first")
    这句话 Mockito 会解析为:当对象 mockedList调用方法方法get并且参数为0时,返回结果为"first",这相当于定制了我们 mock 对象的行为结果(mock LinkedList对象为mockedList,指定其行为get(0)返回结果为"first")。
  • mockedList.get(999)
    由于mockedList没有指定get(999)的行为,所以其结果为null。因为 Mockito 的底层原理是使用 cglib 动态生成一个代理类对象,因此,mock 出来的对象其实质就是一个代理,该代理在没有配置/指定行为的情况下,默认返回空值:
  1. Android单元测试使用 mockitomockito-android),路径:module-name/src/androidTest/java/,该测试需运行安卓真机/模拟器。

上面的 Demo 使用的是静态方法mock模拟出一个实例,我们还可以通过注解@Mock也模拟出一个实例:

    @Mock
    private Intent mIntent;

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void mockAndroid(){
        Intent intent = mockIntent();
        assertThat(intent.getAction()).isEqualTo("com.yn.test.mockito");
        assertThat(intent.getStringExtra("Name")).isEqualTo("Whyn");
    }

    private Intent mockIntent(){
        when(mIntent.getAction()).thenReturn("com.yn.test.mockito");
        when(mIntent.getStringExtra("Name")).thenReturn("Whyn");
        return mIntent;
    }

对于 @Mock, @Spy, @InjectMocks等注解的成员变量的初始化到目前为止有2种方法:

现在,正如我们上面的测试用例所示,对于@Mock等注解的成员变量的初始化又多了一种方法:MockitoRule
规则MockitoRule会自动帮我们调用MockitoAnnotations.initMocks(this)去实例化出注解的成员变量,我们就无需手动进行初始化了。

ps:上面要注意的一个点就是,断言使用的是开源库:Truth

更多Demo,请查看:Mockito

Mockito 一些重要方法简介

  1. @Mock/mock:实例化虚拟对象
//You can mock concrete classes, not just interfaces
LinkedList mockedList = mock(LinkedList.class);

//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

//following prints "first"
System.out.println(mockedList.get(0));

//following throws runtime exception
System.out.println(mockedList.get(1));

//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

//Although it is possible to verify a stubbed invocation, usually it's just redundant
//If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
//If your code doesn't care what get(0) returns, then it should not be stubbed. Not convinced? See here.
verify(mockedList).get(0);
  • 对于所有方法,mock对象默认返回null,原始类型/原始类型包装类默认值,或者空集合。比如对于int/Integer类型,则返回0,对于boolean/Bollean则返回false

  • 行为配置(stub)是可以被复写的:比如通常的对象行为是具有一定的配置,但是测试方法可以复写这个行为。请谨记行为复写可能表明潜在的行为太多了。

  • 一旦配置了行为,方法总是会返回配置值,无论该方法被调用了多少次。

  • 最后一次行为配置是更加重要的 - 当你为一个带有相同参数的相同方法配置了很多次,最后一次起作用。

  1. Argument matchers - 参数匹配
    Mockito 通过参数对象的equals()方法来验证参数是否一致,当需要更多的灵活性时,可以使用参数匹配器:

 //stubbing using built-in anyInt() argument matcher
 when(mockedList.get(anyInt())).thenReturn("element");

 //stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
 when(mockedList.contains(argThat(isValid()))).thenReturn("element");

 //following prints "element"
 System.out.println(mockedList.get(999));

 //you can also verify using an argument matcher
 verify(mockedList).get(anyInt());

 //argument matchers can also be written as Java 8 Lambdas
 verify(mockedList).add(argThat(someString -> someString.length() > 5));

参数匹配器允许更加灵活的验证和行为配置。更多内置匹配器和自定义参数匹配器例子请参考:ArgumentMatchersMockitoHamcrest

注意:如果使用了参数匹配器,那么所有的参数都需要提供一个参数匹配器:

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//above is correct - eq() is also an argument matcher

verify(mock).someMethod(anyInt(), anyString(), "third argument");
//above is incorrect - exception will be thrown because third argument is given without an argument matcher.

类似anyObject()eq()这类匹配器并不返回匹配数值。他们内部记录一个匹配器堆栈并返回一个空值(通常为null)。这个实现是为了匹配 java 编译器的静态类型安全,这样做的后果就是你不能在检验/配置方法外使用anyObject()eq()等方法。

  1. Verifying exact number of invocations / at least x / never - 校验次数

 //using mock
 mockedList.add("once");

 mockedList.add("twice");
 mockedList.add("twice");

 mockedList.add("three times");
 mockedList.add("three times");
 mockedList.add("three times");

 //following two verifications work exactly the same - times(1) is used by default
 verify(mockedList).add("once");
 verify(mockedList, times(1)).add("once");

 //exact number of invocations verification
 verify(mockedList, times(2)).add("twice");
 verify(mockedList, times(3)).add("three times");

 //verification using never(). never() is an alias to times(0)
 verify(mockedList, never()).add("never happened");

 //verification using atLeast()/atMost()
 verify(mockedList, atLeastOnce()).add("three times");
 verify(mockedList, atLeast(2)).add("three times");
 verify(mockedList, atMost(5)).add("three times");

校验次数方法常用的有如下几个:

Method Meaning
times(n) 次数为n,默认为1(times(1)
never() 次数为0,相当于times(0)
atLeast(n) 最少n次
atLeastOnce 最少一次
atMost(n) 最多n次

4. 配置返回类型为void的方法抛出异常:doThrow

doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:
mockedList.clear();

5. Verification in order - 按顺序校验
有时对于一些行为,有先后顺序之分,所以,当我们在校验时,就需要考虑这个行为的先后顺序:

// A. Single mock whose methods must be invoked in a particular order
 List singleMock = mock(List.class);

 //using a single mock
 singleMock.add("was added first");
 singleMock.add("was added second");

 //create an inOrder verifier for a single mock
 InOrder inOrder = inOrder(singleMock);

 //following will make sure that add is first called with "was added first, then with "was added second"
 inOrder.verify(singleMock).add("was added first");
 inOrder.verify(singleMock).add("was added second");

//----------------------------------------------------------------

 // B. Multiple mocks that must be used in a particular order
 List firstMock = mock(List.class);
 List secondMock = mock(List.class);

 //using mocks
 firstMock.add("was called first");
 secondMock.add("was called second");

 //create inOrder object passing any mocks that need to be verified in order
 InOrder inOrder = inOrder(firstMock, secondMock);

 //following will make sure that firstMock was called before secondMock
 inOrder.verify(firstMock).add("was called first");
 inOrder.verify(secondMock).add("was called second");

 // Oh, and A + B can be mixed together at will
  1. Stubbing consecutive calls (iterator-style stubbing) - 存根连续调用
    对于同一个方法,如果我们想让其在多次调用中分别返回不同的数值,那么就可以使用存根连续调用:

 when(mock.someMethod("some arg"))
   .thenThrow(new RuntimeException())
   .thenReturn("foo");

 //First call: throws runtime exception:
 mock.someMethod("some arg");

 //Second call: prints "foo"
 System.out.println(mock.someMethod("some arg"));

 //Any consecutive call: prints "foo" as well (last stubbing wins).
 System.out.println(mock.someMethod("some arg"));

也可以使用下面更简洁的存根连续调用方法:

 when(mock.someMethod("some arg"))
   .thenReturn("one", "two", "three");

注意:存根连续调用要求必须使用链式调用,如果使用的是同个方法的多个存根配置,那么只有最后一个起作用(覆盖前面的存根配置)。

//All mock.someMethod("some arg") calls will return "two"
 when(mock.someMethod("some arg"))
   .thenReturn("one")
 when(mock.someMethod("some arg"))
   .thenReturn("two")
  1. doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod() family of methods
    对于返回类型为void的方法,存根要求使用另一种形式的when(Object)函数,因为编译器要求括号内不能存在void方法。
    例如,存根一个返回类型为void的方法,要求调用时抛出一个异常:
   doThrow(new RuntimeException()).when(mockedList).clear();

   //following throws RuntimeException:
   mockedList.clear();
  1. Spying on real objects - 监视真实对象
    我们前面使用的都是 mock 出来一个对象,这样,当我们没有配置/存根其具体行为的话,结果就会返回空类型。而使用特务对象(spy),那么对于我们没有存根的行为,它会调用原来对象的方法。可以把spy想象成 局部mock
   List list = new LinkedList();
   List spy = spy(list);

   //optionally, you can stub out some methods:
   when(spy.size()).thenReturn(100);

   //using the spy calls *real* methods
   spy.add("one");
   spy.add("two");

   //prints "one" - the first element of a list
   System.out.println(spy.get(0));

   //size() method was stubbed - 100 is printed
   System.out.println(spy.size());

   //optionally, you can verify
   verify(spy).add("one");
   verify(spy).add("two");

注意:由于spy局部mock,所以有时候使用when(Object)时,无法做到存根作用,此时,就可以考虑使用doReturn|Answer|Throw()这类方法进行存根:

   List list = new LinkedList();
   List spy = spy(list);

   //Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
   when(spy.get(0)).thenReturn("foo");

   //You have to use doReturn() for stubbing
   doReturn("foo").when(spy).get(0);

spy并不是对真实对象的代理,相反的,它对传递过来的真实对象进行复制,所以,对于任何真实对象的操作,spy对象并不会感知到,同理,对spy对象的任何操作,也不会影响到真实对象。

当然,如果你想使用mock对象进行 局部mock,通过doCallRealMethod|thenCallRealMethod方法也是可以的:

//you can enable partial mock capabilities selectively on mocks:
    Foo mock = mock(Foo.class);
    //Be sure the real implementation is 'safe'.
    //If real implementation throws exceptions or depends on specific state of the object then you're in trouble.
    when(mock.someMethod()).thenCallRealMethod();
  1. Aliases for behavior driven development (Since 1.8.0) - 测试驱动开发
    以行为驱动开发格式使用 //given //when //then 注释为测试用法基石编写测试用例,这正是 Mockito 官方编写测试用例方法,强烈建议使用这种方式进行测试编写。
 import static org.mockito.BDDMockito.*;

 Seller seller = mock(Seller.class);
 Shop shop = new Shop(seller);

 public void shouldBuyBread() throws Exception {
   //given
   given(seller.askForBread()).willReturn(new Bread());

   //when
   Goods goods = shop.buyBread();

   //then
   assertThat(goods, containBread());
 }
  1. Custom verification failure message (Since 2.1.0) - 自定义错误校验输出信息
 // will print a custom message on verification failure
 verify(mock, description("This will print on failure")).someMethod();

 // will work with any verification mode
 verify(mock, times(2).description("someMethod should be called twice")).someMethod();

11.@InjectMock -- 构造器,方法,成员变量依赖注入
使用@InjectMock注解时,Mockito 会为类构造器,方法或者成员变量依据它们的类型进行自动mock

public class InjectMockTest {
    @Mock
    private User user;
    @Mock
    private ArticleDatabase database;
    @InjectMocks
    private ArticleManager manager;
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testInjectMock() {
        // calls addListener with an instance of ArticleListener
        manager.initialize();
        // validate that addListener was called
        verify(database).addListener(any(ArticleListener.class));
    }

    public static class ArticleManager {
        private User user;
        private ArticleDatabase database;

        public ArticleManager(User user, ArticleDatabase database) {
            super();
            this.user = user;
            this.database = database;
        }

        public void initialize() {
            database.addListener(new ArticleListener());
        }
    }

    public static class User {
    }

    public static class ArticleListener {
    }

    public static class ArticleDatabase {
        public void addListener(ArticleListener listener) {
        }
    }
}

成员变量manager类型为ArticleManager,其上注解了@InjectMocks,所以要mockmanagerMockito 会自动mockArticleManager所需的构造参数(即userdatabase),最终mock得到一个ArticleManager,赋值给manager

  1. ArgumentCaptor -- 参数捕捉
    ArgumentCaptor允许我们在verify的时候获取方法参数内容,这使得我们能在测试过程中能对调用方法参数进行捕捉并测试。
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    @Captor
    private ArgumentCaptor<List<String>> captor;
    @Test
    public void testArgumentCaptor(){
        List<String> asList = Arrays.asList("someElement_test", "someElement");
        final List<String> mockedList = mock(List.class);
        mockedList.addAll(asList);

        verify(mockedList).addAll(captor.capture());//when verify,you can capture the arguments of the calling method
        final List<String> capturedArgument = captor.getValue();
        assertThat(capturedArgument, hasItem("someElement"));
    }

Mocktio 限制

  • 不能mock静态方法
  • 不能mock构造器
  • 不能mock方法equals(), hashCode()
    更多限制点,请查看:FAQ

PowerMockito

针对 Mocktio 无法mock静态方法等限制,使用 PowerMockito 则可以解决这一限制。

  1. Dowanload:
 testImplementation "org.mockito:mockito-core:2.8.47"
 testImplementation 'org.powermock:powermock-api-mockito2:1.7.1'
 testImplementation 'org.powermock:powermock-module-junit4:1.7.1'

详情请查看:Mockito 2 Maven

注: 上面只所以不使用最新的 Mockito 版本,是因为根据 PowerMockito 官方文档,目前 PowerMockito 版本对应支持的 Mockito 版本如下图所示:

Supported versions

因此,这里就选择 Mockito 2.8.47(2.8.x最新版本)

  1. 示例
//static method
public class Static {
    public static String firstStaticMethod() {
        return "I am a firstStatic method";
    }

    public static String secondStaticMethod() {
        return "I am a secondStatic method";
    }
}
//

参考

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

推荐阅读更多精彩内容