使用 Spring Boot 和 @SpringBootTest 进行测试

【注】本文译自: Testing with Spring Boot and @SpringBootTest - Reflectoring

使用 Spring Boot 和 @SpringBootTest 进行测试

使用@SpringBootTest 注解,Spring Boot 提供了一种方便的方法来启动要在测试中使用的应用程序上下文。在本教程中,我们将讨论何时使用 @SpringBootTest 以及何时更好地使用其他工具进行测试。我们还将研究自定义应用程序上下文的不同方法以及如何减少测试运行时间。

 代码示例

本文附有 GitHub 上的工作代码示例。

“使用 Spring Boot 进行测试”系列

本教程是系列的一部分:

  1. 使用 Spring Boot 进行单元测试
  2. 使用 Spring Boot 和 @WebMvcTest 测试 MVC Web Controller
  3. 使用 Spring Boot 和 @DataJpaTest 测试 JPA 查询
  4. 使用 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 和其他一些自动配置
要阅读有关 @WebMvcTest 的更多信息并了解我们如何验证每个职责,请阅读我关于使用 Spring Boot 和 @WebMvcTest 测试 MVC Web 控制器的文章

@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
本教程中的代码示例使用 @ExtendWith 注解告诉 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:将 WebTestClient 添加到测试应用程序上下文。它允许我们测试服务器端点。
  • @AutoConfigureTestDatabase:这允许我们针对真实数据库而不是嵌入式数据库运行测试。
  • @RestClientTest:当我们想要测试我们的 RestTemplate 时它会派上用场。 它自动配置所需的组件以及一个 MockRestServiceServer 对象,该对象帮助我们模拟来自 RestTemplate 调用的请求的响应。
  • @JsonTest:自动配置 JSON 映射器和类,例如 JacksonTesterGsonTester。使用这些我们可以验证我们的 JSON 序列化/反序列化是否正常工作。

设置自定义配置属性

通常,在测试中需要将一些配置属性设置为与生产设置中的值不同的值:

@SpringBootTest(properties = "foo=bar")
class SpringBootPropertiesTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

如果属性 foo 存在于默认设置中,它将被此测试的值 bar 覆盖。

使用 @ActiveProfiles 外部化属性

如果我们的许多测试需要相同的属性集,我们可以创建一个配置文件 application-<profile>.propertieapplication-<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。使用 Mockitogiven 方法,我们指定了此模拟的预期行为,以测试使用此存储库的类。
您可以在我关于模拟的文章中阅读有关 @MockBean 注解的更多信息。

使用 @Import 添加 Bean

如果某些 bean 未包含在默认应用程序上下文中,但我们在测试中需要它们,我们可以使用 @Import 注解导入它们:

package other.namespace;

@Component
public 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。让我们看一个例子。
假设我们在应用程序类上使用 @EnableScheduling 注解。每次启动应用程序上下文时(即使在测试中),所有 @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;

  @Test
  void test() {
    assertThat(schedulingConfiguration).isNull();
  }
}

我们现在已经成功地停用了测试中的预定作业。属性 io.reflectoring.scheduling.enabled 可以通过上述任何方式指定。

为什么我的集成测试这么慢?

包含大量 @SpringBootTest 注释测试的代码库可能需要相当长的时间才能运行。Spring 的测试支持 足够智能,只创建一次应用上下文并在后续测试中重复使用,但是如果不同的测试需要不同的应用上下文,它仍然会为每个测试创建一个单独的上下文,这需要一些时间来完成每个测试。
上面描述的所有自定义选项都会导致 Spring 创建一个新的应用程序上下文。因此,我们可能希望创建一个配置并将其用于所有测试,以便可以重用应用程序上下文。
如果您对测试花费在设置和 Spring 应用程序上下文上的时间感兴趣,您可能需要查看 JUnit Insights,它可以包含在 Gradle 或 Maven 构建中,以生成关于 JUnit 5 如何花费时间的很好的报告。

结论

@SpringBootTest 是一种为测试设置应用程序上下文的非常方便的方法,它非常接近我们将在生产中使用的上下文。有很多选项可以自定义此应用程序上下文,但应谨慎使用它们,因为我们希望我们的测试尽可能接近生产运行。
如果我们想在整个应用程序中进行测试,@SpringBootTest 会带来最大的价值。为了仅测试应用程序的某些切片或层,我们还有其他选项可用。
本文中使用的示例代码可在 github 上找到。

上一篇:Photoshop 美女照片转成清纯的仿手绘效果


下一篇:Spring项目中的测试注解@ContextConfiguration和@SpringBootTest使用区别