mockito简单用法指引

Mockito基础用法指引

什么是mock,为什么需要mock

mock

​ 在单元测试中,某个对象可能包含操作数据库的方法,如果我们不把这个操作数据库的方法屏蔽掉,本地跑此单元测试,是可以通过的。但是当我们将这些代码提交到git,由打包平台进行打包,而打包平台上并没有我们的数据库,此时就会出现打包异常的问题。因此在编写单元测试代码的时候,就应该把这些**“与外界交互”**的操作屏蔽掉。 我们把操作数据库的方法屏蔽掉,这样的方式就叫做打桩(stub)。

mock是对于对象来讲的,而打桩(stub)是对于方法来讲的。

引入mockito包

这是一个maven依赖,直接放到您项目的pom文件中即可,目前项目使用的是3.7.0版本的mockito。

<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.7.0</version>
    <scope>test</scope>
</dependency>

建立一个mock对象

mockito建立一个mock对象非常简单,只需要用mock()函数包裹你需要mock的类即可。

List mockedList = mock(List.class);

如果提示mock() 找不到的错误,请在引入包的地方加上下面这行代码

import static org.mockito.Mockito.*;

此时就建立了一个List的mock对象。List有很多操作,比如add() , remove(), size()等等操作,此时这些操作怎么样了呢?我们来进行一个简单的实验

    @Test
    public void mainTest() {
        List mockedList = mock(List.class);
        mockedList.add("foo");
        Assert.assertEquals(0,mockedList.size()); //pass
    }

可以看到,现在不能成功的往一个数组里面添加元素,这是为什么呢?

答案很简单,我们mock了一个对象,这个对象上的所有方法虽然都存在,但是都不再有效,被mock的那个对象完全被**“架空”**,函数虽然可以被调用但是却不会真正去执行,有的会返回一个默认值,比如0或者false。刚刚mockedList.size()就是这样,返回了0。

mockito的三大法宝

学习mockito基本用法,必然要学习mockito的语法,mockito有三个比较重要的句式分别是

when(...)then(...)
do(...)when(...)
verify()

这三个分别是用来做什么的呢?

when(…)then(…)

mockito有三大法宝,when(…)then(…)句式也有三大法宝,他们分别是

when()...thenReturn()
when(...)thenThrow(...)
when(...)thenAnswer(...)

从名字可以看出来他们之间的区别

thenReturn是当某种条件发生,然后返回一个

thenThrow是当某种条件发生,抛出一个异常

thenAnswer这个貌似不太好理解。现在暂时理解为他既可以返回一个值,也可以抛出异常

举几个例子

when(…)thenReturn(…)

示例代码如下

    @Test
    public void testThenReturn() {
        // 1.single return
        List mockedList = mock(List.class);
        when(mockedList.size()).thenReturn(2);
        when(mockedList.get(0)).thenReturn("foo");
        when(mockedList.get(1)).thenReturn("bar");

        Assert.assertEquals(2, mockedList.size());
        Assert.assertEquals("foo", mockedList.get(0));
        Assert.assertEquals("bar", mockedList.get(1));

        // 2.consecutive return
        LinkedList mockedLinkedList = mock(LinkedList.class);
        when(mockedLinkedList.poll()).thenReturn("foo").thenReturn("bar").thenReturn(null);

        Assert.assertEquals("foo", mockedLinkedList.poll());
        Assert.assertEquals("bar", mockedLinkedList.poll());
        Assert.assertNull(mockedLinkedList.poll());

        // forget any interactions & stubbing before
        reset(mockedLinkedList);

        // 3.consecutive return
        when(mockedLinkedList.poll()).thenReturn("new_foo", "new_bar", null);

        Assert.assertEquals("new_foo", mockedLinkedList.poll());
        Assert.assertEquals("new_bar", mockedLinkedList.poll());
        Assert.assertNull(mockedLinkedList.poll());
    }

when(…)thenReturn(…)共计有三种不同的写法

第一种是返回单个值的写法,此种比较常规,当某个条件发生,就返回某个值

第二种是返回多个值的写法,调用同一个函数每次返回的值不相同

第三次也是返回多个值的写法,在一个thenReturn里面直接返回多个值是第二种的改良版

问题:如果在第三个示例中,第四次调用mockedLinkedList.poll()会返回什么?

答:返回null

