Spring Boot 的测试教程

在前面的 Spring Boot 入门教程 中,我们学习了如何创建 Spring Boot 应用程序并构建一个简单的 REST API。

在本教程中,你将学习如何为 Spring Boot 应用程序编写单元测试、片段测试和集成测试。

测试 Spring Boot 应用

我们应该编写单元测试来验证特定单元(一个类、一个方法或一组类)的业务逻辑,而且它们不应该与任何外部服务(如数据库、队列或其他网络服务等)有联系。如果我们测试的单元依赖于任何外部服务,那么我们可以提供这些依赖关系的模拟(mock)实现,并验证单元的行为。

除了单元测试外,我们还应该编写集成测试,通过与实际服务和依赖的协作者(dependent collaborator)互动来检验子系统或组件的行为。

当我们生成 Spring Boot 应用程序时,会自动添加 spring-boot-starter-test 依赖项,它可以将 SpringTestJUnit5MockitoAssertjJsonPathJsonAssert 等最常用的测试库作为测试依赖项添加到我们的应用程序中。

测试的类型

  • Unit Tests(单元测试): 这些测试用于验证单个单元的行为,最好不要依赖 Spring 或 Hibernate 等框架。
  • Slice Tests(片段测试): 这些测试用于验证应用程序的某个片段,如 Web 层或持久层等。Spring Boot 支持使用 @WebMvcTest@DataJpaTest@DataMongoTest 等测试应用程序的片段。
  • Integration Tests(集成测试): 这些测试以黑盒方式测试应用程序。我们传入一些输入,并期待特定的输出,但我们不知道也不关心内部是如何工作的。Spring Boot 支持使用 @SpringBootTest 编写集成测试。

使用 JUnit 5 和 Mockito 进行单元测试

我们将为之前的 Spring Boot 入门教程 中实现的 REST API 编写测试。

让我们从编写 GreetingService 的单元测试开始。我们将使用 JUnit 5 和 Mockito 来编写单元测试。

// src/test/java/com/sivalabs/helloworld/GreetingServiceTest.java

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

@ExtendWith(MockitoExtension.class) // (1)
class GreetingServiceTest {

    @Mock // (2)
    private ApplicationProperties properties;

    @InjectMocks // (3)
    private GreetingService greetingService;

    @BeforeEach // (4)
    void setUp() {
        given(properties.getGreeting()).willReturn("Hello");
    }

    @Test
    void shouldGreetWithDefaultNameWhenNameIsNotProvided() {
        given(properties.getDefaultName()).willReturn("World");

        String greeting = greetingService.sayHello(null);

        Assertions.assertEquals("Hello World", greeting); //JUnit 5 based assertion (5)
        assertThat(greeting).isEqualTo("Hello World"); //Assertj based assertion (6)
    }

    @Test
    void shouldGreetWithGivenName() {
        String greeting = greetingService.sayHello("John");

        assertThat(greeting).isEqualTo("Hello John");
    }
}
  • (1) 我们使用 MockitoExtension,以便创建 mock 对象并使用注解注入它们。
  • (2) @Mock 注解将使用 ApplicationProperties 类的 mock 实现来初始化属性。
  • (3) @InjectMocks 注解将通过使用定义的 mock 对象注入依赖关系(属性)来创建 GreetingService 的实例。
  • (4) @BeforeEach 注解方法将在每次测试执行前执行,这样我们就可以在其中放入任何常见的测试设置。
  • (5) 我们使用基于 JUnit 5 的断言来断言预期输出和实际输出。
  • (6) 我们使用基于 Assertj 的断言来断言预期输出和实际输出。

JUnit5的断言(如 Assertions.assertEquals())可以完成这项工作,而 Assertj 的断言则支持 fluent 风格的调用,你可以根据对象的类型获得各种方便的断言方法。

使用 Spring 的 Test Slice 注解测试应用程序片段

现在,让我们使用 @WebMvcTestHelloWorldController 写一个测试。

使用 @WebMvcTest 时,只有 Controller、Interceptor 等 Web 层组件会被加载到 ApplicationContext 中。因此,我们需要通过 @MockBean 或使用 @TestConfiguration 配置依赖 bean 等方法添加依赖 bean。

在单元测试中,我们使用 Mockito 的 @Mock 注解创建了一个 mock Bean,但在这里我们使用的是 Spring 的 @MockBean 而不是 @Mock。使用 @WebMvcTest 时,测试实例和 ApplicationContext 的创建由 Spring 负责,而 Spring 并不知道 @Mock 创建的普通 mock 对象。而使用 Spring 的 @MockBean 时,该 mock Bean 将成为 ApplicationContext 的一部分并注入 Controller。

// src/test/java/com/sivalabs/helloworld/HelloWorldControllerTest.java

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.CoreMatchers.is;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(controllers = HelloWorldController.class) // (1)
class HelloWorldControllerTest {
    @Autowired
    private MockMvc mockMvc; // (2)

    @MockBean
    private GreetingService greetingService; // (3)

    @Test
    void shouldReturnGreetingSuccessfully() throws Exception {
        given(greetingService.sayHello("Siva")).willReturn("Hello Siva"); // (4)

        mockMvc.perform(get("/api/hello?name={name}", "Siva"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.greeting", is("Hello Siva"))); // (5)
    }
}
  • (1) 使用 @WebMvcTest(controllers = HelloWorldController.class),我们只测试 web 层 controller,只加载 HelloWorldController
  • (2) 使用 @WebMvcTest 注解时,MockMvc 将自动配置,我们可以自动装配并使用它来调用 API 端点。
  • (3) 我们使用 @MockBean 将模拟 GreetingService Bean 注入 HelloWorldController
  • (4) 在 mock GreetingService Bean 上设置 mock 行为。
  • (5) 调用 GET /api/hello API 并使用 jsonPath 断言断言 Http 响应状态码和 body。

由于 Slice 测试只加载一小部分 Spring 组件,因此比使用 @SpringBootTest 编写的集成测试更快。

使用 @SpringBootTest 进行集成测试

最后,让我们编写一个集成测试,加载整个应用程序,然后调用 API 并测试结果。

// src/test/java/com/sivalabs/helloworld/SpringBootHelloWorldApplicationTests.java

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.CoreMatchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) // (1)
@AutoConfigureMockMvc  // (2)
class SpringBootHelloWorldApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldReturnGreetingSuccessfully() throws Exception {
        mockMvc.perform(get("/api/hello?name={name}", "Siva"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.greeting", is("Hello Siva")));
    }
}
  • (1) 我们使用 @SpringBootTest 来加载整个应用程序,并指定 webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT 来在随机可用端口上启动应用程序,这样就不会与任何正在运行的应用程序发生端口冲突。当在 Jenkins 等构建服务器上运行测试时,这一点尤其有用,因为在这些服务器上会并行运行多个应用程序构建。
  • (2) 在使用 @SpringBootTest 时,MockMvc Bean 不会自动配置,因此我们使用 @AutoConfigureMockMvc 来配置 MockMvc Bean。

我们只是触及了 Spring Boot 应用程序测试的皮毛。在接下来的文章中,我们将探索更多的测试技术。

运行测试

我们可以使用构建工具运行测试,如下所示:

Maven:

./mvnw verify

Gradle:

./gradlew test

总结

我们学习了如何使用 JUnit 5 和 Mockito 编写单元测试,以及使用 Spring Boot Test Slice 支持测试应用程序的片段。最后,我们学习了如何通过引导整个应用程序来编写集成测试。


参考:https://www.sivalabs.in/spring-boot-testing-tutorial/