使用mockito編寫測試用例教程
前言
首先聲明筆者并不是一個TDD開發(fā)的擁躉,我傾向于實用主義。TDD有他的用武之地,但不是銀彈,同理BDD(行為驅動開發(fā))也是如此。筆者堅持寫測試的原因只是為了更加方便的重構,當筆者意識到一個模塊會被大范圍使用且會面臨多次迭代的時候,筆者就會認證寫測試,并且每次出bug都會用測試先復現。如果一個項目只是一次性的demo那些個啥的測試,一把梭哈得了。
什么是TDD
TDD是測試驅動開發(fā)(Test-Driven Development)的英文簡稱,是敏捷開發(fā)中的一項核心實踐和技術,也是一種設計方法論。TDD的原理是在開發(fā)功能代碼之前,先編寫單元測試用例代碼,測試代碼確定需要編寫什么產品代碼。TDD雖是敏捷方法的核心實踐,但不只適用于XP(Extreme Programming),同樣可以適用于其他開發(fā)方法和過程。
為什么要使用mockito
Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors.
Mockito 是非常不錯框架。它使您可以使用干凈簡單的 API 編寫漂亮的測試。 Mockito 不會給你帶來宿醉,因為測試非常易讀并且會產生干凈的驗證錯誤
在開發(fā)程序的時候我們需要測試的類不可能都是簡單的類,很多復雜的邏輯往往需要多個前置條件才能測試到。如果為了測試去滿足這些前置條件未免顯得太繁瑣。比如有的邏輯需要進行HTTP請求某個特定服務,我不想在測試的時候去單獨啟動這個服務。這時候我們就可以mock這個http請求,讓其返回一個特定值,以此簡化測試流程。
如何使用mockito
前期準備
引包
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-inline -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
靜態(tài)導入
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.mockito.Mockito.*;
包裝你要模擬的類
使用mock方法
LinkedList mockedList = mock(LinkedList.class);
使用spy方法
PDStateMachineNode mock = spy(new PDStateMachineNode());
mock和spy的區(qū)別
mock方法和spy方法都可以對對象進行mock。但是前者是接管了對象的全部方法,而后者只是將有樁實現(stubbing)的調用進行mock,其余方法仍然是實際調用。大家先這樣理解后面會具體舉例子。
什么插樁
其實就是對要模擬方法進行包裝,設定返回值或者拋異常之類,直接看官方例子。這里就是對get(0) 和get(1) 進行插樁了
//You can mock concrete classes, not just interfaces
LinkedList mockedList = mock(LinkedList.class);
//插樁
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.
verify(mockedList).get(0);
驗證行為
可以用來驗證某個方法是否調用了
這里使用了官網例子,大致用途就是你可以用它來驗證某個方法被調用了沒有。可以很明顯在的看到他在驗證是否調用了add和clear
//Let's import Mockito statically so that the code looks clearer
import static org.mockito.Mockito.*;
//mock creation
List mockedList = mock(List.class);
//using mock object
mockedList.add("one");
mockedList.clear();
//verification
verify(mockedList).add("one");
verify(mockedList).clear();
下面看看我自己項目里面得例子。這里可以發(fā)現我在傳入參數得時候直接是傳入了any(),這也是mockito提供得,與此對應得還有anyString(),anyInt()等等。
大致意思就是驗證runAviatorScript這個方法被調用的了應該有一次。
@Test
void testOnEntry2() throws NodeExecuteTimeoutException {
PDStateMachineNode mock = spy(PDStateMachineNode.class);
mock.onEntry(any());
// 默認第二個參數就是times(1),因此這里可以不寫
verify(mock,times(1)).runAviatorScript(any(),any(),anyString());
}
參數匹配
方便得模擬一次方法得參數
直接上官方例子,上述代碼也有使用
//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(true);
//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));
如果你使用的參數匹配器,那么所有參數都必須提供參數匹配器,否則會拋異常。
//正確
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//錯誤
verify(mock).someMethod(anyInt(), anyString(), "third argument");
驗證調用次數
就是驗證某個方法調用了多少次
//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, atMostOnce()).add("once");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");
筆者項目中得例子
@Test
void testDELETE() {
Assertions.assertTimeout(Duration.ofSeconds(10), () -> {
mockStatic.when(() -> HttpRequest.delete("test").execute().body()).thenReturn("success");
String execute = (String) AviatorEvaluator.execute("return DELETE("test");");
Assertions.assertTrue(execute.contains("success"));
//在這里
mockStatic.verify(() -> HttpRequest.delete(anyString()), times(2));
});
}
模擬void方法
項目中的例子
當調用onExit的時候啥也不干
@Test
void onExit() throws NodeExecuteTimeoutException {
StateMachineInterpreter interpreter = new StateMachineInterpreter();
StateMachineNode spy = spy(new StateMachineNode());
//啥也不干
doNothing().when(spy).onExit(any());
}
驗證調用順序
有時候在需要驗證某個方法內部調用其他方法的順序。筆者例子如下:
這段測試意思就是模擬 PDStateMachineNode這個類調用verify()方法得邏輯。分兩種情況
onCheck方法返回true(doReturn(true).when(mock).onCheck(any()))
此種情況下,方法調用順序應為:onEntry,onCheck,onMatch,onExit
onCheck返回false(doReturn(false).when(mock).onCheck(any()))
此種情況下,方法調用順序應為:onEntry,onCheck,onFail,onExit
PDStateMachineNode mock = spy(new PDStateMachineNode()); doReturn(true).when(mock).onCheck(any()); mock.verify(any()); InOrder inOrder = inOrder(mock); inOrder.verify(mock).onEntry(any()); inOrder.verify(mock).onCheck(any()); inOrder.verify(mock).onMatch(any()); inOrder.verify(mock).onExit(any()); doReturn(false).when(mock).onCheck(any()); mock.verify(any()); InOrder inOrder2 = inOrder(mock); inOrder2.verify(mock).onEntry(any()); inOrder2.verify(mock).onCheck(any()); inOrder2.verify(mock).onFail(any()); inOrder2.verify(mock).onExit(any());
doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod()
官方建議大部分情況下你應該使用when(),二者區(qū)別后文再說。
doReturn()
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", "bar", "qix");
//You have to use doReturn() for stubbing:
doReturn("foo", "bar", "qix").when(spy).get(0);
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", "bar", "qix");
//You have to use doReturn() for stubbing:
doReturn("foo", "bar", "qix").when(spy).get(0);
doThrow()
doThrow(new RuntimeException()).when(mock).someVoidMethod(); doThrow(RuntimeException.class).when(mock).someVoidMethod();
doAnswer()
doAnswer(new Answer() {
public Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Mock mock = invocation.getMock();
return null;
}})
.when(mock).someMethod();
doNothing()
doNothing().
doThrow(new RuntimeException())
.when(mock).someVoidMethod();
//does nothing the first time:
mock.someVoidMethod();
//throws RuntimeException the next time:
mock.someVoidMethod();
doCallRealMethod()
Foo mock = mock(Foo.class); doCallRealMethod().when(mock).someVoidMethod(); // this will call the real implementation of Foo.someVoidMethod() mock.someVoidMethod();
筆者項目中的使用的例子
@Test
void step() throws NodeExecuteTimeoutException, NextNodesNotExistException {
PDStateMachineNode spy = spy(new PDStateMachineNode());
PDStateMachineNode node = new PDStateMachineNode();
PDStateMachineNode subSpy = spy(node);
doReturn(true).when(subSpy).verify(any());
doReturn(List.of(subSpy)).when(spy).getNextNodes();
PDStateMachineNode step = spy.step(any(PointData.class));
Assertions.assertEquals(subSpy, step);
when(spy.getNextNodes()).thenReturn(new ArrayList<>());
doReturn(true).when(spy).isTerminalNode();
Assertions.assertThrows(NextNodesNotExistException.class, () -> spy.step(any(PointData.class)));
doReturn(new ArrayList<>()).when(spy).getNextNodes();
doReturn(false).when(spy).isTerminalNode();
Assertions.assertEquals(spy, spy.step(any(PointData.class)));
}
靜態(tài)方法模擬
靜態(tài)有返回值且存在鏈式調用
直接看筆者項目中的例子,注意這里下面的例子有些許不同,RETURNS_DEEP_STUBS其實是用來進行嵌套模擬的。因為HttpRequest.get("test").execute().body() 是一個鏈式的調用,實際上涉及了多個類的模擬。如果你沒有這個需求就可以不加。
@BeforeAll
void init() {
mockStatic = mockStatic(HttpRequest.class, RETURNS_DEEP_STUBS);
}
@Test
void testGET() {
Assertions.assertTimeout(Duration.ofSeconds(10), () -> {
mockStatic.when(() -> HttpRequest.get("test").execute().body()).thenReturn("success");
String execute = (String) AviatorEvaluator.execute("return GET("test");");
Assertions.assertTrue(execute.contains("success"));
mockStatic.verify(() -> HttpRequest.get(anyString()), times(2));
});
}
靜態(tài)無返回值
someMock.when(() -> Files.delete(fileToDelete)).thenAnswer((Answer<Void>) invocation -> null); // 也可以是下面這個 // someMock.when(() -> Files.delete(fileToDelete)).thenAnswer(Answers.RETURNS_DEFAULTS);
進階
mock和spy的區(qū)別
mock方法和spy方法都可以對對象進行mock。但是前者是接管了對象的全部方法,而后者只是將有樁實現(stubbing)的調用進行mock,其余方法仍然是實際調用。
使用mock
PDStateMachineNode mock = mock(PDStateMachineNode.class); mock.onEntry(any()); verify(mock, times(1)).runAviatorScript(any(), any(), anyString());
拋錯如下,意思就是runAviatorScript沒有被調用,因此驗證失敗。實際上筆者在onEntry內部是調用了runAviatorScript方法的
Wanted but not invoked:
pDStateMachineNode.runAviatorScript(
<any>,
<any>,
<any string>
);
-> at core.state.GenericStateMachineNode.runAviatorScript(GenericStateMachineNode.java:78)
使用spy,則無任務錯誤。
@Test
void testOnEntry2() throws NodeExecuteTimeoutException {
PDStateMachineNode mock = spy(PDStateMachineNode.class);
mock.onEntry(any());
verify(mock, times(1)).runAviatorScript(any(), any(), anyString());
}
從上述對比就可以理解mock和spy的區(qū)別,對于未指定mock的方法,spy默認會調用真實的方法,有返回值的返回真實的返回值,而mock默認不執(zhí)行,有返回值的,默認返回null。具體細節(jié)筆者也沒有深究比如實際上mock也能做到類似psy的效果
when(...).thenReturn(...)和doReturn(...).when(...)的區(qū)別
● when(...) thenReturn(...)會調用真實的方法,如果你不想調用真實的方法而是想要mock的話,就不要使用這個方法。
● doReturn(...) when(...) 不會調用真實方法
因此針對區(qū)別一般情況下如果時第三方庫得代碼在需要測試得方法則可以使用 do...return進行略過,自己調用自己得方法則建議使用 when...return。但是有時候調用得方法需要一些特殊得環(huán)境才能起作用,那么也能使用 do..return,亦或者被調用得方法已經測試過了也可以使用 do..return。下面看二者區(qū)別得例子。
例子:
@Override
public void onEntry(T event) throws NodeExecuteTimeoutException {
System.out.println("hello");
this.runAviatorScript(this.onEntry, event, "onEntry");
}
測試when..return...:
@Test
void testOnEntry2() throws NodeExecuteTimeoutException {
PDStateMachineNode mock = spy(PDStateMachineNode.class);
when(mock.onCheck(any())).thenReturn(true);
mock.onEntry(any());
verify(mock, times(1)).runAviatorScript(any(), any(), anyString());
}
結果可以看到輸出得hello