再问:这个返回的null有两种可能,第一种是因为多次返回值最后一个是null,所以之后的调用会一直返回null;第二种是因为第四次调用没有被定义,mockito对没有定义的桩返回值也是null。这个null是属于那种情景?

答:第一种,mockito如果返回多个值,多次调用超过原来定义,则之后的每次都是返回最后一个值。示例中的thenReturn(“new_foo”, “new_bar”, null)的null如果换成“apple”之后的多次调用则会一直返回apple。

when(…)thenThrow(…)

示例代码如下

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void testThenThrow() {
        List mockedList = mock(List.class);
        when(mockedList.size()).thenReturn(2);
        when(mockedList.get(0)).thenReturn("foo");
        when(mockedList.get(1)).thenReturn("bar");
        when(mockedList.get(2)).thenThrow(IndexOutOfBoundsException.class);
        when(mockedList.remove(2)).thenThrow(new IndexOutOfBoundsException("Max index is 1."));

        Assert.assertEquals(2, mockedList.size());
        Assert.assertEquals("foo", mockedList.get(0));
        Assert.assertEquals("bar", mockedList.get(1));

        thrown.expect(IndexOutOfBoundsException.class);
        mockedList.get(2);

        thrown.expect(IndexOutOfBoundsException.class);
        thrown.expectMessage("Max index is 1.");
        mockedList.remove(2);
    }

抛出异常和返回一个值类似,可以直接抛出一个异常类,也可以返回一个异常对象,对象也可以自定义异常信息

when(…)thenAnswer(…)

    @Test
    public void testThenAnswer() {
        List mockedList = mock(List.class);
        when(mockedList.size()).thenReturn(20);
        when(mockedList.get(anyInt())).thenAnswer((Answer<String>) invocation -> {
            Object[] args = invocation.getArguments();
            Integer index = (Integer) args[0];
            if (index == 0) {
                return "foo";
            } else if (index == 1) {
                return "bar";
            } else if (index < 10) {
                return "baz";
            } else if (index < 20) {
                return "qux";
            } else {
                throw new IndexOutOfBoundsException();
            }
        });

        Assert.assertEquals(20, mockedList.size());
        Assert.assertEquals("foo", mockedList.get(0));
        Assert.assertEquals("bar", mockedList.get(1));
        Assert.assertEquals("baz", mockedList.get(8));
        Assert.assertEquals("qux", mockedList.get(16));

        thrown.expect(IndexOutOfBoundsException.class);
        mockedList.get(20);
    }

从示例代码中可以看到when(…)thenAnswer(…)的作用有以下三种

1、他可以返回一个值也可以抛出异常

2、他可以获取到传入的参数

3、他可以对参数进行判断,决定下一步动作

好像get到的点就只是他既可以抛出异常也可以返回一个值。不着急,慢慢往下看,你就知道answer有大用处了

do(…)when(…)

和when()…then()一样,do(…)when(…)也有三个不同的函数,他们分别是

doReturn(…)when(…)

doThrow(…)when(…)

doAnswer()…when()…

是不是很像刚才的when(…)then(…)句式?在绝大多数情况下他们两个都是可以相互替换的,但是某些情况下他们就不可以互换,至于哪些情况,mock & spy一节会提到这些区别。

doReturn(…)when(…)

    @Test
    public void testDoReturn() {
        List mockedList = mock(List.class);
        doReturn(2).when(mockedList).size();
        doReturn("foo").when(mockedList).get(0);
        doReturn("bar").when(mockedList).get(1);

        Assert.assertEquals(2, mockedList.size());
        Assert.assertEquals("foo", mockedList.get(0));
        Assert.assertEquals("bar", mockedList.get(1));
    }

刚才的when(…)then(…)做个对比看一下

when(mockedList.size()).thenReturn(2);
        when(mockedList.get(0)).thenReturn("foo");
        when(mockedList.get(1)).thenReturn("bar");

do…when…的时候函数写在外面,例如doReturn(2).when(mockedList).size(); 而when…then…函数写在里面when(mockedList.get(0)).thenReturn(“foo”);除此之外他们都是相同的。因此绝大部分情况下他们是可以相互替换的,至于写哪种句式全看个人喜好。

doThrow(…)when(…)

@Test
    public void testDoThrow() {
        List mockedList = mock(List.class);
        doReturn(2).when(mockedList).size();
        doThrow(IndexOutOfBoundsException.class).when(mockedList).get(2);

        Assert.assertEquals(2, mockedList.size());
        thrown.expect(IndexOutOfBoundsException.class);
        mockedList.get(2);
    }

