Spring Boot单元和集成测试概述 | rieckpil


单元和集成测试是您作为开发人员日常生活不可或缺的一部分。特别是对于Spring Boot而言,新手为他们的应用程序编写有意义的测试是一个障碍:

  • 从哪里开始我的测试工作?
  • Spring Boot如何帮助我编写高效的测试?
  • 我应该使用哪些库?

通过此博客,您将获得有关单元引导和集成测试如何与Spring Boot一起工作的概述。最重要的是,您将学习Spring首先要关注的功能和库。本文充当聚合器,在许多地方,您都可以找到其他文章和指南的链接,这些文章和指南对这些概念进行了更详细的说明。
 
使用Spring Boot进行单元测试
单元测试为您的测试策略奠定了基础。您使用Spring Initializr引导的每个Spring Boot项目都具有编写单元测试的坚实基础。几乎没有什么可设置的,因为Spring Boot Starter Test包含所有必要的构建基块。
除了包含和管理Spring Test的版本外,此Spring Boot Starter包括并管理以下库的版本:
  • JUnit 4/5
  • Mockito
  • 断言库,如AssertJ,Hamcrest,JsonPath等。

大多数时候,您的单元测试不需要任何特定的Spring Boot或Spring Test功能,因为它们仅依赖JUnit和Mockito。
使用单元测试,您可以单独测试*Service类,例如模拟Mock要测试的类的每个协作者:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
 
import java.math.BigDecimal;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
 
@ExtendWith(MockitoExtension.class) // register the Mockito extension
public class PricingServiceTest {
 
  @Mock
// // Instruct Mockito to mock this object
  private ProductVerifier mockedProductVerifier;
 
  @Test
  public void shouldReturnCheapPriceWhenProductIsInStockOfCompetitor() {
    when(mockedProductVerifier.isCurrentlyInStockOfCompetitor(
"AirPods"))
      .thenReturn(true);
//Specify what boolean value to return
 
    PricingService cut = new PricingService(mockedProductVerifier);
 
    assertEquals(new BigDecimal(
"99.99"), cut.calculatePrice("AirPods"));
  }
}

从上面import的测试类的部分可以看到,Spring根本没有import进来。因此,您可以应用对任何其他Java应用程序进行单元测试的技术和知识。
因此,重要的是要学习JUnit 4/5和Mockito的基础知识,以充分利用您的单元测试。
对于应用程序的某些部分,单元测试不会带来很多好处。持久层或测试HTTP客户端就是一个很好的例子。测试应用程序的这些部分,最终将几乎复制您的实现,因为您必须模拟与其他类的大量交互。
这里更好的方法是使用切片的Spring Context,您可以使用Spring Boot测试注释轻松地自动配置它。
 
使用切片的Spring上下文进行测试
在传统的单元测试之上,您可以使用Spring Boot编写针对应用程序特定部分(切片)的测试。SpringTestContext框架和Spring Boot一起将为Spring Context量身定制具有足够用于特定测试的组件的Spring Context。
这些测试的目的是在不启动整个应用程序的情况下单独测试应用程序的特定部分。这样既可以缩短测试执行时间,又可以减少对大量测试设置的需求。
如何命名此类测试?我认为,它们在单元测试或集成测试类别中均不会下降100%。一些开发人员将它们称为单元测试,因为它们例如独立测试一个控制器。其他开发人员将它们归类为集成测试,因为涉及到Spring支持。无论您如何命名,至少在团队中都要确保有一致的理解。
Spring Boot提供了大量的注解来测试您的应用程序的不同部分隔离:@JsonTest,@WebMvcTest,@DataMongoTest,@JdbcTest,等。
它们全部自动配置切片的Spring,TestContext并且仅包含与测试应用程序的特定部分相关的Spring Bean。我在整篇文章中都专门介绍了这些注释中最常见的注释,并解释了它们的用法。
两个最重要的注释(考虑首先学习它们)是:


还有一些注释可用于您应用程序的更多细分部分:

您始终可以通过以下方式显式地导入组件@Import或定义其他Spring Bean来丰富测试的自动配置上下文@TestConfiguration:
@WebMvcTest(PublicController.class)
class PublicControllerTest {
 
  @Autowired
  private MockMvc mockMvc;
 
  @Autowired
  private MeterRegistry meterRegistry;
 
  @MockBean
  private UserService userService;
 
  @TestConfiguration
  static class TestConfig {
 
    @Bean
    public MeterRegistry meterRegistry() {
      return new SimpleMeterRegistry();
    }
 
  } 
}

 
JUnit 4与JUnit 5陷阱
在回答Stack Overflow上的问题时,我经常遇到的一个大陷阱是同一测试中JUnit 4和JUnit 5(更具体地讲,JUnit Jupiter)的混合。在同一测试类中使用不同JUnit版本的API会导致意外的输出和失败。
重要的是要注意导入import,尤其是@Test注释:
// JUnit 4
import org.junit.Test;
 
// JUnit Jupiter (part of JUnit 5)
import org.junit.jupiter.api.Test;

对于JUnit 4的其他指标有:@RunWith,@Rule,@ClassRule,@Before,@BeforeClass,@After,@AfterClass。
为了避免意外混用不同的JUnit版本,从项目中排除它们有助于始终选择正确的导入:
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
    <exclusion>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
  </exclusions>
</dependency>

除了Spring Boot Starter Test之外,其他测试依赖项还可能包括旧版本的JUnit:
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>${testcontainers.version}</version>
  <exclusions>
    <exclusion>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </exclusion>
  </exclusions>
