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 上下载