doThrow和thenThrow并无不同,区别也只是写法不同

doAnswer()…when()…

    @Test
    public void testDoAnswer() {
        Map mockedMap = mock(Map.class);
        doAnswer((Answer<Integer>) invocation -> {
            Object[] args = invocation.getArguments();
            String key = args[0].toString();
            return Integer.valueOf(key.split("-")[1]);
        }).when(mockedMap).get(anyString());

        Assert.assertEquals(98, mockedMap.get("Tom-98"));
        Assert.assertEquals(66, mockedMap.get("Nicolas-66"));
        Assert.assertEquals(100, mockedMap.get("Julia-100"));
    }

doAnswer()…when()…和when(…)thenAnswer(…)…也是一样的,不多赘述

参数匹配器

public void testDoReturn() {
        List mockedList = mock(List.class);
        doReturn(2).when(mockedList).size();
        doReturn("foo").when(mockedList).get(0);
        doReturn("bar").when(mockedList).get(1);


        Assert.assertEquals(2, mockedList.size());
        Assert.assertEquals("foo", mockedList.get(0));
        Assert.assertEquals("bar", mockedList.get(1));  
    }

doReturn(“foo”).when(mockedList).get(0);意思是获取当尝试获取数组里面下标为0的时候,返回一个“foo”字符串,如果这个桩是在for里面调用呢,可能从下标0访问到下标999。为了解决这个问题,mockito提供了一个叫参数匹配器的东西。

doReturn("bar").when(mockedList).get(antInt());

Assert.assertEquals("bar", mockedList.get(999));

只要传入的是int,那就返回一个“bar”,这样就决解了刚才提到的问题。常见的参数匹配器还有

 anyBoolean, anyByte, anyChar, anyString, notNull, startsWith //详情请看mockito官方文档

但是这个问题解决了,有衍生出来两个新问题

1、如果代码多次对这个函数打桩怎么办?

doReturn("foo").when(mockedList).get(0);
doReturn("bar").when(mockedList).get(antInt());

2、如果要只有传入指定字符串的时候才返回怎么办?

doReturn("bar").when(mockedList).get(antInt()"aaa");

对于第一种情况,后面的桩将会覆盖前面的桩,如果需要差异化处理此时就需要用到Answer,看上面的thenAnswer示例代码,你会豁然开朗

第二种情况如果使用了参数匹配器则都必须使用参数匹配器,指定字符串可以采用eq进行比较,上述代码可以改成doReturn(“bar”).when(mockedList).get(antInt(), eq(“aaa”));

verify

verify见名知意,mock了一个对象,这个对象最终要被调用的。如何了解这个对象的调用情况呢?此时就用到了verify()。verify可以验证mock对象被调用的情况,比如调用了多少次、多长时间内被调用了多少次、调用顺序等等,均可以用此来验证。

verify次数

@Test
    public void testVerify() {
        Iterator mockedIterator = mock(Iterator.class);
        when(mockedIterator.hasNext()).thenReturn(true, true, false);
        when(mockedIterator.next()).thenReturn("foo", "bar");

        while (mockedIterator.hasNext()) {
            mockedIterator.next();
        }

        // verify the 'hasNext()' method was called three times
        verify(mockedIterator, times(3)).hasNext();
        verify(mockedIterator, atLeast(2)).hasNext();

        // verify the 'next()' method was called two times
        verify(mockedIterator, times(2)).next();
        verify(mockedIterator, atLeastOnce()).next();
        verify(mockedIterator, atMost(3)).next();

        // verify the 'remove()' method was never called
        verify(mockedIterator, never()).remove();
    }

验证一个迭代器某个方法被调用情况,比如恰好被调用了多少次,最少被调用次数,最多被调用次数,以及验证从来没有被调用。

verify顺序

 @Test
    public void testInOrder() {
        Map<String, String> mockedMap = mock(Map.class);
        when(mockedMap.get("foo")).thenReturn("foo_value");

        mockedMap.put("foo", "foo_value");
        Assert.assertEquals("foo_value", mockedMap.get("foo"));

        // make sure that 'put' operation precedes 'get' operation
        InOrder inOrder = inOrder(mockedMap);
        inOrder.verify(mockedMap).put("foo", "foo_value");
        inOrder.verify(mockedMap).get("foo");
    }

