首頁 > 軟體

使用mockito編寫測試用例教學

2022-08-25 14:02:53

前言

首先宣告筆者並不是一個TDD開發的擁躉,我傾向於實用主義。TDD有他的用武之地,但不是銀彈,同理BDD(行為驅動開發)也是如此。筆者堅持寫測試的原因只是為了更加方便的重構,當筆者意識到一個模組會被大範圍使用且會面臨多次迭代的時候,筆者就會認證寫測試,並且每次出bug都會用測試先復現。如果一個專案只是一次性的demo那些個啥的測試,一把梭哈得了。

什麼是TDD

TDD是測試驅動開發(Test-Driven Development)的英文簡稱,是敏捷開發中的一項核心實踐和技術,也是一種設計方法論。TDD的原理是在開發功能程式碼之前,先編寫單元測試用例程式碼,測試程式碼確定需要編寫什麼產品程式碼。TDD雖是敏捷方法的核心實踐,但不只適用於XP(Extreme Programming),同樣可以適用於其他開發方法和過程。

為什麼要使用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 不會給你帶來宿醉,因為測試非常易讀並且會產生乾淨的驗證錯誤

在開發程式的時候我們需要測試的類不可能都是簡單的類,很多複雜的邏輯往往需要多個前置條件才能測試到。如果為了測試去滿足這些前置條件未免顯得太繁瑣。比如有的邏輯需要進行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>

靜態匯入

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());

mockspy的區別

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();

下面看看我自己專案裡面得例子。這裡可以發現我在傳入引數得時候直接是傳入了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(),二者區別後文再說。

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)));
}

靜態方法模擬

靜態有返回值且存在鏈式呼叫

直接看筆者專案中的例子,注意這裡下面的例子有些許不同,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));
    });
}

靜態無返回值

someMock.when(() -> Files.delete(fileToDelete)).thenAnswer((Answer<Void>) invocation -> null);
// 也可以是下面這個
// someMock.when(() -> Files.delete(fileToDelete)).thenAnswer(Answers.RETURNS_DEFAULTS);

進階

mock和spy的區別

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的區別,對於未指定mock的方法,spy預設會呼叫真實的方法,有返回值的返回真實的返回值,而mock預設不執行,有返回值的,預設返回null。具體細節筆者也沒有深究比如實際上mock也能做到類似psy的效果

when(...).thenReturn(...)和doReturn(...).when(...)的區別

● when(...) thenReturn(...)會呼叫真實的方法,如果你不想呼叫真實的方法而是想要mock的話,就不要使用這個方法。

● doReturn(...) when(...) 不會呼叫真實方法

因此針對區別一般情況下如果時第三方庫得程式碼在需要測試得方法則可以使用 do...return進行略過,自己呼叫自己得方法則建議使用 when...return。但是有時候呼叫得方法需要一些特殊得環境才能起作用,那麼也能使用 do..return,亦或者被呼叫得方法已經測試過了也可以使用 do..return。下面看二者區別得例子。

例子:

@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實際上不執行包裝得方法,也就沒有執行onEntry方法,自然裡面 runAviatorScript也就沒有執行,因此就會導致驗證錯誤。

BDDMockito(行為驅動測試)

什麼是BDD

行為驅動開發(英語:Behavior-driven development,縮寫BDD)是一種敏捷軟體開發的技術,它鼓勵軟體專案中的開發者、QA和非技術人員或商業參與者之間的共同作業。BDD最初是由Dan North在2003年命名,它包括驗收測試和客戶測試驅動等的極限程式設計的實踐,作為對測試驅動開發的迴應。在過去數年裡,它得到了很大的發展。行為驅動測試的開發風格使用//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());
 }

如何模擬異常

可以發現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);

實戰中使用

這裡不僅模擬了方法的返回值,還模擬了springbootcontroller的呼叫

@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測試用例教學的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com