Spring Boot 中的测试
1、概览
本文将带你了解如何使用 Spring Boot 中的框架支持来编写测试,包括可以独立运行的单元测试,以及在执行测试之前加载 Spring Application Context 的集成测试。
2、项目设置
本文中的示例项目是一个 “雇员管理 API”,提供了对 Employee 资源的一些操作。是一个典型的MVC三层架构,从 Controller 到 Service 最后到持久层。
3、Maven 依赖
首先,添加测试依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-test 是主要的依赖,它包含了测试所需的大部分依赖。
H2 DB 是内存数据库,非常方便用于测试。
3.1、JUnit 4
从 Spring Boot 2.4 开始,JUnit 5 的 Vintage 引擎已从 spring-boot-starter-test 中移除。如果仍想使用 JUnit 4 编写测试,则需要添加以下 Maven 依赖:
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
4、使用 @SpringBootTest 进行集成测试
顾名思义,集成测试的重点是集成应用的不同层。这也意味着不涉及模拟(Mock)。
理想情况下,应将集成测试与单元测试分开,并且不应与单元测试一起运行。为此,可以使用不同的 Profile(配置文件),只运行集成测试。这样做的几个原因可能是集成测试比较耗时,而且可能需要一个实际的数据库来执行。
本文使用 H2 内存存储,所以这不是问题。
集成测试需要启动一个容器来执行测试用例。因此,需要进行一些额外的设置。所有这些在 Spring Boot 中都很容易实现:
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.MOCK,
classes = Application.class)
@AutoConfigureMockMvc
@TestPropertySource(
locations = "classpath:application-integrationtest.properties")
public class EmployeeRestControllerIntegrationTest {
@Autowired
private MockMvc mvc;
@Autowired
private EmployeeRepository repository;
// 编写测试用例
}
当需要加载整个容器时,@SpringBootTest 注解就派上用场了。该注解负责创建要测试中使用的 ApplicationContext。
可以使用 @SpringBootTest 的 webEnvironment 属性来配置运行环境;本文在这里使用了 WebEnvironment.MOCK,这样容器就能在模拟 servlet 环境中运行了。
接下来,@TestPropertySource 注解有助于配置测试专用 properties 文件的位置。注意,使用 @TestPropertySource 加载的属性文件将覆盖现有的 application.properties 文件。
application-integrationtest.properties 包含配置持久层存储的详细信息:
spring.datasource.url = jdbc:h2:mem:test
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect
如果想针对 MySQL 运行集成测试,可以在 properties 文件中更改上述值。
集成测试的测试用例可能与 Controller 层单元测试类似:
@Test
public void givenEmployees_whenGetEmployees_thenStatus200()
throws Exception {
createTestEmployee("bob");
mvc.perform(get("/api/employees")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content()
.contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].name", is("bob")));
}
与 Controller 层单元测试不同的是,这里没有任何 Mock,而是执行端到端的场景。
5、使用 @TestConfiguration 配置测试
如上节所述,使用 @SpringBootTest 注解的测试将加载整个 Application Context,这意味着可以通过 @Autowire 注解将组件扫描到的任何 Bean 注入到测试中:
@RunWith(SpringRunner.class)
@SpringBootTest
public class EmployeeServiceImplIntegrationTest {
@Autowired
private EmployeeService employeeService;
// class code ...
}
如果希望加载特殊的测试配置,而不是真实的 Application Context。可以使用 @TestConfiguration 注解来实现这一目的。使用注解有两种方法。一种是在同一个测试类中的静态内部类中使用,通过 @Autowire 注入该 Bean:
@RunWith(SpringRunner.class)
public class EmployeeServiceImplIntegrationTest {
@TestConfiguration
static class EmployeeServiceImplTestContextConfiguration {
@Bean
public EmployeeService employeeService() {
return new EmployeeService() {
// 实现方法
};
}
}
@Autowired
private EmployeeService employeeService;
}
或者,也可以创建一个单独的测试配置类:
@TestConfiguration
public class EmployeeServiceImplTestContextConfiguration {
@Bean
public EmployeeService employeeService() {
return new EmployeeService() {
// 实现方法
};
}
}
使用 @TestConfiguration 注解的配置类不在组件扫描之列,因此需要在每个要通过 @Autowire 注入 Bean 的测试中明确导入配置类。可以使用 @Import 注解来做到这一点:
@RunWith(SpringRunner.class)
@Import(EmployeeServiceImplTestContextConfiguration.class)
public class EmployeeServiceImplIntegrationTest {
@Autowired
private EmployeeService employeeService;
// 剩余代码
}
6、使用 @MockBean 进行模拟
Service 层依赖于 Repository:
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Override
public Employee getEmployeeByName(String name) {
return employeeRepository.findByName(name);
}
}
测试 Service 层,并不需要知道或关心持久层是如何实现的。即,理想情况下,编写和测试 Service 层代码,并不需要引入持久层。
为此,可以使用 Spring Boot Test 提供的 Mock(模拟)支持。
先来看看测试类的结构:
@RunWith(SpringRunner.class)
public class EmployeeServiceImplIntegrationTest {
@TestConfiguration
static class EmployeeServiceImplTestContextConfiguration {
@Bean
public EmployeeService employeeService() {
return new EmployeeServiceImpl();
}
}
@Autowired
private EmployeeService employeeService;
@MockBean
private EmployeeRepository employeeRepository;
// 编写测试用例
}
要检测 Service 类,需要创建一个 Service 类的实例,并通过 @Bean 其作注册为 Bean,以便可以在测试类中使用 @Autowired 注入它。可以使用 @TestConfiguration 注解实现这一配置。
另一个值得注意的地方是 @MockBean 的使用。它为 EmployeeRepository 创建了一个 Mock,可用于绕过对实际 EmployeeRepository 的调用:
@Before
public void setUp() {
Employee alex = new Employee("alex");
Mockito.when(employeeRepository.findByName(alex.getName()))
.thenReturn(alex);
}
设置完成后,测试用例将更加简单:
@Test
public void whenValidName_thenEmployeeShouldBeFound() {
String name = "alex";
Employee found = employeeService.getEmployeeByName(name);
assertThat(found.getName())
.isEqualTo(name);
}
7、使用 @DataJpaTest 进行集成测试
使用一个名为 Employee 的实体,它的属性是 id 和 name:
@Entity
@Table(name = "person")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Size(min = 3, max = 20)
private String name;
// Get、Set 构造器
}
Repository 如下:
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
public Employee findByName(String name);
}
持久层代码就到这里了。现在,开始编写测试类。
首先,创建测试类的骨架:
@RunWith(SpringRunner.class)
@DataJpaTest
public class EmployeeRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private EmployeeRepository employeeRepository;
// 编写测试用例
}
@RunWith(SpringRunner.class) 在 Spring Boot 测试功能和 JUnit 之间架起了一座桥梁。每当在 JUnit 测试中使用任何 Spring Boot 测试功能时,都需要使用此注解。
@DataJpaTest 提供了测试持久层所需的一些标准设置:
- 配置 H2 内存数据库
- 设置 Hibernate、Spring Data 和
DataSource - 执行
@EntityScan - 开启 SQL 日志
要执行数据库操作,需要数据库中已经存在一些记录。可以使用 TestEntityManager 来设置这些数据。
Spring Boot TestEntityManager 是标准 JPA EntityManager 的替代品,它提供了编写测试时常用的方法。
EmployeeRepository 是要测试的组件。
现在,编写第一个测试用例:
@Test
public void whenFindByName_thenReturnEmployee() {
// 给定
Employee alex = new Employee("alex");
entityManager.persist(alex);
entityManager.flush();
// 当
Employee found = employeeRepository.findByName(alex.getName());
// 那么
assertThat(found.getName())
.isEqualTo(alex.getName());
}
在上述测试中,使用 TestEntityManager 在数据库中插入一个 Employee 对象,并通过 findByName API 读取该 Employee。
assertThat(...) 部分来自 AssertJ 库,该库与 Spring Boot 绑定在一起。
8、使用 @WebMvcTest 进行单元测试
Controller 依赖于 Service 层;为简单起见,Controller 只包含一个方法:
@RestController
@RequestMapping("/api")
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/employees")
public List<Employee> getAllEmployees() {
return employeeService.getAllEmployees();
}
}
由于只关注 Controller 代码,因此很自然地在单元测试中模拟 Service 层代码:
@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeRestController.class)
public class EmployeeRestControllerIntegrationTest {
@Autowired
private MockMvc mvc;
@MockBean
private EmployeeService service;
// 编写测试用例
}
要测试 Controller,可以使用 @WebMvcTest。它会为单元测试自动配置 Spring MVC 基础架构。
在大多数情况下,@WebMvcTest 仅限于加载单个 Controller。还可以与 @MockBean 一起使用,为任何所需的依赖提供模拟实现。
@WebMvcTest 还会自动配置 MockMvc,它提供了一种无需启动完整 HTTP 服务器即可轻松测试 MVC Controller 的强大方法。
说完这些,现在来编写测试用例:
@Test
public void givenEmployees_whenGetEmployees_thenReturnJsonArray()
throws Exception {
Employee alex = new Employee("alex");
List<Employee> allEmployees = Arrays.asList(alex);
given(service.getAllEmployees()).willReturn(allEmployees);
mvc.perform(get("/api/employees")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].name", is(alex.getName())));
}
get(...) 方法调用可由其他与 HTTP(如 put()、post() 等)相对应的方法代替。注意,还在请求中设置了 Content Type。
MockMvc 非常灵活,可以使用它创建任何请求。
9、自动配置测试
Spring Boot 自动配置注解的神奇功能之一是,它有助于加载完整应用的部分和测试特定的代码层。
除上述注解外,还有一些广泛使用的注解:
@WebFluxTest:可以使用@WebFluxTest注解来测试 Spring WebFlux Controller。它通常与@MockBean一起使用,为所需的依赖提供模拟实现。@JdbcTest:可以使用@JdbcTest注解来测试 JPA 应用,但它只适用于只需要数据源的测试。该注解配置了一个内存嵌入式数据库和一个JdbcTemplate。@JooqTest:要测试与 jOOQ 相关的测试,可以使用@JooqTest注解来配置DSLContext。@DataMongoTest:用于测试 MongoDB 应用,@DataMongoTest是一个非常有用的注解。默认情况下,如果驱动可用(在依赖中),它会配置内存嵌入式 MongoDB,配置MongoTemplate,扫描@Document类,并配置 Spring Data MongoDB Repository。@DataRedisTest:使测试 Redis 应用变得更容易。它扫描@RedisHash类,并默认配置 Spring Data Redis Repository。@DataLdapTest配置内存中的嵌入式 LDAP(如果可用)、配置LdapTemplate、扫描@Entry类,并默认配置 Spring Data LDAP Repository。@RestClientTest:通常使用@RestClientTest注解来测试 REST 客户端。它能自动配置不同的依赖,如支持 Jackson、GSON 和 Jsonb;配置RestTemplateBuilder;并在默认情况下添加对MockRestServiceServer的支持。@JsonTest: 仅使用测试 JSON 序列化所需的 Bean 初始化 Spring Application Context。
有关这些注解以及如何进一步优化集成测试的更多信息,请参阅 Spring Boot 中文文档。
10、总结
本文介绍了 Spring Boot 中的测试支持,包括集成测试和单元测试。还介绍了如何编写高效的测试用例。
Ref:https://www.baeldung.com/spring-boot-testing