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

可以使用 @SpringBootTestwebEnvironment 属性来配置运行环境;本文在这里使用了 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 的实体,它的属性是 idname

@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 客户端。它能自动配置不同的依赖,如支持 JacksonGSONJsonb;配置 RestTemplateBuilder;并在默认情况下添加对 MockRestServiceServer 的支持。
  • @JsonTest: 仅使用测试 JSON 序列化所需的 Bean 初始化 Spring Application Context。

有关这些注解以及如何进一步优化集成测试的更多信息,请参阅 Spring Boot 中文文档

10、总结

本文介绍了 Spring Boot 中的测试支持,包括集成测试和单元测试。还介绍了如何编写高效的测试用例。


Ref:https://www.baeldung.com/spring-boot-testing