</dependency>

为了避免将来包含任何(偶然的)JUnit 4依赖关系,可以使用Maven Enforcer插件并将其定义为禁止的依赖关系。一旦有人包含一个新的测试依赖关系,该依赖关系会暂时拉动JUnit 4,这将使构建失败。
请注意,从Spring Boot 2.4.0开始,Spring Boot Starter Test依赖项vintage-engine默认不再包含。
 
使用Spring Boot进行集成测试
使用集成测试,通常可以组合测试应用程序的多个组件。在大多数情况下,您将为此使用@SpringBootTest注释,并使用或从外部访问您的应用程序。
@SpringBootTest将为您的测试填充整个应用程序上下文。使用时,了解它的webEnvironment属性很重要。如果不指定此属性,则此类测试将不会启动嵌入式Servlet容器(例如Tomcat),而是使用模拟的Servlet环境。因此,您的应用程序将无法在本地端口访问。
您可以通过指定DEFINE_PORT或来覆盖此行为RANDOM_PORT:

// or DEFINED_PORT
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)

对于启动嵌入式Servlet容器的集成测试,您可以注入应用程序的端口并使用TestRestTemplateWebTestClient或从外部访问它:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ApplicationTests {
 
  @LocalServerPort
  private Integer port;
 
  @Autowired
  private TestRestTemplate testRestTemplate;
 
  @Test
  void accessApplication() {
    System.out.println(port);
  }
}

由于SpringTestContext框架将填充整个应用程序上下文,因此您必须确保存在所有依赖的基础结构组件(例如,数据库,消息传递队列等)。
这就是Testcontainer发挥作用的地方。测试容器将为您的测试管理任何Docker容器的生命周期:
@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationIT {
 
  @Container
  public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
    .withPassword("inmemory")
    .withUsername(
"inmemory");
 
  @DynamicPropertySource
  static void postgresqlProperties(DynamicPropertyRegistry registry) {
    registry.add(
"spring.datasource.url", postgreSQLContainer::getJdbcUrl);
    registry.add(
"spring.datasource.password", postgreSQLContainer::getPassword);
    registry.add(
"spring.datasource.username", postgreSQLContainer::getUsername);
  }
 
  @Test
  public void contextLoads() {
  }
 
}

一旦您的应用程序与其他系统通信,您就需要一个解决方案来模拟HTTP通信。这是很常见的情况,例如在应用程序启动时从远程REST API或OAuth2访问令牌中获取数据。借助WireMock,您可以存根并准备HTTP响应以模拟远程系统的存在。
此外,SpringTestContext框架具有一项巧妙的功能,可以缓存和重用以及已经启动的上下文。这可以帮助减少构建时间并大大改善您的反馈周期。
 
使用Spring Boot进行端到端测试
端到端(E2E)测试的目的是从用户的角度验证系统。这包括针对主要用户旅程的测试(例如,下订单或创建新客户)。与集成测试相比,此类测试通常涉及用户界面(如果有的话)。
您还可以在继续进行生产部署之前,针对dev或staging环境上的应用程序的已部署版本执行E2E测试。
对于使用服务器端渲染(例如Thymeleaf)或自包含系统方法(由Spring Boot后端提供前端)的应用程序,您可以使用@SpringBootTest这些测试。
一旦需要与浏览器进行交互,Selenium通常是默认选择。如果您已经与Selenium合作了一段时间,您可能会发现自己一遍又一遍地实现相同的帮助器功能。为了获得更好的开发人员体验并减少编写涉及浏览器交互的测试时的头痛,请考虑使用Selenide。Selenide是Selenium低级API之上的抽象,用于编写稳定而简洁的浏览器测试。
以下测试展示了如何使用Selenide访问和测试Spring Boot应用程序的公共页面:
@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class BookStoreTestcontainersWT {
 
  @LocalServerPort
  private Integer port;
 
  @Test
  public void shouldDisplayBook() {
 
    Configuration.timeout = 2000;
    Configuration.baseUrl = "http://localhost:" + port;
 
    open(
"/book-store");
 
    $(By.id(
"all-books")).shouldNot(Condition.exist);
    $(By.id(
"fetch-books")).click();
    $(By.id(
"all-books")).shouldBe(Condition.visible);
  }
}

对于您需要启动E2E测试的基础架构组件,Testcontainers再次扮演着重要的角色。如果您必须启动多个Docker容器,则TestcontainersDocker Compose模块会派上用场:
public static DockerComposeContainer<?> environment =
  new DockerComposeContainer<>(new File("docker-compose.yml"))
    .withExposedService(
"database_1", 5432, Wait.forListeningPort())
    .withExposedService(
"keycloak_1", 8080, Wait.forHttp("/auth").forStatusCode(200)
      .withStartupTimeout(Duration.ofSeconds(30)))
    .withExposedService(
"sqs_1", 9324, Wait.forListeningPort());

概括
Spring Boot为单元测试和集成测试提供了出色的支持。由于每个Spring Boot项目都包括Spring Boot Starter Test,因此它使测试成为一等公民。该入门程序为您提供了具有基本测试库的基本测试工具箱。
最重要的是,Spring Boot测试注释使对应用程序不同部分的编写测试变得轻而易举。您将获得TestContext仅包含相关Spring Bean的量身定制的Spring。
要熟悉Spring Boot项目的单元和集成测试,请考虑以下步骤: