网站首页 > 技术文章 正文
测试容器技术
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 等多种库供您选择进行测试。
猜你喜欢
- 2025-01-09 精通Spring Boot 3 : 13. Spring Cloud 与 Spring Boot (4)
- 2025-01-09 Spring Boot集成Redis Search快速入门Demo
- 2025-01-09 Spring Boot 3.x嵌入MongoDB 进行测试
- 2025-01-09 java安全之fastjson链分析
- 2025-01-09 MyBatis初级实战之五:一对一关联查询
- 2025-01-09 DevSecOps 管道: 使用Jenkins实现安全的多语言应用程序
- 2025-01-09 Liquibase+Spring+Maven: 管理数据库轻松搞定
- 2025-01-09 比较一下JSON与XML两种数据格式?
- 2025-01-09 Java批量导入时,如何去除重复数据并返回结果?
- 2025-01-09 Spring Boot集成Mockito快速入门Demo
- 02-21走进git时代, 你该怎么玩?_gits
- 02-21GitHub是什么?它可不仅仅是云中的Git版本控制器
- 02-21Git常用操作总结_git基本用法
- 02-21为什么互联网巨头使用Git而放弃SVN?(含核心命令与原理)
- 02-21Git 高级用法,喜欢就拿去用_git基本用法
- 02-21Git常用命令和Git团队使用规范指南
- 02-21总结几个常用的Git命令的使用方法
- 02-21Git工作原理和常用指令_git原理详解
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)