baeldung 2017-04-26
1. 概述
在本教程中,我们将了解如何使用 Spring Boot 提供的框架支持来编写测试。我们将涵盖可以独立运行的单元测试,以及在执行测试前需要启动 Spring 上下文的集成测试。
2. 项目设置
本文将使用的应用程序是一个提供对 Employee(员工)资源进行基本操作的 API。这是一个典型的分层架构 —— API 请求从 Controller 层传递到 Service 层,再到持久化(Persistence)层。
3. Maven 依赖
首先添加我们的测试依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-test 是主要的测试依赖,包含了我们测试所需的大部分组件。
H2 数据库作为内存数据库,无需配置和启动真实数据库即可用于测试。
4. 使用 @SpringBootTest 进行集成测试
顾名思义,集成测试关注的是应用程序不同层级之间的集成。这意味着不会使用任何 Mock(模拟)对象。
理想情况下,我们应该将集成测试与单元测试分开,并避免一起运行。可以通过使用不同的 Profile 来仅运行集成测试。这样做的原因可能包括:集成测试耗时较长,或者需要真实的数据库环境。
不过在本文中,我们不会重点讨论这一点,而是使用内存中的 H2 数据库。
集成测试需要启动容器来执行测试用例,因此需要一些额外的设置 —— 而在 Spring Boot 中,这一切都非常简单:
@ExtendWith(SpringExtension.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,供测试使用。webEnvironment属性用于配置运行时环境;这里使用WebEnvironment.MOCK,使容器在模拟的 Servlet 环境中运行。@TestPropertySource注解用于指定测试专用的属性文件位置。注意,通过此注解加载的属性文件会覆盖原有的application.properties文件。
application-integrationtest.properties 文件包含用于配置持久化存储的详细信息:
spring.datasource.url = jdbc:h2:mem:test
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect
如果希望针对 MySQL 运行集成测试,只需修改上述属性值即可。
集成测试的测试用例可能与 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 注解的测试会启动完整的应用上下文,这意味着我们可以将任何通过组件扫描发现的 Bean 通过 @Autowired 注入到测试类中:
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class EmployeeServiceImplIntegrationTest {
@Autowired
private EmployeeService employeeService;
// 类代码 ...
}
然而,有时我们可能希望避免启动真实的应用上下文,而使用专门的测试配置。这时可以使用 @TestConfiguration 注解。有两种使用方式:
方式一:在测试类中定义静态内部类
@ExtendWith(SpringExtension.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 注解的配置类会被排除在组件扫描之外。因此,我们需要在每个需要注入该 Bean 的测试类中显式导入它,可通过 @Import 注解实现:
@ExtendWith(SpringExtension.class)
@Import(EmployeeServiceImplTestContextConfiguration.class)
public class EmployeeServiceImplIntegrationTest {
@Autowired
private EmployeeService employeeService;
// 其余类代码
}
6. 使用 @MockBean 进行 Mock
我们的 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 支持。
首先看测试类的基本结构:
@ExtendWith(SpringExtension.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),以便在测试类中通过 @Autowired 注入。这可以通过 @TestConfiguration 实现。
另一个关键点是 @MockBean 的使用。它会为 EmployeeRepository 创建一个 Mock 对象,从而绕过对真实 EmployeeRepository 的调用:
@BeforeEach
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;
// 标准的 getter、setter 和构造函数
}
以下是使用 Spring Data JPA 的 Repository 接口:
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
public Employee findByName(String name);
}
持久化层代码到此为止。现在开始编写测试类。
首先创建测试类的基本结构:
@ExtendWith(SpringExtension.class)
@DataJpaTest
public class EmployeeRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private EmployeeRepository employeeRepository;
// 在此处编写测试用例
}
@ExtendWith(SpringExtension.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() {
// given
Employee alex = new Employee("alex");
entityManager.persist(alex);
entityManager.flush();
// when
Employee found = employeeRepository.findByName(alex.getName());
// then
assertThat(found.getName()).isEqualTo(alex.getName());
}
在上述测试中,我们使用 TestEntityManager 向数据库插入一个 Employee,并通过按名称查找的 API 读取它。
assertThat(...) 来自 AssertJ 库,该库已随 Spring Boot 一起提供。
8. 使用 @WebMvcTest 进行单元测试
我们的 Controller 依赖于 Service 层;为简化起见,只包含一个方法:
@RestController
@RequestMapping("/api")
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/employees")
public List<Employee> getAllEmployees() {
return employeeService.getAllEmployees();
}
}
由于我们只关注 Controller 代码,因此在单元测试中自然应该 Mock Service 层:
@ExtendWith(SpringExtension.class)
@WebMvcTest(EmployeeRestController.class)
public class EmployeeRestControllerIntegrationTest {
@Autowired
private MockMvc mvc;
@MockBean
private EmployeeService service;
// 在此处编写测试用例
}
- 使用
@WebMvcTest可以自动配置 Spring MVC 基础设施,用于单元测试。 - 通常,
@WebMvcTest仅用于启动单个 Controller。也可以结合@MockBean为所需依赖提供 Mock 实现。 @WebMvcTest还会自动配置MockMvc,它提供了一种强大且便捷的方式来测试 MVC 控制器,而无需启动完整的 HTTP 服务器。
接下来编写测试用例:
@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() 等。注意,我们还在请求中设置了内容类型。
MockMvc 非常灵活,可以构造任意类型的请求。
9. 自动配置的测试
Spring Boot 的自动配置注解的一大优势在于,它能只加载应用程序的部分组件,从而针对性地测试代码库的特定层级。
除了上述提到的注解外,以下是一些广泛使用的注解:
| 注解 | 描述 |
|---|---|
@WebFluxTest |
用于测试 Spring WebFlux 控制器,通常与 @MockBean 结合使用以提供依赖的 Mock 实现。 |
@JdbcTest |
用于仅需 DataSource 的 JPA 应用测试,会配置内存嵌入式数据库和 JdbcTemplate。 |
@JooqTest |
用于测试 jOOQ 相关组件,会配置 DSLContext。 |
@DataMongoTest |
用于测试 MongoDB 应用,若驱动可用则配置内存嵌入式 MongoDB、MongoTemplate,扫描 @Document 类并配置 Spring Data MongoDB Repository。 |
@DataRedisTest |
用于 Redis 应用测试,默认扫描 @RedisHash 类并配置 Spring Data Redis Repository。 |
@DataLdapTest |
配置内存嵌入式 LDAP(若可用)、LdapTemplate,扫描 @Entry 类并配置 Spring Data LDAP Repository。 |
@RestClientTest |
用于测试 REST 客户端,自动配置 Jackson/Gson/Jsonb 支持,配置 RestTemplateBuilder,并默认支持 MockRestServiceServer。 |
@JsonTest |
仅初始化测试 JSON 序列化所需的 Spring 应用上下文 Bean。 |
更多关于这些注解的信息,以及如何进一步优化集成测试,请参阅我们关于《优化 Spring 集成测试》的文章。
10. 结论
在本文中,我们深入探讨了 Spring Boot 的测试支持,并展示了如何高效地编写单元测试。