优秀的编程知识分享平台

网站首页 > 技术文章 正文

精通Spring Boot 3 : 8. Spring Boot 测试 (2)

nanyue 2025-01-09 15:09:47 技术文章 2 ℃

测试容器技术

Testcontainers (https://testcontainers.com/) 是一个开源框架,允许您通过单元测试框架在 Docker 容器中运行某些服务。Spring Boot 现在可以轻松使用 Testcontainers,无需任何配置;您只需在 Maven 或 Gradle 构建文件中添加两个依赖项:org.springframework.boot:spring-boot-testcontainers 和 org.testcontainers:junit-jupiter(Maven 的范围为 test,Gradle 的为 testImplementation)。

之前的章节展示了 spring-boot-docker-compose 功能的使用,该功能允许开发者读取提供的 docker-compose.yaml 文件,并通过创建应用程序所需的环境来运行应用程序。请注意,此功能仅在运行应用程序时有效,而在执行测试时无效。因此,解决方案是 Testcontainers!

通过 Testcontainers,Spring Boot 引入了以下注解:

  • @Testcontainers:负责启动和停止容器;该注解会查找所有定义的 @Container 注解。
  • @Container: 配置所有测试容器框架初始化所需的设置。
  • @ServiceConnection:负责为您的应用程序创建默认的连接信息。在这种情况下,具体取决于您将使用的技术。例如,在本章中,我们为用户应用程序项目使用 Postgres,而为我的复古应用程序项目使用 MongoDB,因此需要包含相应的 Testcontainers 依赖项,分别是 org.testcontainers:postgresql 或 org.testcontainers:mongodb。

你会发现,在 Spring Boot 中使用 Testcontainers 时,通常需要一些时间来下载镜像(如果镜像不存在)并开始测试。为了解决这个问题,Spring Boot 团队创建了@RestartScope 注解,允许你在使用 Spring Boot Dev Tools 时,当应用程序重新启动时重新创建容器(保持容器运行)。Dev Tools 的一个主要特点是你可以在自己喜欢的 IDE 中使用它,并且在每次保存新文件时,它会自动重新启动应用程序。没有 Dev Tools 的情况下,通常的步骤是修改应用程序、保存,然后停止或重新启动应用程序;而使用 Spring Boot Dev Tools 时,你无需这样做,因为它会在文件保存时自动重新启动应用程序。

所以,让我们来看看如何在 Spring Boot 中使用 Testcontainers。下面的代码片段展示了如何使用 @Testcontainers、@Container 和 @ServiceConnection 注解,以及如何启用 PostgreSQL 容器:

@SpringBootTest
@Testcontainers
public class UserTests {
    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgreSQLContainer =
       new PostgreSQLContainer<>("postgres:latest");
    // Your test here ...
}

在接下来的章节中,我们将把这视为测试的一部分。

Spring Boot 测试模块

  • @WebMvcTest
  • 目的:在独立环境中测试 Spring MVC 控制器。
  • 包括:Web 层的组件(如控制器、过滤器、视图解析器等)
  • 不包括服务层、仓库层及其他非网络组件。
  • 适合用于测试控制器的行为、请求映射、验证以及视图的渲染(如适用)。
  • @DataJpaTest
  • 目的:在独立环境中测试 Spring Data JPA 仓库。
  • 包括 JPA 仓库、实体类及相关配置。
  • 不包括服务层、Web 层及其他非 JPA 组件。
  • 适用范围:测试仓库的 CRUD 操作、查询以及自定义仓库方法。
  • @JdbcTest
  • 目的:测试不使用 Spring Data JPA 的普通 JDBC 数据访问代码。
  • 包括数据源配置、JDBC 模板和 SQL 脚本。
  • 不包括:JPA 仓库、Web 层及其他非 JDBC 组件。
  • 适用场景:测试低级 JDBC 操作、SQL 脚本以及不使用 JPA 的数据访问逻辑。
  • @JsonTest
  • 目的:测试 JSON 的序列化与反序列化。
  • 包括:Jackson 或 Gson 的配置以及自定义的序列化和反序列化器。
  • 不包括:Web 层、数据访问层及其他非 JSON 组件。
  • 适用场景:验证您的对象是否能够正确地序列化和反序列化为 JSON。
  • @RestClientTest
  • 目的:测试 Spring 的 RestTemplate 或 WebClient 的 REST 客户端。
  • 包括:REST 客户端组件、错误处理配置及相关模块。
  • 不包括:Web 层(服务器端)、数据访问层以及其他非 REST 客户端组件。
  • 适用于验证 REST 客户端配置、构建请求、处理响应和错误场景。
  • @DataMongoTest
  • 目的:在独立环境中测试 Spring Data MongoDB 仓库。
  • 包括 MongoDB 仓库、实体类及相关配置。
  • 不包括:Web 层、JPA 组件及其他非 MongoDB 组件。
  • 适用场景:测试 MongoDB 的存储库操作、查询以及自定义存储库方法。
  • @SpringBootTest(特例)
  • 目的:虽然不完全是一个“切片”,但它提供了一种测试整个应用程序上下文的方式。
  • 包含:所有 Spring Bean 及其配置。
  • 适合进行端到端测试或集成测试,这些测试需要完整的应用程序上下文。

让我们先从@JsonTest 注解入手。

@JsonTest

@JsonTest 是一个注解,允许您测试 JSON 的序列化和反序列化。该注解还会自动配置合适的映射器支持,如果在您的类路径中找到以下任一库:Jackson、Gson 或 Jsonb。

列表 8-4 展示了 UserJsonTest 类。

package com.apress.users;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.assertThrows;
@JsonTest
public class UserJsonTests {
    @Autowired
    private JacksonTester<User> jacksonTester;
    @Test
    void serializeUserJsonTest() throws IOException{
        User user = UserBuilder.createUser(Validation.buildDefaultValidatorFactory().getValidator())
                .withEmail("dummy@email.com")
                .withPassword("aw2s0me")
                .withName("Dummy")
                .withRoles(UserRole.USER)
                .active().build();
        JsonContent<User> json =  jacksonTester.write(user);
        assertThat(json).extractingJsonPathValue("$.email").isEqualTo("dummy@email.com");
        assertThat(json).extractingJsonPathArrayValue("$.userRole").size().isEqualTo(1);
        assertThat(json).extractingJsonPathBooleanValue("$.active").isTrue();
        assertThat(json).extractingJsonPathValue("$.gravatarUrl").isNotNull();
        assertThat(json).extractingJsonPathValue("$.gravatarUrl").isEqualTo(UserGravatar.getGravatarUrlFromEmail(user.getEmail()));
    }
    @Test
    void serializeUserJsonFileTest() throws IOException{
        User user = UserBuilder.createUser(Validation.buildDefaultValidatorFactory().getValidator())
                .withEmail("dummy@email.com")
                .withPassword("aw2s0me")
                .withName("Dummy")
                .withRoles(UserRole.USER)
                .active().build();
        System.out.println(user);
        JsonContent<User> json =  jacksonTester.write(user);
                     assertThat(json).isEqualToJson("user.json");
    }
    @Test
    void deserializeUserJsonTest() throws Exception{
        String userJson = """
                {
                  "email": "dummy@email.com",
                  "name": "Dummy",
                  "password": "aw2s0me",
                  "userRole": ["USER"],
                  "active": true
                }
                """;
        User user = this.jacksonTester.parseObject(userJson);
        assertThat(user.getEmail()).isEqualTo("dummy@email.com");
        assertThat(user.getPassword()).isEqualTo("aw2s0me");
        assertThat(user.isActive()).isTrue();
    }
    @Test
    void userValidationTest(){
        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy( () -> UserBuilder.createUser(Validation.buildDefaultValidatorFactory().getValidator())
                        .withEmail("dummy@email.com")
                        .withName("Dummy")
                        .withRoles(UserRole.USER)
                        .active().build());
        // Junit 5
        Exception exception = assertThrows(ConstraintViolationException.class, () -> {
            UserBuilder.createUser(Validation.buildDefaultValidatorFactory().getValidator())
                    .withName("Dummy")
                    .withRoles(UserRole.USER)
                    .active().build();
        });
        String expectedMessage = "email: Email can not be empty";
        assertThat(exception.getMessage()).contains(expectedMessage);
    }
}

8-4 src/test/java/apress/com/users/UserJsonTest.java

UserJsonTest 类包含以下内容:

  • 该注解将此类标记为仅进行 JSON 序列化。它会根据类路径中所使用的依赖库(无论是 Jackson(默认与 spring-boot-starter-web 一起提供)、Jsonb 还是 Gson)自动配置测试。
  • JacksonTester:该类负责域类的序列化和反序列化。它使用默认的 Jackson 库,并通过 ObjectMapper 类进行序列化。
  • 这个类包含来自 JSON 测试工具的内容,非常有助于获取序列化中的值。
  • UserBuilder:这个类接收一个验证类,用于检查和验证被@NotBlank 或@NotNull 注解标记的字段值。
  • 验证:该类属于雅加达验证包,旨在帮助审查和验证用注解标记的字段,例如@NotBlank、@NoNull 等。
  • 我们正在使用 AssertJ 库来进行该类的序列化和反序列化断言。
  • 这个类帮助识别在测试过程中抛出的任何错误或异常类型。在清单 8-4 中,我们创建了一个不符合@NotBlank 注解的 User 类,因此该类将其识别为 ConstraintViolationException 异常。
  • assertThrows:我们可以用相同的方法处理这个 assertThrows 调用。不同的方式实现相同的断言。

正如你所看到的,测试你的域名变得更加简单,甚至在等待某些响应或特定服务时,你可以通过对类进行序列化和反序列化来测试自己的域名架构。此外,请注意,我们可以针对 JSON 文件进行测试,这里指的是 user.json 文件。该文件必须放在 src/test/resources/com/apress/users 文件夹中,以便测试框架能够识别。

@WebMvcTest

如果我们可以进行独立的域/模式测试,那么我们可以在我们的 Web 控制器上进行这些测试吗?当然可以!我们可以使用@WebMvcTest 注解来专门测试 Web 端点——也就是我们的控制器。@WebMvcTest 注解会自动配置所有 Spring MVC 的基础设施,但仅限于带有@RestController 注解的类、Filter 接口的实现以及其他所有与 Web 相关的类。默认情况下,它将 webEnvironment 设置为 MOCK,这意味着该注解继承自@AutoConfigureMockMvc,这样您就可以使用 MockMvc 类。

让我们来看看 UserControllerTests 类,参见列表 8-5。

package com.apress.users;
import com.fasterxml.jackson.databind.ObjectMapper;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.Optional;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(controllers = { UsersController.class })
public class UserControllerTests {
  @Autowired
    private MockMvc mockMvc;
    @MockBean
    private UserRepository userRepository;
    @Test
    void getAllUsersTest() throws Exception {
        when(userRepository.findAll()).thenReturn(Arrays.asList(
                UserBuilder.createUser()
                        .withName("Ximena")
                        .withEmail("ximena@email.com")
                        .active()
                        .withRoles(UserRole.USER, UserRole.ADMIN)
                        .withPassword("aw3s0m3R!")
                        .build(),
                UserBuilder.createUser()
                        .withName("Norma")
                        .withEmail("norma@email.com")
                        .active()
                        .withRoles(UserRole.USER)
                        .withPassword("aw3s0m3R!")
                        .build()
        ));
        mockMvc.perform(get("/users"))
                .andDo(print())
                .andExpect(status().isOk())
      .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$[0].active").value(true));
    }
    @Test
    void newUserTest() throws Exception {
        User user = UserBuilder.createUser()
                .withName("Dummy")
                .withEmail("dummy@email.com")
                .active()
                .withRoles(UserRole.USER, UserRole.ADMIN)
                .withPassword("aw3s0m3R!")
                .build();
        when(userRepository.save(user)).thenReturn(user);
        mockMvc.perform(post("/users")
                        .content(toJson(user))
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.email").value("dummy@email.com"));
    }
    @Test
    void findUserByEmailTest() throws Exception {
        User user = UserBuilder.createUser()
                .withName("Dummy")
                .withEmail("dummy@email.com")
                .active()
                .withRoles(UserRole.USER, UserRole.ADMIN)
                .withPassword("aw3s0m3R!")
                .build();
        when(userRepository.findById(user.getEmail())).thenReturn(Optional.of(user));
        mockMvc.perform(get("/users/{email}",user.getEmail())
                .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.email").value("dummy@email.com"));
    }
    @Test
    void deleteUserByEmailTest() throws Exception{
        User user = UserBuilder.createUser()
                .withEmail("dummy@email.com")
                .build();
        doNothing().when(userRepository).deleteById(user.getEmail());
        mockMvc.perform(delete("/users/{email}",user.getEmail()))
                .andExpect(status().isNoContent());
    }
    private static String toJson(final Object obj) {
        try {
            return new ObjectMapper().writeValueAsString(obj);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

8-5 src/main/java/apress/com/users/UserControllerTests.java

UserControllerTests 类包含以下内容:

  • @WebMvcvTest:这个注解继承了 @AutoConfiguraMockMvc 注解,因此可以直接使用 MockMvc 类,这也意味着 webEnvironment 是 MOCK。通过这个注解,我们可以仅测试我们的 web 控制器,而无需启动整个服务器进行测试。您可以在这个注解中声明更多的控制器。
  • MockMvc:正如您所知,这个类是 Spring MVC 测试支持的服务器端入口。在这种情况下,它使我们能够向控制器发起 HTTP 请求。
  • @MockBean:这个注解用于模拟 bean 的行为。当你不想等待外部服务准备就绪时,这非常有用,因为你可以模拟其行为。在这种情况下,我们正在模拟 UserRepository。
  • 我们使用 when().thenReturn 来准备调用,然后执行这些调用并期望结果。此外,我们还使用 doNothing().when()。正如你所看到的,Mockito 库提供了一个非常流畅的 API,适用于这些场景。

在继续下一部分之前,请花时间深入了解@WebMvcTest 注解和 UserControllerTests 类。

@DataJpaTest

通过这个注解,您可以测试与 JPA 技术相关的所有内容。它会为您自动配置所有存储库和所需实体,让您无需启动 Web 服务器或其他依赖项即可进行测试;换句话说,@DataJpaTest 专注于数据层的测试。它还将 spring.jpa.show-sql 属性设置为 true,以便您在执行测试时可以查看查询内容。接下来,让我们看看 UserJpaRepositoryTests 类。请参见列表 8-6。

package com.apress.users;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Import;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
@Import({UserConfiguration.class})
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DataJpaTest
public class UserJpaRepositoryTests {
    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:latest");
    @Autowired
    UserRepository userRepository;
    @Test
    void findAllTest(){
        var expectedUsers = userRepository.findAll();
        assertThat(expectedUsers).isNotEmpty();
        assertThat(expectedUsers).isInstanceOf(Iterable.class);
        assertThat(expectedUsers).element(0).isInstanceOf(User.class);
        assertThat(expectedUsers).element(0).matches( user -> user.isActive());
    }
    @Test
    void saveTest(){
        var dummyUser =  UserBuilder.createUser()
                .withName("Dummy")
                .withEmail("dummy@email.com")
                .active()
                .withRoles(UserRole.INFO)
                .withPassword("aw3s0m3R!")
                .build();
        var expectedUser = userRepository.save(dummyUser);
        assertThat(expectedUser).isNotNull();
        assertThat(expectedUser).isInstanceOf(User.class);
        assertThat(expectedUser).hasNoNullFieldsOrProperties();
        assertThat(expectedUser.isActive()).isTrue();
    }
    @Test
    void findByIdTest(){
        var expectedUser = userRepository.findById("norma@email.com");
        assertThat(expectedUser).isNotNull();
        assertThat(expectedUser.get()).isInstanceOf(User.class);
        assertThat(expectedUser.get().isActive()).isTrue();
        assertThat(expectedUser.get().getName()).isEqualTo("Norma");
    }
    @Test
    void deleteByIdTest(){
        var expectedUser = userRepository.findById("ximena@email.com");
        assertThat(expectedUser).isNotNull();
        assertThat(expectedUser.get()).isInstanceOf(User.class);
        assertThat(expectedUser.get().isActive()).isTrue();
        assertThat(expectedUser.get().getName()).isEqualTo("Ximena");
        userRepository.deleteById("ximena@email.com");
        expectedUser = userRepository.findById("ximena@email.com");
        assertThat(expectedUser).isNotNull();
        assertThat(expectedUser).isEmpty();
    }
}

8-6 src/test/java/apress/com/users/UserJpaRepositoryTests.java

UserJpaRepositoryTests 类包含以下内容:

  • @DataJpaTest:该注解会自动配置与 JPA 相关的所有内容,包括从存储库到 EntityManager(作为 JPA 实现所需),并且还继承了@Transactional 注解,这确保了您的测试是完全事务性的。使用此注解,您无需任何 Web 层即可对数据持久性进行操作。
  • Spring 的一个很酷的功能是可以通过这个注解导入特定的配置。在这种情况下,我们导入了 UserConfiguration,并在其中添加了一些用户。
  • @Testcontainers:正如之前所述,这个注解用于启动和停止容器;它会查找所有定义的 @Container 注解。在这种情况下,我们将这个类标记为 Testcontainers,它会查找任何 @Container 注解,并为运行指定的容器镜像设置环境。
  • @AutoConfigureTestDatabase:在测试数据层时,通常使用内存数据库更为理想,因为它既快速又高效;但有时您需要在真实数据库上进行测试,这时 @AutoConfigureTestDatabase 注解将不会使用嵌入式自动配置,而是会遵循您通过 @Container 和 @ServerConnection 指定的内容。
  • @Container, @ServerConnection, PostgreSQLContainer: 你已经熟悉这些注解了。所有这些注解会自动配置 PostgreSQL 容器的启动和停止,以及需要连接参数(如用户名、密码、URL、方言、驱动类等)的 DataSource 接口。
  • 在这个类中,我们使用 AssertJ,它提供了一个流畅的 API 用于进行断言。

Spring Boot 不仅支持 JPA,还提供了以下注解(以及更多),这些注解遵循相同的模式,可以根据您需要测试的技术来隔离测试

  • @JdbcTest:与 JdbcTemplate 编程相关的测试,内容涵盖第 4 章和第 5 章
  • @DataJdbcTest:用于测试 Spring Data 存储库
  • 所有与 jOOQ 相关的测试用例
  • 针对 Spring Data Neo4J 的测试
  • 所有与 Spring Data Redis 相关的测试内容
  • 用于进行 LDAP 测试

@WebFluxTest

如果您有任何基于 Spring Boot WebFlux 的反应式 Web 应用程序,您可以使用 @WebFluxTest 注解进行测试,该注解会自动配置 WebFlux 应用程序所需的所有 Web 相关的 bean 和注解。它会自动配置 WebTestClient 接口,以便对 WebFlux 端点进行任何请求或交换,并且与 @MockBean 注解配合使用时,可以轻松处理任何服务或存储库。

让我们来看看 RetroBoardWebFluxTests 类,参见列表 8-7。

package com.apress.myretro;
import com.apress.myretro.board.Card;
import com.apress.myretro.board.CardType;
import com.apress.myretro.board.RetroBoard;
import com.apress.myretro.service.RetroBoardService;
import com.apress.myretro.web.RetroBoardController;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.UUID;
@WebFluxTest(controllers = {RetroBoardController.class})
public class RetroBoardWebFluxTests {
    @MockBean
    RetroBoardService retroBoardService;
    @Autowired
    private WebTestClient webClient;
    @Test
    void getAllRetroBoardTest(){
        Mockito.when(retroBoardService.findAll()).thenReturn(Flux.just(
                new RetroBoard(UUID.randomUUID(),"Simple Retro", Arrays.asList(
                        new Card(UUID.randomUUID(),"Happy to be here", CardType.HAPPY),
                        new Card(UUID.randomUUID(),"Meetings everywhere", CardType.SAD),
                        new Card(UUID.randomUUID(),"Vacations?", CardType.MEH),
                        new Card(UUID.randomUUID(),"Awesome Discounts", CardType.HAPPY),
                        new Card(UUID.randomUUID(),"Missed my train", CardType.SAD)
                ))
        ));
        webClient.get()
                .uri("/retros")
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectBody().jsonPath("$[0].name").isEqualTo("Simple Retro");
        Mockito.verify(retroBoardService,Mockito.times(1)).findAll();
    }
    @Test
    void findRetroBoardByIdTest(){
        UUID uuid = UUID.randomUUID();
        Mockito.when(retroBoardService.findById(uuid)).thenReturn(Mono.just(
                new RetroBoard(uuid,"Simple Retro", Arrays.asList(
                        new Card(UUID.randomUUID(),"Happy to be here", CardType.HAPPY),
                        new Card(UUID.randomUUID(),"Meetings everywhere", CardType.SAD),
                        new Card(UUID.randomUUID(),"Vacations?", CardType.MEH),
                        new Card(UUID.randomUUID(),"Awesome Discounts", CardType.HAPPY),
                        new Card(UUID.randomUUID(),"Missed my train", CardType.SAD)
                ))
        ));
        webClient.get()
                .uri("/retros/{uuid}",uuid.toString())
                .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .exchange()
                .expectStatus().isOk()
                .expectBody(RetroBoard.class);
        Mockito.verify(retroBoardService,Mockito.times(1)).findById(uuid);
    }
    @Test
    void saveRetroBoardTest(){
        RetroBoard retroBoard = new RetroBoard();
        retroBoard.setName("Simple Retro");
        Mockito.when(retroBoardService.save(retroBoard))
                .thenReturn(Mono.just(retroBoard));
        webClient.post()
                .uri("/retros")
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(retroBoard))
                .exchange()
                .expectStatus().isOk();
        Mockito.verify(retroBoardService,Mockito.times(1)).save(retroBoard);
    }
    @Test
    void deleteRetroBoardTest(){
        UUID uuid = UUID.randomUUID();
        Mockito.when(retroBoardService.delete(uuid)).thenReturn(Mono.empty());
        webClient.delete()
                .uri("/retros/{uuid}",uuid.toString())
                .exchange()
                .expectStatus().isOk();
        Mockito.verify(retroBoardService,Mockito.times(1)).delete(uuid);
    }
}

8-7 src/test/java/apress/com/myretro/ RetroBoardWebFluxTests.java

RetroBoardWebFluxTests 类包括以下内容:

  • @WebFluxTests:该注解配置与 WebFlux 应用程序相关的所有内容,包括查找@Controller、Filter 等。您可以添加要测试的控制器,这里是 RetroBoardController 类。请注意,此测试与数据层隔离,仅测试 web 层。
  • WebTestClient:回顾第 7 章,这个类用于测试 HTTP 和 WebFlux 端点,并返回响应的模拟对象,从而使得进行断言和测试我们的类变得更加简单。
  • 我们再次使用 Mockito 库,不仅为了准备调用,还为了验证调用是否按照我们所说的次数执行。

@DataMongoTest

Spring Boot 除了支持 SQL 测试外,还为 NoSQL 数据库提供了测试支持,例如 @DataMongoTest 注解可以用来专门测试 MongoDB 数据层。

让我们来看看 RetroBoardMongoTests 类。请参阅第 8-8 号列表。

package com.apress.myretro;
import com.apress.myretro.board.RetroBoard;
import com.apress.myretro.persistence.RetroBoardRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.junit.jupiter.Container;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@ActiveProfiles("mongoTest")
@DataMongoTest
public class RetroBoardMongoTests {
    @Container
    static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:latest");
    static {
        mongoDBContainer.start();
    }
    @DynamicPropertySource
    static void setProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
    }
    @Autowired
    RetroBoardRepository retroBoardRepository;
    @Test
    void saveRetroTest(){
        var name = "Spring Boot 3 Retro";
        RetroBoard retroBoard = new RetroBoard();
        retroBoard.setId(UUID.randomUUID());
        retroBoard.setName(name);
        var retroBoardResult = this.retroBoardRepository.insert(retroBoard).block();
        assertThat(retroBoardResult).isNotNull();
        assertThat(retroBoardResult.getId()).isInstanceOf(UUID.class);
        assertThat(retroBoardResult.getName()).isEqualTo(name);
    }
    @Test
    void findRetroBoardById(){
        RetroBoard retroBoard = new RetroBoard();
        retroBoard.setId(UUID.randomUUID());
        retroBoard.setName("Migration Retro");
        var retroBoardResult = this.retroBoardRepository.insert(retroBoard).block();
        assertThat(retroBoardResult).isNotNull();
        assertThat(retroBoardResult.getId()).isInstanceOf(UUID.class);
        Mono<RetroBoard> retroBoardMono = this.retroBoardRepository.findById(retroBoardResult.getId());
        StepVerifier
                .create(retroBoardMono)
                .assertNext( retro -> {
                    assertThat(retro).isNotNull();
                    assertThat(retro).isInstanceOf(RetroBoard.class);
                    assertThat(retro.getName()).isEqualTo("Migration Retro");
                })
                .expectComplete()
                .verify();
    }
}

8-8 src/main/java/apress/com/myretro/RetroBoardMongoTests.java

RetroBoardMongoTests 类包括以下内容:

  • @DataMongoTest:该注解会自动配置所有 Mongo 层,能够找到与 Mongo 数据层相关的所有内容,包括领域类和仓库。
  • @ActiveProfiles: 你知道这个注解会为 mongoTest 设置配置文件,非常有助于隔离测试。
  • 你也知道这个注解,它用于启动和停止容器。在这种情况下,我们静态声明了 MongoDB 容器,并省略了@Testcontainers 注解,仅仅是为了演示另一种实现相同结果的方法。
  • 使用@ServiceConnection 注解(在本章前面介绍过),测试将会使用正确的 Mongo 连接属性(默认值)进行设置,但在这种情况下,我们采用另一种方法,即通过@DynamicPropertiesSource 注解动态设置连接属性。使用此注解,您可以覆盖所有默认值。

使用 Testcontainers 来运行您的 Spring Boot 应用程序

到目前为止,我们一直使用 spring-boot-docker-compose 依赖来运行应用程序,而 spring-boot-testcontainers 依赖仅用于测试。通过 Testcontainers,Spring Boot 提供了一种无需使用 docker-compose 直接从容器运行应用程序的方法。怎么做呢?这非常简单。请打开 RetroBoardTestConfiguration 类,参见清单 8-9。

package com.apress.myretro;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.devtools.restart.RestartScope;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.MongoDBContainer;
@Profile({"!mongoTest"})
@Configuration
public class RetroBoardTestConfiguration {
        @Bean
        @RestartScope
        @ServiceConnection
        public MongoDBContainer mongoDBContainer(){
                return new MongoDBContainer("mongo:latest");
        }
        public static void main(String[] args) {
                SpringApplication.from(MyretroApplication::main).run(args);
        }
}

8-9 src/main/java/apress/com/myretro/RetroBoardTestConfiguration.java

让我们来分析 RetroBoardTestConfiguration 类

  • 每次运行应用程序时,容器都会重启。为了避免这种情况,可以使用 spring-boot-devtools 依赖项,并通过 @RestartScope 来防止容器在每个测试方法执行前重启。这样,运行测试所需的时间会更短,因为您不需要等待容器重启。
  • 正如您所知,这个注解会设置您要运行的容器的所有连接参数,因此您无需担心这些参数。
  • 这非常重要!请注意,我们使用了一个主方法(是的,这是我们应用程序的另一个入口点)。在这种情况下,我们不是使用 SpringApplication.run() 方法,而是使用指向主类的 SpringApplication.from() 方法(在这里是 MyretroApplication::main)。

概要

在本章中,您学习了 Spring Boot 中单元测试和集成测试的支持。您发现 Spring Boot 测试是基于 Spring Framework 测试的,并且 Spring Boot 的自动配置可以帮助配置许多测试。

你还了解到,Spring Boot 测试包括切片测试,这使你能够按技术对所有层进行独立测试——从使用 @JsonTest 注解的领域类,到使用 @WebMvcTest 注解的 Web 控制器,再到使用 @DataJpaTest 注解的数据层。

本章还向您介绍了 Testcontainers,它不仅可以帮助进行测试,还可以用于运行您的应用程序。您了解了许多测试方法,这得益于 Spring Boot 测试框架,它提供了 AssertJ、Mockito、Hamcrest 等多种库供您选择进行测试。

Tags:

最近发表
标签列表