測試do...return...
@Test
void testOnEntry2() throws NodeExecuteTimeoutException {
PDStateMachineNode mock = spy(PDStateMachineNode.class);
doNothing().when(mock).onEntry(any());
mock.onEntry(any());
verify(mock, times(1)).runAviatorScript(any(), any(), anyString());
}
結果可以看到不僅沒輸出還報錯了,為什么呢?因為 do..return實際上不執(zhí)行包裝得方法,也就沒有執(zhí)行onEntry方法,自然里面 runAviatorScript也就沒有執(zhí)行,因此就會導致驗證錯誤。

BDDMockito(行為驅動測試)
什么是BDD
行為驅動開發(fā)(英語:Behavior-driven development,縮寫BDD)是一種敏捷軟件開發(fā)的技術,它鼓勵軟件項目中的開發(fā)者、QA和非技術人員或商業(yè)參與者之間的協作。BDD最初是由Dan North在2003年命名,它包括驗收測試和客戶測試驅動等的極限編程的實踐,作為對測試驅動開發(fā)的回應。在過去數年里,它得到了很大的發(fā)展。行為驅動測試的開發(fā)風格使用//given//when//then 作為測試方法的基本部分。
其實還是比較簡單的,粗淺的理解就是換了幾個API。
舉個例子
直接看幾個官方的例子
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());
}
如何模擬異常
可以發(fā)現willThrow就像之前的doThrow差不多
//given
willThrow(new RuntimeException("boo")).given(mock).foo();
//when
Result result = systemUnderTest.perform();
//then
assertEquals(failure, result);
驗證調用次數
person.ride(bike); person.ride(bike); then(person).should(times(2)).ride(bike); then(person).shouldHaveNoMoreInteractions(); then(police).shouldHaveZeroInteractions();
驗證調用順序
InOrder inOrder = inOrder(person); person.drive(car); person.ride(bike); person.ride(bike); then(person).should(inOrder).drive(car); then(person).should(inOrder, times(2)).ride(bike);
實戰(zhàn)中使用
這里不僅模擬了方法的返回值,還模擬了springboot中controller的調用
@Test
void shouldNotListRoles() throws Exception {
given(roleService.findAll()).willReturn(new ArrayList<>());
ResultActions actions = this.mvc.perform(get("/api/role/getRoles"));
actions.andExpect(status().isOk()).andReturn().getResponse().setCharacterEncoding("UTF-8");
actions.andDo(print()).andExpect(jsonPath("$.data.length()").value(Matchers.is(0)));
}
@Test
void shouldCreateRole() throws Exception {
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
Role role = new Role(null, "mfine",
LocalDateTime.now(), "", 0, null, "admin");
// 這里 也使用了參數匹配器
given(roleService.insertSelective(BDDMockito.any())).willReturn(1);
ResultActions actions = this.mvc.perform(post("/api/role/createRole").content(objectMapper.writeValueAsString(role))
.accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON));
actions.andExpect(status().isOk()).andReturn().getResponse().setCharacterEncoding("UTF-8");
actions.andExpect(ResultMatcher.matchAll(result -> {
Assert.assertTrue(result.getResponse().getContentAsString().contains("success"));
}));
}
總結
完整的測試時重構得底氣,當沒有測試得時候一切重構都是扯淡。程序出bug之后第一件事應該是復現bug,增補測試然后再是修復bug,如果是線上緊急情況那也應該在時候補充測試。但是測試也不是銀彈,所謂的TDD和BDD也要看情況使用,沒有萬能方案只有適合方法。
以上就是使用mockito編寫測試用例教程的詳細內容,更多關于mockito測試用例教程的資料請關注腳本之家其它相關文章!
相關文章
Springboot項目基于Devtools實現熱部署步驟詳解
這篇文章主要介紹了Springboot項目基于Devtools實現熱部署,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-06-06
SpringBoot2.6.x默認禁用循環(huán)依賴后的問題解決
由于SpringBoot從底層逐漸引導開發(fā)者書寫規(guī)范的代碼,同時也是個憂傷的消息,循環(huán)依賴的應用場景實在是太廣泛了,所以SpringBoot 2.6.x不推薦使用循環(huán)依賴,本文給大家說下SpringBoot2.6.x默認禁用循環(huán)依賴后的應對策略,感興趣的朋友一起看看吧2022-02-02
關于yml文件字符串,List,Map的書寫方式并使用@ConfigurationProperties注入配置類
這篇文章主要介紹了關于yml文件字符串,List,Map的書寫方式并使用@ConfigurationProperties注入配置類,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12