verify也可以验证调用顺序,如果验证顺序不变 ,mockedMap.put(); mockedMap.get()交换顺序,则此单元测试也不能正常通过。

verify calls

@Test
    public void testCalls() {
        Iterator mockedIterator = mock(Iterator.class);
        when(mockedIterator.hasNext()).thenReturn(true, true, true, true, false);
        when(mockedIterator.next()).thenReturn("foo", "bar", "baz", "qux");

        while (mockedIterator.hasNext()) {
            mockedIterator.next();
        }

        // verify the 'hasNext()' method was called at least five times
        inOrder(mockedIterator).verify(mockedIterator, calls(5)).hasNext();

        // verify the 'next()' method was called at least four times
        inOrder(mockedIterator).verify(mockedIterator, calls(4)).next();
    }

calls也可以用来验证最少调用次数

verify 验证其他交互操作

以下代码可以进行验证有没有操作,在已知操作后面后没有其他的更多的操作。当然也可以在某次调用之前清空历史操作记录,方便下次在进行验证。

    @Test
    public void testVerifyNoInteractions() {
        List mockedList1 = mock(List.class);
        List mockedList2 = mock(List.class);

        // verify that no interactions happened on given mocks.
        verifyNoInteractions(mockedList1, mockedList2);
    }

    @Test
    public void testVerifyNoMoreInteractions() {
        List<String> mockedList1 = mock(List.class);
        mockedList1.add("foo");
        mockedList1.add("foo");

        List mockedList2 = mock(List.class);
        mockedList2.get(0);
        mockedList2.get(0);
        mockedList2.get(0);

        inOrder(mockedList1).verify(mockedList1, times(2)).add("foo");
        inOrder(mockedList2).verify(mockedList2, times(3)).get(0);

        // verify that no more interactions happened on given mocks.
        verifyNoMoreInteractions(mockedList1, mockedList2);
    }

    @Test
    public void testClearInvocations() {
        List<String> mockedList = mock(List.class);
        mockedList.add("foo");
        mockedList.add("foo");

        inOrder(mockedList).verify(mockedList, times(2)).add("foo");
        clearInvocations(mockedList);
        // no interactions happened after clear invocations
        verifyNoInteractions(mockedList);
    }

mock & spy

回到最初的起点,我们为什么要做mock?

我们mock的原因是某个对象的方法不好测试,因此需要打桩(就是把对象的真实方法屏蔽掉,换成桩)。这样就衍生出来一个问题,对象是什么不能确定,对象里面包含的方法也不能确定,因此一个对象可能包含大量的不好测试的方法、也有可能包含少量的不好测试方法,对于这两种情形,应当采取不同的方法加以应对,mocktio里面增加了spy这个方法,用来处理这个问题。

mock是建立一个假的对象,原来的对象方法全部被架空,对某个方法打桩,这个方法才会返回值

spy是建立一个真实的对象,原来对象的方法真实存在,对某个方法打桩,这个桩会覆盖掉原来的真实方法

do(…)when()… 和 when(…)then(…) 区别

监控对象上的区别

这里建立一个student类,这个类有两个方法,一个是获取学生name,一个是从数据库中获取学生分数

private static class Student {
        private MongoCollection<Document> studentCollection = null;
        private String name;

        public Student(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name == null ? "Tom Cruise" : this.name;
        }
        public Integer getMathScoreFromDB() {
            if (studentCollection != null) {
                return studentCollection.find(new Document("name", this.name)).first().getInteger("score");
            }
            throw new MongoException("");
        }
    }

验证以下代码

public void testSpy() { // It is suitable for a small number of methods that are to be mocked.

        // partial mock
        Student student = spy(new Student("Julia Roberts"));
        doReturn(100).when(student).getMathScoreFromDB(); // use doReturn() for stubbing
        Assert.assertEquals("Julia Roberts", student.getName());
        Assert.assertEquals(100, student.getMathScoreFromDB().intValue());

        reset(student);

        student = spy(new Student("Nicolas Cage"));
        Assert.assertEquals("Nicolas Cage", student.getName());
        thrown.expect(MongoException.class);
        // use thenReturn() for stubbing, but it does't work. The real method will be called
        when(student.getMathScoreFromDB()).thenReturn(80);
        student.getMathScoreFromDB();
    }

调用student.getName()的时候,之前并没有对这个方法进行打桩,因此这个地方调用的是对象的真实的方法。用doReturn的时候对getMathScoreFromDB()此函数进行打桩,返回结果自然是我们打桩的结果。

