Spring Boot 單元測試實踐(三)

語言: CN / TW / HK

theme: smartblue highlight: github


前言

本文為 Spring Boot 單元測試實踐系列文章第三篇。本文主要內容為兩種單元測試寫法、引數捕獲以及 Answer 的基本介紹及用法。

前文:
Spring Boot 單元測試實踐:單元測試的基本方式
Spring Boot 單元測試實踐(二):常用斷言,及業務方法編寫單元測試示例

實踐

版本:Spring Boot 2.3.12.RELEASE、Junit5

單元測試寫法

方式一:@Import + @ExtendWith(@RunWith)

此方式為個人習慣使用,在前文也多次提及。

```java @Import({OtherService.class}) @ExtendWith(SpringExtension.class) public class ServiceInit1Test {

@MockBean
private UserRepository userRepository;
@SpyBean
private DummyRepository dummyRepository;
@MockBean
private MockRepository mockRepository;
@Resource
private OtherService service;

@Test
public void init() {
    Assertions.assertNotNull(userRepository);
    Assertions.assertNotNull(dummyRepository);
    Assertions.assertNotNull(mockRepository);

    service.doSomething();
}

} ```

方式二:@InjectMocks

此方式不會啟動 Spring 容器,更像是一種脫離了 Spring 的普通的單元測試,在其它需要進行依賴注入的框架也能使用。

```java import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy;

public class ServiceInit2Test {

@Mock
private UserRepository userRepository;
@Spy
private DummyRepository dummyRepository;
@Mock
private MockRepository mockRepository;
@InjectMocks
private OtherService service;

@BeforeEach
public void setUp() {
    MockitoAnnotations.initMocks(this);
}

@Test
public void init() {
    Assertions.assertNotNull(userRepository);
    Assertions.assertNotNull(dummyRepository);
    Assertions.assertNotNull(mockRepository);

    service.doSomething();
}

} ```

引數捕獲

在業務方法中可能會呼叫到返回值為 void 的方法,而該方法的入參可能是經過一些邏輯處理得到的,且該邏輯無法單獨作為一個方法處理(如果可以單獨封裝為方法,則只需要單獨編寫用例斷言返回值即可),此時可以選擇對該方法的核心引數進行斷言。

ArgumentCaptor 引數捕獲器,作為引數與 doNothing().when() 結合使用,不執行方法邏輯,僅用於捕獲引數,並通過 getValue() or getAllValues() 返回單個引數或者多個引數(比如 for 迴圈後)

```java import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.springframework.context.annotation.Import; import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.spy;

@Import(CaptureService.class) @ExtendWith(SpringExtension.class) public class CaptureServiceTest {

@Resource
private CaptureService service;

@Test
public void captor() {
    CaptureService spy = spy(service);
    ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
    doNothing().when(spy).print(captor.capture());
    spy.add(1,2);

    Integer value = captor.getValue();
    Assertions.assertThat(value).isEqualTo(3);

}

} ```

Answer

Answer 是一個配置 mock 結果的通用介面。

使用 Answer 可以做兩件事,一是如 ArgumentCaptor 一樣捕獲引數,二是同於 thenReturn(),可以自定義返回值,通過不同的條件返回不同的值。如當 ArgumentCaptorthenReturn() 都不滿足時,可以考慮下使用 Answer。

其它的 stubebr 方法其底層實現其實都是使用 Answer,只是進一步封裝了一些通用的方法便於使用。

``` import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.context.annotation.Import; import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy;

@Import(AnswerService.class) @ExtendWith(SpringExtension.class) public class AnswerServiceTest {

@Resource
private AnswerService service;

@Test
public void answer() {
    AnswerService spy = spy(service);
    doAnswer(invocationOnMock ->{
        Integer i = invocationOnMock.getArgument(0, Integer.class);
        Assertions.assertThat(i).isEqualTo(3);
        Integer result = (Integer) invocationOnMock.callRealMethod();
        Assertions.assertThat(result).isEqualTo(4);
        return -1;
    }).when(spy).doSomething(anyInt());

    spy.add(1,2);
}

} ```

ArgumentCaptor 和 Answer 的用例中使用 spy 的原因是 when、thenReturn、doAnswer 等方法的引數需要為 stubber 物件才能生效,即 mock 或者 spy 物件