Spring Boot 单元测试实践(三)

1,764 阅读3分钟

前言

本文为 Spring Boot 单元测试实践系列文章第三篇。本文主要内容为多种 Spring 容器初始化方式、参数捕获以及 Answer 的基本介绍及用法。

前文:
Spring Boot 单元测试实践:单元测试的基本方式
Spring Boot 单元测试实践(二):常用断言,及业务方法编写单元测试示例

实践

Spring 容器初始化方式

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

此方式为个人习惯使用,在前文也多次提及。

@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 的普通的单元测试,在其它需要进行依赖注入的框架也能使用。

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

}

方式三:TestContextManager

在 Junit4 中想要执行参数化测试,又想要启动 Spring 容器则可以通过TestContextManager实现

@RunWith 注解只支持一种 Runner,而在 Junit4 参数化测试又得单独选择 Parameterized
在 Junit5 通过方法注解(@ParameterizedTest, @ValueSource)即可实现参数化测试

@RunWith(Parameterized.class)
@Import({URLRedirectSelector.class})
@ActiveProfiles("test")
public class MixURLRedirectTest extends BaseTest {

    @Autowired
    private URLRedirectSelector selector;

    @Before
    public void setUp() throws Exception {
        TestContextManager testContextManager = new TestContextManager(getClass());
        testContextManager.prepareTestInstance(this);
    }

}

TestContextManagerTestContext 是 SpringTest 框架的核心,也是整个测试框架的入口

TestContextManager 会创建并管理 TestContext,而测试上下文中缓存了各种实例信息,同时可以获取到 Spring ApplicationContext

参数捕获

在业务方法中可能会调用到返回值为 void 的方法,而该方法的入参可能是经过一些逻辑处理得到的,且该逻辑无法单独作为一个方法处理(如果可以单独封装为方法,则只需要单独编写用例断言返回值即可),此时可以选择对该方法的核心参数进行断言。

ArgumentCaptor 参数捕获器,作为参数与 doNothing().when() 结合使用,不执行方法逻辑,仅用于捕获参数,并通过 getValue() or getAllValues() 返回单个参数或者多个参数(比如 for 循环后)

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 对象