除了用doReturn(100).when(student).getMathScoreFromDB(),再用 when(student.getMathScoreFromDB()).thenReturn(80);进行打桩测试发现单元测试不能通过。原因是如果是对一个被监控的对象(spy)进行打桩,一定要用do(…)when(…)而不能用when(…)then(…)

do(…)when(…)和when(…)then(…)的区别可以概括为:

1、测试void函数
2、在受监控的对象上测试函数
3、不知一次的测试为同一个函数,在测试过程中改变mock对象的行为

再来举个例子

@Test
    public void mainTest() {
        List list = new LinkedList();
        List spy = spy(list);

        when(spy.get(0)).thenReturn("foo");

    }

此时将会报java.lang.IndexOutOfBoundsException异常,原因就是我们无法用when(…)then(…)对一个收到监控的对象进行打桩

如果换成以下代码则运行通过。

 List list = new LinkedList();
   List spy = spy(list);
   doReturn("foo").when(spy).get(0);

查看mockito源码发现,只有do句式才能对spy对象的方法进行打桩,when句式无效

真对象假操作

请看下面代码

public void testDoNothing() {
        List mockedList = spy(Lists.newArrayList("foo", "bar"));
        Assert.assertEquals("foo", mockedList.get(0));
        Assert.assertEquals("bar", mockedList.get(1));
        Assert.assertEquals(2, mockedList.size());
        mockedList.clear(); // do clear the list
        Assert.assertEquals(0, mockedList.size());

        mockedList = spy(Lists.newArrayList("new_foo", "new_bar"));
        doNothing().when(mockedList).clear();
        Assert.assertEquals("new_foo", mockedList.get(0));
        Assert.assertEquals("new_bar", mockedList.get(1));
        Assert.assertEquals(2, mockedList.size());
        mockedList.clear(); // do nothing
        Assert.assertEquals(2, mockedList.size());
    }

第一次调用的时候这个list真的会被清空,如何对clear()方法进行打桩呢,就是用do(…)when(…)句式来进行的

假对象真操作

mock 和 spy成对出现,既然有了再真对象进行假操作(stub)肯定就有在假对象上面做真操作的方式。

public void testThenCallRealMethod() { // It is suitable for a small number of methods that are actually called.
        // partial mock
        Student student = mock(Student.class);
        when(student.getMathScoreFromDB()).thenReturn(98);
        when(student.getName()).thenCallRealMethod();

        //doCallRealMethod().when(student).getName();
        Assert.assertEquals(98, student.getMathScoreFromDB().intValue());
        Assert.assertEquals("Tom Cruise", student.getName());

		
    }

这里的student对象是被mock掉的,它的所有方法都被架空,如何调用对象的真方法呢?when(…)then(…)句式可以采用thenCallRealMethod()进行调用,如果是do(…)then(…)则可以采用doCallRealMethod()进行调用。

其他特性

清空操作

 public void testClearInvocations() {
        List<String> mockedList = mock(List.class);
        mockedList.add("foo");
        mockedList.add("foo");

        inOrder(mockedList).verify(mockedList, times(2)).add("foo");
        clearInvocations(mockedList);
        // no interactions happened after clear invocations
        verifyNoInteractions(mockedList);
    }

可以对mock对象清空所有历史操作,用于验证此对象不会被再次调用

自定义输出

@Test(expected = MockitoAssertionError.class)
    public void testDescription() {
        Map<String, String> mockedMap = when(mock(Map.class).get("foo")).thenReturn("foo_value").getMock();
        verify(mockedMap, description("The key 'bar' not exist.")).get("bar");
    }

输出信息可以自定义,方便快速排查问题

总结

在这里插入图片描述

这张图大致描述了本文所涉及的内容。

mockito的三大法宝是when(…)then(…),do(…)when(…),verify()。其中when和do句式下面分别可以用return返回一个值,throw抛出一个异常,answer去对参数进行处理然后返回,这个三个都可能会用到参数匹配器。verify是对mock对象进行校验,对这个对象有没有做操作,做了多少次操作进行验证

mock对象建立有两种方式,一种是直接mock,此时对象的所有方法都被屏蔽,需要调用真实函数可以采用thenCallRealMethod()或者doCallRealMethod()。第二种是采用spy的方式,对一个真实对象操作,采用do句式对需要的方法进行打桩。

本资源可以在 https://gitee.com/tancehello/mockito-study 上下载

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值