Spring 集成测试

数据库测试

数据库测试通常有两种做法

  • 采用嵌入式内存数据
  • 采用真实的数据库,事务回滚

测试配置

我们做测试的一个关键点就是不能随意修改代码,切记,不能为了测试的需要而修改代码。如果真的要修改,也许应该修改的是设计,而不仅仅是代码。

不能修改代码,但我们可以提供不同的配置。然测试连接到不同的数据库上。

@ExtendWith(SpringExtension.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource("classpath:test.properties")
public class TodoItemRepositoryTest {
  ...
}

嵌入式内存数据库

在 Java 世界中,常见的嵌入式内存数据库有 H2、HSQLDB、Apache 的 Derby 等。我们配置一个测试的依赖就好,以 H2 为例,像下面这样。

testImplementation "com.h2database:h2:$h2Version"

然后提供一个配置

jdbc.driverClassName=org.h2.Driver
jdbc.url=jdbc:h2:mem:todo;DB_CLOSE_DELAY=-1
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=create

如果运气好,测试可以顺利运行。

为什么会归结于运气呢?这不是嵌入式数据库的问题,是每个数据库SQL不一致的问题,真实情况下总有一部分 SQL 只能运行在特定的引擎上。

所以,实际项目嵌入式数据库测试用的不多。

事务回滚

采用标准的应用配置

spring.datasource.url=jdbc:mysql://localhost:3306/todo_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=todo
spring.datasource.password=geektime
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

采用这种做法,就不必担心 SQL 不兼容的问题了。

我们的事务回滚体现在 @DataJpaTest 上, 它把数据库回滚做成默认配置,所以我们什么都不用做。

@ExtendWith(SpringExtension.class)
@DataJpaTest
public class ExampleRepositoryTests {
  @Autowired
  private TestEntityManager entityManager;

  @Test
  public void should_work() throws Exception {
    this.entityManager.persist(new User("sboot", "1234"));
    ...
  }
}

如果你用的不是 JPA 而是其它的数据访问方式,Spring 也给我们提供了 @JdbcTest,只要有 DataSource, 它就可以很好地工作起来,这适用于绝大多数的测试情况。模拟数据可以直接使用 sql。如下

@JdbcTest
@Sql({"test-data.sql"})
class EmployeeDAOIntegrationTest {
  @Autowired
  private DataSource dataSource;
  
  ...
}

Web 接口测试

我们采用整体集成的方式对系统进行测试,关键点是 @SpringBootTest

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class TodoItemResourceTest {
  ...
}

集成测试分两种

  • 所有代码都集成的测试
  • 针对外部组件的测试

测试 web 接口也有类似单元测试的方式

@WebMvcTest(TodoItemResource.class)
public class TodoItemResourceTest {
  ...
}

我们指定了要测试的组件 TodoItemResource.这个测试里,它只会集成与 TodoItemResource 相关的部分。

如果把它视为单元测试,服务层后面的代码都是外部的,我们可以采用模拟对象把它控制在可控范围内,MockBean 就开始发挥作用了。

@WebMvcTest(TodoItemResource.class)
public class TodoItemResourceTest {
  @MockBean
  private TodoItemService service;
  
  @Test
  public void should_add_item() throws Exception {
    when(service.addTodoItem(TodoParameter.of("foo"))).thenReturn(new TodoItem("foo"));
    ...
  }
}

@MockBean 标记的模拟对象会参与到组件组装的过程,我们就可以设置他的行为。如果 web 层与服务处有复杂交互,这种做法就可以很好的处理。但是,不建议做的这么复杂。

当年 Spring 摆脱了大部分对于应用服务器的依赖,但是 Web 却是它一直没有摆脱的。所以,怎么更好地不依赖于 Web 服务器进行测试,就是摆在 Spring 面前的问题。答案是 Spring 提供了模拟的 Web 环境

我们再来回顾一下


@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class TodoItemResourceTest {
    @Autowired
    private MockMvc mockMvc;
    ...

    @Test
    public void should_add_item() throws Exception {
        String todoItem = "{ " +
                "\"content\": \"foo\"" +
                "}";
        mockMvc.perform(MockMvcRequestBuilders.post("/todo-items")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(todoItem))
                .andExpect(status().isCreated());
        assertThat(repository.findAll()).anyMatch(item -> item.getContent().equals("foo"));
    }
}

关键是 @AutoConfigureMockMvc,它为我们配置好了 MockMvc,剩下的就是我们使用这个配置好的环境进行访问。

所谓的模拟环境就是它根本没有启动真正的 Web 服务器,而是直接调用了我们的代码,省略了请求走网络的过程。但是请求进入服务器后的主要处理都在(无论是各种 Filter 的处理,还是从请求体到请求对象的转换)。所以MockMvc 是 Spring 轻量级开发的一个重要的组成部分。

总结

Spring 集成测试

  • 数据库
    • 事务回滚
      • @DataJpaTest
      • @JdbcTest
      • @Transactional
    • 内存数据库
  • Web 接口
    • 集成测试 @SpingBootTest
    • 一个单元集成测试 @WebMvcTest
    • 模拟对象 @MockBean

采用轻量级的测试手段,保证代码的正确性

Last Updated:
Contributors: himcs