在前面的 Spring Boot 入门教程 中,我们学习了如何创建 Spring Boot 应用程序并构建一个简单的 REST API。
在本教程中,你将学习如何为 Spring Boot 应用程序编写单元测试、片段测试和集成测试。
测试 Spring Boot 应用 {#测试-spring-boot-应用}
我们应该编写单元测试来验证特定单元(一个类、一个方法或一组类)的业务逻辑,而且它们不应该与任何外部服务(如数据库、队列或其他网络服务等)有联系。如果我们测试的单元依赖于任何外部服务,那么我们可以提供这些依赖关系的模拟(mock)实现,并验证单元的行为。
除了单元测试外,我们还应该编写集成测试,通过与实际服务和依赖的协作者(dependent collaborator)互动来检验子系统或组件的行为。
当我们生成 Spring Boot 应用程序时,会自动添加 spring-boot-starter-test
依赖项,它可以将 SpringTest
、JUnit5
、Mockito
、Assertj
、JsonPath
、JsonAssert
等最常用的测试库作为测试依赖项添加到我们的应用程序中。
测试的类型 {#测试的类型}
- Unit Tests(单元测试): 这些测试用于验证单个单元的行为,最好不要依赖 Spring 或 Hibernate 等框架。
- Slice Tests(片段测试): 这些测试用于验证应用程序的某个片段,如 Web 层或持久层等。Spring Boot 支持使用
@WebMvcTest
、@DataJpaTest
、@DataMongoTest
等测试应用程序的片段。 - Integration Tests(集成测试): 这些测试以黑盒方式测试应用程序。我们传入一些输入,并期待特定的输出,但我们不知道也不关心内部是如何工作的。Spring Boot 支持使用
@SpringBootTest
编写集成测试。
使用 JUnit 5 和 Mockito 进行单元测试 {#使用-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 注解测试应用程序片段 {#使用-spring-的-test-slice-注解测试应用程序片段}
现在,让我们使用 @WebMvcTest
为 HelloWorldController
写一个测试。
使用 @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 进行集成测试 {#使用-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/