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