使用 Spring Boot 和 @SpringBootTest 进行测试
【注】本文译自: Testing with Spring Boot and @SpringBootTest - Reflectoring
使用@SpringBootTest 注解,Spring Boot 提供了一种方便的方法来启动要在测试中使用的应用程序上下文。在本教程中,我们将讨论何时使用 @SpringBootTest 以及何时更好地使用其他工具进行测试。我们还将研究自定义应用程序上下文的不同方法以及如何减少测试运行时间。
代码示例
本文附有 GitHub 上 的工作代码示例。
“使用 Spring Boot 进行测试”系列
本教程是系列的一部分:
- 使用 Spring Boot 进行单元测试
- 使用 Spring Boot 和 @WebMvcTest 测试 MVC Web Controller
- 使用 Spring Boot 和 @DataJpaTest 测试 JPA 查询
-
使用 Spring Boot 和
@SpringBootTest
进行测试
集成测试与单元测试
在开始使用 Spring Boot 进行集成测试之前,让我们定义集成测试与单元测试的区别。
单元测试涵盖单个“单元”,其中一个单元通常是单个类,但也可以是组合测试的一组内聚类。
集成测试可以是以下任何一项:
- 涵盖多个“单元”的测试。它测试两个或多个内聚类集群之间的交互。
- 覆盖多个层的测试。这实际上是第一种情况的特化,例如可能涵盖业务服务和持久层之间的交互。
- 涵盖整个应用程序路径的测试。在这些测试中,我们向应用程序发送请求并检查它是否正确响应并根据我们的预期更改了数据库状态。
Spring Boot 提供了 @SpringBootTest 注解,我们可以使用它来创建一个应用程序上下文,其中包含我们对上述所有测试类型所需的所有对象。但是请注意,过度使用 @SpringBootTest 可能会导致测试套件运行时间非常长。
因此,对于涵盖多个单元的简单测试,我们应该创建简单的测试,与单元测试非常相似,在单元测试中,我们手动创建测试所需的对象图并模拟其余部分。这样,Spring 不会在每次测试开始时启动整个应用程序上下文。
测试切片
我们可以将我们的 Spring Boot 应用程序作为一个整体来测试、一个单元一个单元地测试、也可以一层一层地测试。使用 Spring Boot 的测试切片注解,我们可以分别测试每一层。
在我们详细研究 @SpringBootTest 注解之前,让我们探索一下测试切片注解,以检查 @SpringBootTest 是否真的是您想要的。
@SpringBootTest 注解加载完整的 Spring 应用程序上下文。相比之下,测试切片注释仅加载测试特定层所需的 bean。正因为如此,我们可以避免不必要的模拟和副作用。
@WebMvcTest
我们的 Web 控制器承担许多职责,例如侦听 HTTP 请求、验证输入、调用业务逻辑、序列化输出以及将异常转换为正确的响应。我们应该编写测试来验证所有这些功能。
@WebMvcTest 测试切片注释将使用刚好足够的组件和配置来设置我们的应用程序上下文,以测试我们的 Web 控制器层。例如,它将设置我们的@Controller、@ControllerAdvice、一个 MockMvc bean 和其他一些自动配置。
@WebFluxTest
@WebFluxTest 用于测试 WebFlux 控制器。@WebFluxTest 的工作方式类似于 @WebMvcTest 注释,不同之处在于它不是 Web MVC 组件和配置,而是启动 WebFlux 组件和配置。其中一个 bean 是 WebTestClient,我们可以使用它来测试我们的 WebFlux 端点。
@DataJpaTest
就像@WebMvcTest 允许我们测试我们的 web 层一样,@DataJpaTest 用于测试持久层。
它配置我们的实体、存储库并设置嵌入式数据库。现在,这一切都很好,但是,测试我们的持久层意味着什么? 我们究竟在测试什么? 如果查询,那么什么样的查询?要找出所有这些问题的答案,请阅读我关于使用 Spring Boot 和 @DataJpaTest 测试 JPA 查询的文章。
@DataJdbcTest
Spring Data JDBC 是 Spring Data 系列的另一个成员。 如果我们正在使用这个项目并且想要测试持久层,那么我们可以使用 @DataJdbcTest 注解 。@DataJdbcTest 会自动为我们配置在我们的项目中定义的嵌入式测试数据库和 JDBC 存储库。
另一个类似的项目是 Spring JDBC,它为我们提供了 JdbcTemplate 对象来执行直接查询。@JdbcTest 注解自动配置测试我们的 JDBC 查询所需的 DataSource 对象。
依赖
本文中的代码示例只需要依赖 Spring Boot 的 test starter 和 JUnit Jupiter:
dependencies {
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
}
使用 @SpringBootTest创建 ApplicationContext
@SpringBootTest 在默认情况下开始在测试类的当前包中搜索,然后在包结构中向上搜索,寻找用 @SpringBootConfiguration 注解的类,然后从中读取配置以创建应用程序上下文。这个类通常是我们的主要应用程序类,因为 @SpringBootApplication 注解包括 @SpringBootConfiguration 注解。然后,它会创建一个与在生产环境中启动的应用程序上下文非常相似的应用程序上下文。
我们可以通过许多不同的方式自定义此应用程序上下文,如下一节所述。
因为我们有一个完整的应用程序上下文,包括 web 控制器、Spring 数据存储库和数据源,@SpringBootTest 对于贯穿应用程序所有层的集成测试非常方便:
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@Test
void registrationWorksThroughAllLayers() throws Exception {
UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
mockMvc.perform(post("/forums/{forumId}/register", 42L)
.contentType("application/json")
.param("sendWelcomeMail", "true")
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk());
UserEntity userEntity = userRepository.findByName("Zaphod");
assertThat(userEntity.getEmail()).isEqualTo("zaphod@galaxy.net");
}
@ExtendWith
本教程中的代码示例使用 @ExtendWithh 注解告诉 JUnit 5 启用 Spring 支持。从 Spring Boot 2.1 开始,我们不再需要加载 SpringExtension,因为它作为元注释包含在 Spring Boot 测试注释中,例如 @DataJpaTest、@WebMvcTest 和 @SpringBootTest。
在这里,我们另外使用 @AutoConfigureMockMvc 将 MockMvc 实例添加到应用程序上下文中。
我们使用这个 MockMvc 对象向我们的应用程序执行 POST 请求并验证它是否按预期响应。
然后,我们使用应用程序上下文中的 UserRepository 来验证请求是否导致数据库状态发生预期的变化。
自定义应用程序上下文
我们可以有很多种方法来自定义 @SpringBootTest 创建的应用程序上下文。让我们看看我们有哪些选择。
自定义应用上下文时的注意事项
应用程序上下文的每个自定义都是使其与在生产设置中启动的“真实”应用程序上下文不同的另一件事。因此,为了使我们的测试尽可能接近生产, 我们应该只定制让测试运行真正需要的东西!
添加自动配置
在上面,我们已经看到了自动配置的作用:
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
}
:还有很多其他可用的自动配置,每个都可以将其他 bean 添加到应用程序上下文中。以下是文档中其他一些有用的内容:
- @AutoConfigureWebTestClient:Adds WebTestClient to the test application context. It allows us to test server endpoints.将 WebTestClient 添加到测试应用程序上下文。它允许我们测试服务器端点。
- @AutoConfigureTestDatabase:这允许我们针对真实数据库而不是嵌入式数据库运行测试。
- @RestClientTest:当我们想要测试我们的 RestTemplate 时它会派上用场。 它自动配置所需的组件以及一个 MockRestServiceServer 对象,该对象帮助我们模拟来自 RestTemplate 调用的请求的响应。
- @JsonTest:自动配置 JSON 映射器和类,例如 JacksonTester 或 GsonTester。使用这些我们可以验证我们的 JSON 序列化/反序列化是否正常工作。
设置自定义配置属性
通常,在测试中需要将一些配置属性设置为与生产设置中的值不同的值:
@SpringBootTest(properties = "foo=bar")
class SpringBootPropertiesTest {
@Value("${foo}")
String foo;
@Test
void test(){
assertThat(foo).isEqualTo("bar");
}
如果属性 foo 存在于默认设置中,它将被此测试的值 bar 覆盖。
使用 @ActiveProfiles 外部化属性
如果我们的许多测试需要相同的属性集,我们可以创建一个配置文件 application-<profile>.propertie 或 application-<profile>.yml 并通过激活某个配置文件从该文件加载属性:
# application-test.yml foo: bar
@SpringBootTest
@ActiveProfiles("test")
class SpringBootProfileTest {
@Value("${foo}")
String foo;
@Test
void test(){
assertThat(foo).isEqualTo("bar");
}
使用 @TestPropertySource设置自定义属性
另一种定制整个属性集的方法是使用 @TestPropertySource 注释:
# src/test/resources/foo.properties
foo=bar
@SpringBootTest
@TestPropertySource(locations = "/foo.properties")
class SpringBootPropertySourceTest {
@Value("${foo}")
String foo;
@Test
void test(){
assertThat(foo).isEqualTo("bar");
}
foo.properties 文件中的所有属性都加载到应用程序上下文中。@TestPropertySource 还可以 配置更多。
使用 @MockBean 注入模拟
如果我们只想测试应用程序的某个部分而不是从传入请求到数据库的整个路径,我们可以使用 @MockBean 替换应用程序上下文中的某些 bean:
@SpringBootTest
class MockBeanTest {
@MockBean
private UserRepository userRepository;
@Autowired
private RegisterUseCase registerUseCase;
@Test
void testRegister(){
// given
User user = new User("Zaphod", "zaphod@galaxy.net");
boolean sendWelcomeMail = true;
given(userRepository.save(any(UserEntity.class))).willReturn(userEntity(1L));
// when
Long userId = registerUseCase.registerUser(user, sendWelcomeMail);
// then
assertThat(userId).isEqualTo(1L);
}
在这种情况下,我们用模拟替换了 UserRepository bean。 使用 Mockito 的 given 方法,我们指定了此模拟的预期行为,以测试使用此存储库的类。
您可以在我关于模拟的文章中阅读有关 @MockBean 注解的更多信息。
使用 @Import添加 Bean
如果某些 bean 未包含在默认应用程序上下文中,但我们在测试中需要它们,我们可以使用 @Import 注解导入它们:
package other.namespace;@Componentpublic class Foo {}
@SpringBootTest
@Import(other.namespace.Foo.class)
class SpringBootImportTest {
@Autowired
Foo foo;
@Test
void test() {
assertThat(foo).isNotNull();
}
默认情况下,Spring Boot 应用程序包含它在其包和子包中找到的所有组件,因此通常只有在我们想要包含其他包中的 bean 时才需要这样做。
使用 @TestConfiguration覆盖 Bean
使用 @TestConfiguration,我们不仅可以包含测试所需的其他 bean,还可以覆盖应用程序中已经定义的 bean。在我们关于使用 @TestConfiguration 进行测试 的文章中阅读更多相关信息。
创建自定义@SpringBootApplication
我们甚至可以创建一个完整的自定义 Spring Boot 应用程序来启动测试。如果这个应用程序类与真正的应用程序类在同一个包中,但是在测试源而不是生产源中,@SpringBootTest 会在实际应用程序类之前找到它,并从这个应用程序加载应用程序上下文。
或者,我们可以告诉 Spring Boot 使用哪个应用程序类来创建应用程序上下文:
@SpringBootTest(classes = CustomApplication.class)
class CustomApplicationTest {
}
但是,在执行此操作时, 我们正在测试可能与生产环境完全不同的应用程序上下文 ,因此仅当无法在测试环境中启动生产应用程序时,这才应该是最后的手段。但是,通常有更好的方法,例如使真实的应用程序上下文可配置以排除不会在测试环境中启动的 bean。让我们看一个例子。
假设我们在应用程序类上使用 @EnableSchedulin 注解。每次启动应用程序上下文时(即使在测试中),所有 @Scheduled 作业都将启动,并且可能与我们的测试冲突。 我们通常不希望作业在测试中运行,因此我们可以创建第二个没有 @EnabledScheduling 注释的应用程序类,并在测试中使用它。 但是,更好的解决方案是创建一个可以使用属性切换的配置类:
@Configuration
@EnableScheduling
@ConditionalOnProperty(
name = "io.reflectoring.scheduling.enabled",
havingValue = "true",
matchIfMissing = true)
public class SchedulingConfiguration {
}
我们已将 @EnableScheduling 注解从我们的应用程序类移到这个特殊的配置类。将属性
io.reflectoring.scheduling.enabled 设置为 false 将导致此类不会作为应用程序上下文的一部分加载:
@SpringBootTest(properties = "io.reflectoring.scheduling.enabled=false")
class SchedulingTest {
@Autowired(required = false)
private SchedulingConfiguration schedulingConfiguration;