精通Spring Boot 3 : 8. Spring Boot 测试 (1)
在本章中,我们将讨论 Spring Boot 如何利用 Spring 测试框架的强大功能,通过提供强大的工具来轻松进行单元测试和集成测试,从而促进开发。在之前的章节中,我们对两个应用程序进行了测试,但尚未涵盖 Spring Boot 测试框架的其他重要特性,因此让我们开始讨论 Spring 测试框架,它是 Spring Boot 测试的基础。
Spring 测试框架
Spring 框架的一个主要理念是鼓励开发者创建简单且松散耦合的类,并通过接口进行编程,从而使软件更加健壮和可扩展。Spring 框架提供了便于单元测试和集成测试的工具(实际上,如果你真正以接口编程,就不需要 Spring 来测试系统的功能);换句话说,你的应用程序应该能够使用 JUnit 或 TestNG 测试引擎进行测试,使用对象(通过简单的 new 操作符实例化)——无需 Spring 或其他任何容器。
Spring 框架提供了多个测试包,帮助创建应用程序的单元测试和集成测试。它通过提供多种模拟对象(如环境、属性源、JNDI、Servlet,以及反应式的 ServerHttpRequest 和 ServerHttpResponse 测试工具)来实现单元测试,从而帮助您在隔离环境中测试代码。
Spring 框架中最常用的测试功能之一是集成测试,其主要目标是
- 管理 Spring IoC 容器在测试执行期间的缓存
- 交易管理系统
- 测试夹具实例的依赖注入方式
- Spring 专用基础类
Spring 框架通过在测试中集成 ApplicationContext,提供了一种简单的测试方式。Spring 测试模块提供了多种使用 ApplicationContext 的方法,包括编程方式和注解方式:
- BootstrapWith:一个类级注解,用于配置 Spring TestContext 框架的启动方式。
- @ContextConfiguration:定义了类级别的元数据,以确定如何加载和配置用于集成测试的 ApplicationContext。这是您类中必不可少的注解,因为 ApplicationContext 会在这里加载所有的 bean 定义。
- @WebAppConfiguration:一个类级注解,用于声明用于集成测试的 ApplicationContext 应该是 WebApplicationContext。
- @ActiveProfile:一个类级注解,用于声明在加载 ApplicationContext 进行集成测试时,应该激活哪些 bean 定义配置文件。
- @TestPropertySource:一个类级别的注解,用于配置属性文件的位置以及要添加到为集成测试加载的 ApplicationContext 环境中的属性源集合的内联属性。
- @DirtiesContext:表示在测试执行过程中,底层的 Spring ApplicationContext 已被修改(例如,通过更改单例 bean 的状态而导致的损坏),需要关闭。
Spring 框架还提供了许多其他注解,例如@TestExecutionListeners、@Commit、@Rollback、@BeforeTransaction、@AfterTransaction、@Sql、@SqlGroup、@SqlConfig、@Timed、@Repeat、@IfProfileValue 等。
正如你所看到的,使用 Spring 框架进行测试时有很多选择。通常,你会使用 @RunWith 注解来整合所有测试框架的功能。例如,以下代码展示了如何仅使用 Spring 进行单元测试和集成测试:
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class UsersTests {
@Test
public void userPersistenceTest(){
//...
}
}
现在,让我们来探索 Spring Boot 的一系列功能,这些功能可以帮助您轻松地创建更好的单元测试、集成测试和按层的隔离测试。
Spring Boot 测试框架
Spring Boot 利用 Spring 测试框架的强大功能,通过增强和添加新的注解和特性,使开发人员的测试变得更加简单。
如果您想开始使用 Spring Boot 提供的所有测试功能,只需将 spring-boot-starter-test 依赖项添加到您的应用程序中,作用域设置为测试。如果您是通过 Spring Initializr (https://start.spring.io) 创建项目的,那么这个依赖项已经包含在内。
spring-boot-starter-test 依赖提供了多个测试框架,这些框架与所有 Spring Boot 测试功能非常兼容,包括 Junit 5、AssertJ、Hamcrest、Mockito、JSONassert、JsonPath,以及 Spring Test 和 Spring Boot Test 工具及其对 Spring Boot 应用程序的集成支持。如果您使用的是其他测试框架,它也很可能与 Spring Boot Test 模块良好兼容;您只需手动添加这些依赖项即可。
Spring Boot 提供了 @SpringBootTest 注解,简化了测试 Spring 应用程序的方式。通常,在 Spring 测试中,您需要添加多个注解来测试应用程序的特定功能或特性,但在 Spring Boot 中则不需要。@SpringBootTest 注解包含一些在测试 Web 应用程序时非常有用的参数,其中 webEnvironment 参数可以接受 RANDOM_PORT、DEFINED_PORT、MOCK 和 NONE 等值。@SpringBootTest 注解还定义了属性(您希望测试的不同值的属性)和 args(您通常在命令行中传递的参数,例如 @SpringBootTest(args = "--app。name=Users")), classes(您使用@Configuration 注解声明的类的列表)和 useMainMethod,值如 ALWAYS 和 WHEN_AVAILABLE(这表示它将使用应用程序的主方法来设置 ApplicationContext 以运行测试)。
在接下来的章节中,我将向您展示我们迄今为止开发的两个不同项目中的一些主要测试类。
在模拟环境中测试 Web 应用程序
@SpringBootTest 注解默认使用模拟环境,这意味着它不会启动服务器,已准备好测试 Web 端点。要使用此功能,我们需要结合使用 MockMvc 类和 @AutoConfigurationMockMvc 注解,后者作为测试类的标记。该注解将配置 MockMvc 类及其所有依赖项,例如过滤器和安全性(如果有的话)等。如果我们仅使用 Spring(不使用 Spring Boot),可以使用 MockMvc 类,但需要额外的步骤,因为它依赖于 WebApplicationContext。幸运的是,使用 Spring Boot,我们只需通过 @Autowired 注解将其注入即可。
让我们来看看测试文件夹中的用户应用项目,类名为 UserMockMvcTests。请参阅列表 8-1。
package com.apress.users;
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.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("mockMvc")
public class UserMockMvcTests {
@Autowired
MockMvc mockMvc;
@Test
void createUserTests() throws Exception {
String location = mockMvc.perform(post("/users")
.contentType("application/json")
.content("""
{
"email": "dummy@email.com",
"name": "Dummy",
"password": "aw2s0meR!",
"gravatarUrl": "https://www.gravatar.com/avatar/fb651279f4712e209991e05610dfb03a?d=wavatar",
"userRole": ["USER"],
"active": true
}
"""))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andReturn().getResponse().getHeader("Location");
mockMvc.perform(get(location))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").exists())
.andExpect(jsonPath("$.active").value(true));
}
@Test
void getAllUsersTests() throws Exception {
mockMvc.perform(get("/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Dummy"))
.andExpect(jsonPath("$..active").value(hasItem(true)))
.andExpect(jsonPath("$[*]").value(hasSize(1)));
}
}
8-1 src/test/java/apress/com/users/UserMockMvcTests.java
让我们来分析 UserMockMvcTests 类
- @SpringBootTest:这个注解在使用 Spring Boot 进行测试时至关重要。它提供了 SpringBootContextLoader 作为默认的上下文加载器,能够搜索所有的@Configuration 类,允许自定义环境属性,并可以注册 TestRestTemplate(我们在之前的章节中使用过)或 WebTestClient(适用于反应式应用程序,如 MongoDB)。正如我之前提到的,它还接受参数,您可以添加网络环境的类型——默认是 MOCK——以及相关的参数和属性。
- @AutoConfigureMockMvc:该注解创建了可注入的 MockMvc bean,用于执行所有服务器端测试,以及其他功能,如过滤器、安全性等。
- @ActiveProfiles:这个注解可以帮助我们只运行在指定名称下定义的 bean(在这里是 mockMvc)。当进行大量测试并需要特定行为时,这非常有用。这个注解会激活 mockMvc 配置文件。通常,在每个配置类中,您可以添加 @Profile({""}) 注解,以指定该配置文件所需的 bean。
- MockMvc:这个 Bean 类是所有服务器端测试的入口,支持请求构建器和结果匹配器,可以与 Mockito、Hamcrest 和 AssertJ 等多种测试库结合使用。
- MockMvcRequestBuilders:MockMvc 类提供了一个 perform 方法,该方法接受一个请求构建器,而 MockMvcRequestBuilders 类则提供了一个流畅的 API,允许您执行 get、post、put 等操作。这些请求构建器以路径的形式接受 URI(不需要添加完整的 URL),您可以根据需要构建请求,例如添加头部、内容类型和内容等。这是因为它返回一个 MockHttpServletRequestBuilder 类,提供了丰富的请求自定义选项。
- Hamcrest 和 MockMvcResultMatchers:在调用 MockMvc 的 perform 方法后,您可以访问 ResultsActions 接口,在这里可以对执行请求的结果添加各种期望;它包括 andExpect(ResultMatcher)、andExpectAll(ResultMatcher...)、andDo(ResultHandler) 和 andReturn() 方法。andReturn() 方法返回一个 MvcResult 接口,它也是一个流式 API,您可以获取请求返回的任何内容(如内容、头部等)。由于某些方法需要 ResultMatcher 接口,您可以使用 Hamcrest 库找到相应的实现(例如 contains、hasSize 等)。在我们的测试中,我们使用 jsonPath 结果匹配器,以便与 JSON 响应进行交互。
需要特别提到的是,这些测试需要一些额外的支持,以便能够使用这些库,我们将在接下来的部分中对此进行讨论。
使用模拟和间谍对象
Spring Boot 测试包含两个注解,可以用来模拟 Spring bean,这在这些 bean 依赖于外部服务(如第三方 REST 端点、数据库连接等)时非常有用。如果其中一个服务不可用,这些注解可以提供帮助。我们先来看看 UserMockBeanTests 类。请参见列表 8-2。
package com.apress.users;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
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.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("mockBean")
public class UserMockBeanTests {
@Autowired
private MockMvc mockMvc;
@MockBean
UserRepository userRepository;
@Test
void saveUsers() throws Exception {
var user = UserBuilder.createUser()
.withName("Dummy")
.withEmail("dummy@email.com")
.active()
.withRoles(UserRole.USER)
.withPassword("aw3s0m3R!")
.build();
when(userRepository.save(any())).thenReturn(user);
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"email": "dummy@email.com",
"name": "Dummy",
"password": "aw2s0meR!",
"gravatarUrl": "https://www.gravatar.com/avatar/fb651279f4712e209991e05610dfb03a?d=wavatar",
"userRole": ["USER"],
"active": true
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value(user.getName()))
.andExpect(jsonPath("$.email").value(user.getEmail()))
.andExpect(jsonPath("$.userRole").isArray())
.andExpect(jsonPath("$.userRole[0]").value("USER"));
verify(userRepository, times(1)).save(Mockito.any(User.class));
}
}
8-2 src/test/java/apress/com/users/UserMockBeanTests.java
UserMockBeanTests 类包含以下内容:
- @MockBean:这个注解用于模拟标记的对象,使您能够为该对象添加所需的行为和结果。它替换了对象的实现,因此可以轻松进行自定义。要使用这个 bean,您需要使用像 Mockito 这样的框架。在我们在列表 8-2 中的测试中,我们模拟了 when() UserRepository,这意味着我们不需要任何数据库连接或相关内容;基本上,我们是在模拟它的行为。
- Mockito.*:Mockito 库帮助我们为模拟对象(在本例中为 UserRepository)添加行为,使用 thenReturn()方法。然后,我们可以在调用后使用 verify()方法来验证行为。请注意,Mockito 库提供了许多有用的流畅 API。在我们的测试中,我们指定当 UserRepository 调用 save 方法(无论传入什么值)时,它将返回我们指定的用户,最后我们验证在保存 User 对象时我们的模拟对象被调用了一次。
请记住,@MockBean 会将实际的 bean 替换为一个易于修改的模拟实现,以便获得所需的行为。
接下来,我们来看看 UserSpyBeanTests 类,见清单 8-3。
package com.apress.users;
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.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.List;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
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;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("spyBean")
public class UserSpyBeanTests {
@Autowired
private MockMvc mockMvc;
@SpyBean
private UserRepository userRepository;
@Test
public void testGetAllUsers() throws Exception {
List<User> mockUsers = Arrays.asList(
UserBuilder.createUser()
.withName("Ximena")
.withEmail("ximena@email.com")
.build(),
UserBuilder.createUser()
.withName("Norma")
.withEmail("norma@email.com")
.build()
);
doReturn(mockUsers).when(userRepository).findAll();
mockMvc.perform(get("/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Ximena"))
.andExpect(jsonPath("$[1].name").value("Norma"));
verify(userRepository).findAll();
}
}
8-3 src/test/java/apress/com/users/UserSpyBeanTests.java
让我们来分析 UserSpyBeanTests 类:
- @SpyBean:这个注解用于创建一个间谍 bean。间谍 bean 类似于模拟对象,但它保留了真实 bean 的原始行为。它允许你拦截和验证方法调用,并为特定方法指定特定行为,同时保持其他方法不变。在我们列表 8-3 的示例中,我们使用 Mockito 库,在使用 UserRepository 实例时调用 doReturn() 方法,并在调用 findAll() 方法时使用它。最后,我们验证实际的 findAll() 方法是否被调用。