Mockito Cookbook
前言
对Mockito的很多api感到迷惑,不知道起到什么作用。基于目前基于单元测试开发的想法,需要在开发之前大致了解清楚Mockito常用api的用法,以便于做快速单元测试。
首先Mockito主要作用是模拟一个类的行为,并通过模拟行为对类的调用次数、执行流程做判断。 在使用Mockito之前,需要在测试类中对它做初始化,这里有两种方法:
@ExtendWith(MockitoExtension.class)
@BeforeEachpublic void init() { MockitoAnnotations.openMocks(this);}
Mock & Spy
@Mockprivate List<String> mockList;----------------------------------------------List<String> mockList = Mockito.mock(List.class);
以上即完成了对List对象的模拟。mockList是模拟出来的对象,它具有List接口具有的所有能力,但是并不真正调用它。 与Mock相对,Spy不是模拟一个对象,而是监听一个真实的对象:
@Spyprivate List<String> spyList = new ArrayList<>();--------------------------------------------List<String> list = new ArrayList<>();List<String> spyList = Mockito.spy(list);
对象被spy之后可以通过when/then之类的方法覆盖原本的方法执行。
List<String> list = new ArrayList<String>(); List<String> spyList = spy(list);
assertEquals(0, spyList.size());
doReturn(100).when(spyList).size(); assertThat(spyList).hasSize(100);
verify cookbook
@Testpublic void test() { mockList.add("one"); Mockito.verify(mockList).add("one"); Assertions.assertEquals(mockList.size(), 0);}
在这段代码中,首先调用了这个虚拟List对象的add()方法添加了一个元素。之后verify(mockList)的意思是“我要对mockList这个对象做一下验证啦!”,那么要做什么验证呢?就是验证它是否调用了add()方法并且添加了“one”
进去。上面这段代码执行的话可以正常通过测试无事发生,但是如果删除掉第三行,verify方法就会报错Wanted but not invoked:mockList.add("one");
。或者将第三行add的值改成"two"
,也会得到一个错误Argument(s) are different! Wanted:mockList.add("one");Actual invocations have different arguments:mockList.add("two");
。
有意思的是,第五行的断言是正确的,可能对mock对象的调用不会调用被mock的类?所以被mock类的“调用add方法则size返回值加1”这种逻辑不会被执行。
OK,verify的作用总结来说就是“验证交互”。它还有以下用法:
- 验证交互次数&至少发生一定次数:
List<String> mockedList = mock(MyList.class);mockedList.size();verify(mockedList, times(1)).size();--------------------------------------------------------------List<String> mockedList = mock(MyList.class);mockedList.clear();mockedList.clear();mockedList.clear();
verify(mockedList, atLeast(1)).clear();verify(mockedList, atMost(10)).clear();
- 验证未与指定mock类发生交互;
List<String> mockedList = mock(MyList.class);verifyNoInteractions(mockedList);
- 验证未与指定方法发生交互;
List<String> mockedList = mock(MyList.class);verify(mockedList, times(0)).size();
- 验证没有意外的交互-这条会失败;
List<String> mockedList = mock(MyList.class);mockedList.size();mockedList.clear();
verify(mockedList).size();assertThrows(NoInteractionsWanted.class, () -> verifyNoMoreInteractions(mockedList));
- 验证交互顺序;
List<String> mockedList = mock(MyList.class);mockedList.size();mockedList.add("a parameter");mockedList.clear();
InOrder inOrder = Mockito.inOrder(mockedList);inOrder.verify(mockedList).size();inOrder.verify(mockedList).add("a parameter");inOrder.verify(mockedList).clear();
- 验证交互是否未发生;
List<String> mockedList = mock(MyList.class);mockedList.size();
verify(mockedList, never()).clear();
- 验证与确切参数的交互;
List<String> mockedList = mock(MyList.class);mockedList.add("test");
verify(mockedList).add("test");
- 验证与 flexible/any 参数的交互;
List<String> mockedList = mock(MyList.class);mockedList.add("test");
verify(mockedList).add(anyString());
- 使用参数捕获验证交互:
List<String> mockedList = mock(MyList.class);mockedList.addAll(Lists.<String> newArrayList("someElement"));
ArgumentCaptor<List<String>> argumentCaptor = ArgumentCaptor.forClass(List.class);verify(mockedList).addAll(argumentCaptor.capture());
List<String> capturedArgument = argumentCaptor.getValue();assertThat(capturedArgument).contains("someElement");
when/then cookbook
先创建一个简单类:
public class MyList extends AbstractList<String> {
@Override public String get(final int index) { return null; } @Override public int size() { return 1; }}
然后给mock类配置简单的返回值:
MyList listMock = mock(MyList.class);when(listMock.add(anyString())).thenReturn(false);
boolean added = listMock.add(randomAlphabetic(6));assertThat(added).isFalse();
可以看到when/then的语法很语义化,第二行可以这么读:“当listMock调用add方法并且参数是任意字符串的时候,return false”。 之后的两行就是对配置的这个行为的验证:调用add并随便传入什么东西,然后验证返回值是否是false。 上例还有另一种写法:
MyList listMock = mock(MyList.class);doReturn(false).when(listMock).add(anyString());
boolean added = listMock.add(randomAlphabetic(6));assertThat(added).isFalse();
配置一个方法调用出现的异常:
MyList listMock = mock(MyList.class);when(listMock.add(anyString())).thenThrow(IllegalStateException.class);
assertThrows(IllegalStateException.class, () -> listMock.add(randomAlphabetic(6)));
配置具有 void 返回类型的方法的行为 — 抛出异常:
MyList listMock = mock(MyList.class);doThrow(NullPointerException.class).when(listMock).clear();
assertThrows(NullPointerException.class, () -> listMock.clear());
配置多个调用的行为:
MyList listMock = mock(MyList.class);when(listMock.add(anyString())) .thenReturn(false) .thenThrow(IllegalStateException.class);
assertThrows(IllegalStateException.class, () -> { listMock.add(randomAlphabetic(6)); listMock.add(randomAlphabetic(6));});
配置spy的行为:
MyList instance = new MyList();MyList spy = spy(instance);
doThrow(NullPointerException.class).when(spy).size();
assertThrows(NullPointerException.class, () -> spy.size());
配置方法以在模拟上调用真实的底层方法:
MyList listMock = mock(MyList.class);when(listMock.size()).thenCallRealMethod();
assertThat(listMock).hasSize(1);
使用自定义 Answer 配置模拟方法调用:
MyList listMock = mock(MyList.class);doAnswer(invocation -> "Always the same").when(listMock).get(anyInt());
String element = listMock.get(1);assertThat(element).isEqualTo("Always the same");
这里的answer应该是可以在这个闭包里自定义调用这个方法的逻辑。
ArgumentCaptor
参数捕获器,直接上例子:
@Testpublic void whenNotUseCaptorAnnotation_thenCorrect() { List mockList = Mockito.mock(List.class); ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);
mockList.add("one"); Mockito.verify(mockList).add(arg.capture());
assertEquals("one", arg.getValue());}
可以看到,ArgumentCaptor对象在验证的时候调用可以将原本的参数捕获到。暂时不知道还能用在什么地方,感觉可以用于捕获方法运行中的中间结果并验证。
另外其也可以使用@Captor
注解创建。
InjectMocks
InjectMocks可以将mock对象注入到另一个类的属性中: 先来看一个类:
public class MyDictionary { Map<String, String> wordMap;
public MyDictionary() { wordMap = new HashMap<String, String>(); } public void add(final String word, final String meaning) { wordMap.put(word, meaning); } public String getMeaning(final String word) { return wordMap.get(word); }}
我们准备注入wordMap这个属性:
@MockMap<String, String> wordMap;
@InjectMocksMyDictionary dic = new MyDictionary();
@Testpublic void whenUseInjectMocksAnnotation_thenCorrect() { Mockito.when(wordMap.get("aWord")).thenReturn("aMeaning");
assertEquals("aMeaning", dic.getMeaning("aWord"));}
这里可以看到,首先创建一个wordMap的mock对象,然后在MyDictionary对象上加@InjectMocks注解,wordMap就会被注入进去,我们就可以通过控制wordMap的行为改变被注入的类的行为。