小架构step系列15:白盒集成测试
1 概述
白盒集成测试是spring-test提供的一种不需要启动Web Server就可以触发Controller里的http方法执行的一种方式,由于是从Controller就开始测试,相当于把对应方法后面的一整个流程都进行了测试,这个流程里面可能包含数据库的操作,可能包含文件操作,可能包含中间操作等,如果这些都需要进行配套,比如准备好文件存储的OSS、准备好类似kafka等消息中间件,那这个测试会相当难做,即使能够做也很难多做,成本非常高。本文就列一下这些场景的一种做法,仅供参考。
2 常用方式
2.1 数据库访问
2.1.1 内置数据库
开发一个业务系统,几乎都是需要数据库的,所以代码需要访问数据库,设计到这个场景的测试方法有两种。
一种是使用Spring提供的内置数据库,如HSQL、H2、Derby等,参考官方文档:https://docs.spring.io/spring-framework/docs/5.3.39/reference/html/data-access.html#jdbc-embedded-database-support
public class DataAccessIntegrationTestTemplate {private EmbeddedDatabase db;@BeforeEachpublic void setUp() {// creates an HSQL in-memory database populated from default scripts// classpath:schema.sql and classpath:data.sqldb = new EmbeddedDatabaseBuilder().generateUniqueName(true).addDefaultScripts().build();}@Testpublic void testDataAccess() {JdbcTemplate template = new JdbcTemplate(db);template.query( /* ... */ );}@AfterEachpublic void tearDown() {db.shutdown();}
}
此方式的好处是不需要安装数据库,也不需要清理数据,就像每个测试用例都用一个新的数据库一样。
2.1.2 外部数据库
第二种就是使用外部数据库,此时测试用例用上事务注解@Transactional,该注解由spring-tx包提供,需要引入spring-tx包的依赖:
<dependency><groupId>org.springframework</groupId><artifactId>spring-tx</artifactId>
</dependency>@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class HelloControllerTest {
}
从之前的原理来看,执行测试用例就相当于一个普通的方法调用,加上这个注解之后,相当于整个测试用例的执行都是在一个事务中执行,测试用例执行完成之后,测试框架会自动回滚,就相当于没有真正把数据写到数据库里,这样就不需要清理数据。
注意:此方式需要准备一个外部的数据库,并正常配置数据库连接参数。好处是真正能够测试到实际使用的数据库。
2.2 文件访问
当需要读取或者写出文件时,JDK提供了临时文件这种方式可以用到测试当中:
File tempFile = File.createTempFile("fileName", ".tmp");
在测试用例中生成一个临时文件,然后传到Service中代替业务实际使用的文件。
注意:要设计好如何把临时文件传到Service中,这可以倒逼把代码写得可测。
2.3 其它
其它类型,如使用消息中间件收发消息,使用ElasticSearch存取数据,使用OSS存取文件,使用HttpClient访问外部链接等,这些都需要封装出一个Repository或者Facade接口,通过mock的方式使用mock对象进行代替。
// 正常的业务代码
@Service
public class GroupMemberCreatorImpl implements GroupMemberCreator {private GroupMemberRepository repository;@Autowiredpublic GroupMemberCreatorImpl(GroupMemberRepository repository) {this.repository = repository;}@Overridepublic GroupMember create(Long groupId, String memberName) {GroupMember member = new GroupMember(groupId, memberName);return repository.save(member); // 在测试中,调此接口会触发mock对象的行为}
}// 测试代码
@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest {@Autowiredprivate MockMvc mockMvc;@MockBeanprivate GroupMemberRepository groupMemberRepository;@Testpublic void create_normal_group_member() throws Exception {Long groupId = 1L;String memberName = "zhangsan";GroupMember member = new GroupMember(groupId, memberName);// 当调Repository.save()时,返回值使用mock对象代替,不真正执行业务代码Repository.save()的逻辑Mockito.when(groupMemberRepository.save(Mockito.any())).thenReturn(member);MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/createGroupMember").contentType(MediaType.APPLICATION_FORM_URLENCODED).content("groupId=" + groupId + "&memberName=" + memberName)).andExpect(status().isOk()).andReturn();}
}
- NoSQL数据库读写数据,如Redis、MongoDB等;
- 消息中间件的收发消息,比如Kafka、RabbitMQ、RocketMQ等;
- 搜索引擎搜索和存储数据,比如ElasticSearch;
- 任务调度,如XXL-Job;
- 图片/文件存取,如OSS;
- 访问外部链接,如HttpClient;
- 导入导出Excel数据,如EasyExcel、POI等;
- 调用RPC接口;
- 其它第三方SDK等;