1. 概述

本教程将深入探讨 如何利用 Spring Boot 的框架支持编写测试。我们将覆盖两种测试类型:可独立运行的单元测试,以及执行前会启动 Spring 上下文的集成测试。

如果你是 Spring Boot 新手,建议先阅读我们的 Spring Boot 入门教程

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.3.2</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 层单元测试的关键区别:这里没有模拟任何组件,执行的是端到端场景

5. 使用 @TestConfiguration 配置测试

如前节所示,标注 @SpringBootTest 的测试会启动完整应用上下文,这意味着我们可以 @Autowire 任何被组件扫描到的 Bean:

@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 的类会被排除在组件扫描之外。因此需要在每个需要 @Autowire 的测试中显式导入,使用 @Import 注解实现:

@ExtendWith(SpringExtension.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 提供的模拟支持可以帮我们实现这一点

先看测试类骨架:

@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 类,需要将其实例创建为 @Bean 以便在测试类中 @Autowire。这可通过 @TestConfiguration 实现。

另一个关键点是 @MockBean 的使用。它会为 EmployeeRepository 创建模拟对象,从而绕过对真实 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 的实体,包含 idname 属性:

@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
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 代码,单元测试中自然要模拟 Service 层:

@ExtendWith(SpringExtension.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 控制器的强大方式。

现在编写测试用例:

@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 配合使用,为依赖提供模拟实现
@JdbcTest 用于测试只需 DataSource 的 JPA 应用。配置内存嵌入式数据库和 JdbcTemplate
@JooqTest 用于测试 jOOQ 相关组件。配置 DSLContext
@DataMongoTest 用于测试 MongoDB 应用。配置内存嵌入式 MongoDB(若驱动可用)、MongoTemplate,扫描 @Document 类并配置 Spring Data MongoDB 仓库
@DataRedisTest 简化 Redis 应用测试。默认扫描 @RedisHash 类并配置 Spring Data Redis 仓库
@DataLdapTest 配置内存嵌入式 LDAP(若可用)、LdapTemplate,扫描 @Entry 类并配置 Spring Data LDAP 仓库
@RestClientTest 用于测试 REST 客户端。自动配置 Jackson、Gson、Jsonb 等依赖;配置 RestTemplateBuilder;默认添加 MockRestServiceServer 支持
@JsonTest 仅初始化测试 JSON 序列化所需的 Spring 应用上下文

关于这些注解的更多细节及如何优化集成测试,可参考我们的 Spring 集成测试优化 文章。

10. 总结

本文深入探讨了 Spring Boot 的测试支持,展示了如何高效编写单元测试。

想继续学习测试相关知识,可阅读我们关于 集成测试优化 Spring 集成测试JUnit 5 单元测试 的独立文章。


原始标题:Testing in Spring Boot | Baeldung

» 下一篇: JAX-WS 入门指南