前言
之前写过一篇单元测试相关的文章,细心的同学会发现,单元测试其实是面向后端代码层面的测试,它只能保证单个函数或单个类的行为正常,并不能保证API正常,然而后端开发人员最终需要交付的其实是一个功能正常的API,那么应该如何保证API的功能正常呢?
开发甲:我会开发完成后直接将API交给前端进行联调,联调的过程中出现问题我再处理。
开发乙:我会通过Postman工具来手动模拟用户请求,然后观察API行为以及数据是否正常,然后我才会将API交给前端进行联调。
开发甲的模式会导致联调时间变长,联调时间变长意味着前端的效率会被降低。开发乙的模式看起来比较理想,但现实情况中开发乙并不会在postman中管理及维护这些测试用例,慢慢的这些所谓的测试用例就与其实现代码脱钩了,于是当某个功能发生变化需要对相关API进行回归测试时,便只能依托于测试小哥哥"点点点测试"。
针对上述情况,其实有另一种更合适的方案:API集成测试。
SpringBoot+Junit5示例
以下是通过SpringBoot+Junit5完成的一个最简易的API集成测试
Maven
spring-boot-starter-web提供MVC支持
spring-boot-starter-test提供了Junit支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Controller
定义了一个非常简单的API
@RestController
@AllArgsConstructor
@RequestMapping("/api")
public class ApiController {
@PostMapping("/order")
public OrderResp order() {
return OrderResp.builder().tranceNo(UUID.randomUUID().toString()).orderCreateTime(LocalDateTime.now().toString()).build();
}
}
@Data
@SuperBuilder
@NoArgsConstructor
public class OrderResp {
private String tranceNo;
private String orderCreateTime;
}
Test
test_order_success是为/api/order编写的一个测试用例,可以看到该测试用例规定了/api/order在特定情况下的行为,是"开发乙模式"的一种量化,当/api/order的行为被破坏时,该测试用例可以在回归测试阶段提前暴露风险。
例如:某开发人员在不知情的情况下修改了代码,删除了OrderResp中的tranceNo属性,此时由于/api/order的行为被破坏,test_order_success测试用例将执行失败,此时需要开发人员检查测试用例进行确认。
@SpringBootTest(classes = {IntegrationTestApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class IntegrationTestApplicationTests {
@Autowired
protected MockMvc mockMvc;
}
class ApiControllerTest extends IntegrationTestApplicationTests {
@Test
void test_order_success() throws Exception {
// 以POST方式请求/api/order,并且不携带任何请求参数时
mockMvc.perform(MockMvcRequestBuilders.post("/api/order"))
// 得到的HTTP响应状态码应该是200
.andExpect(MockMvcResultMatchers.status().isOk())
// 得到的http相应内容应该以json方式返回,并且tranceNo属性不能为空
.andExpect(MockMvcResultMatchers.jsonPath("$.tranceNo").isNotEmpty())
// 得到的http相应内容应该以json方式返回,并且orderCreateTime属性不能为空
.andExpect(MockMvcResultMatchers.jsonPath("$.orderCreateTime").isNotEmpty())
.andReturn();
}
}
思考
-
集成测试是什么?
集成测试其实是一个广泛的概念,本文所讲的集成测试或许应该称之为API测试,API测试属于集成测试的一个子集,它重点关注API的行为
-
集成测试与单元测试的区别是什么?
关注点不同:单元测试关注函数的行为,(API)集成测试关注API的行为
粒度不同:单元测试的粒度为单个函数/类,(API)集成测试的粒度为单个API
-
单元测试与集成测试的目标以及它们的适用场景?
单元测试的目标是在函数发生变化时,能够保证原有的函数行为不被破坏。
(API)集成测试的目标是在API内部发生变化时,能够保证原有的API行为不被破坏。
目前我接触到的公司都没有适合单元测试茁壮生长的土壤,这是因为国内的大环境导致的,许多开发者会把所有的业务逻辑直接堆积在Service层,这样一来代码的复用率极低,大量的一次性代码堆积在项目中,此时单元测试已经失去了原有的意义彻底沦落为测试覆盖率的工具,而API集成测试因为不关注API的内部变化,所以它仍然可以起到最基础的监测作用。
因此单元测试只适用于复用性较高或存在复用性的函数或类中(Util类就是一个很好的例子)。其实集成测试也是如此,如果一个API没有被外部使用,那么这个API就不存在外部行为,这个时候的集成测试其实也没有意义。
-
单元测试的函数行为与集成测试的Api行为具体指什么?
函数行为与API行为其实都是一个广泛的概念。
函数行为可以理解为函数返回值、是否抛异常等
API行为可以理解为http响应状态码、响应数据、是否超时等
-
集成测试的优势是什么?
可读性:当你对一个API不了解的时候,通过测试用例可以帮助你加深了解
可维护性:当代码发生行为变化时,集成测试可以检测到变化,从而进行变化确认并同步维护测试用例
可重复使用、可自动化:编写的测试用例可以在回归测试阶段产生巨大的作用,对于重构也能发挥一定的作用
误区
-
集成测试/单元测试没什么用
-
集成测试或单元测试只是为了满足测试覆盖率
-
在测试用例中关注了过多的实现细节
下面的例子中将“是否保存了订单、订单金额是否相等、订单状态是否等于PENDING”也都归类于API的行为之一
class ApiControllerTest extends IntegrationTestApplicationTests { @Autowired OrderMapper orderMapper; @Test void test_order_success() throws Exception { MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/api/order")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.tranceNo").isNotEmpty()) .andExpect(MockMvcResultMatchers.jsonPath("$.orderCreateTime").isNotEmpty()) .andReturn(); OrderResp resp = JSON.parseObject(result.getResponse().getContentAsString(), OrderResp.class); Order order = orderMapper.selectById(resp.getTranceNo()); Assertions.assertNotNull(order); Assertions.assertEquals(0, resp.getAmount().compareTo(order.getAmount())); Assertions.assertEquals("PENDING", order.getStatus()); } }
这样做确实可以检测到更多的变化,但同时也僵化了测试用例,因为它关注了太多的实现细节,所以任何一个细节产生的变化都会反应到该测试用例从而导致用例失败。当这类测试用例越来越多时,重构会变成了一件几乎不可能的事情,因为重构意味着推翻原有的技术实现,推翻原有的技术实现也就意味着大规模的测试用例都将执行失败。一个好的测试用例应该允许改变实现细节,而不允许改变外部行为。
总结
- 单元测试关注函数/类的行为,API集成测试关注API的行为
- 一次性代码和不会被使用到的API不适合为其编写测试用例
- 使用API集成测试可以在避免手动测试的同时收获一套自动化测试用例(这些测试用例在进行回归测试时,将产生巨大的作用)
- 一个好的测试用例应该允许改变实现细节,而不允许改变外部行为
相关技术栈及其概念介绍
-
Junit、Testng(测试框架)
测试用例的运行时容器,有点类似于Tomcat的概念,Junit与Testng的关系类似于Tomcat与Netty
不要混用Junit与Testng,在生成测试报告时他们会存在冲突(不要问我为什么知道(┭┮﹏┭┮))
-
TestContainer(中间件依赖工具)
TestContainer是依赖于Docker环境实现的一种集成测试工具,理论上它可以满足任何中间件(mysql、redis、mq等)的搭建。
-
Wrimock(第三方服务mock工具)
通过stub的方式来处理与第三方API的交互
-
Jacoco(测试覆盖率分析工具)
用来分析测试覆盖率并生成可视化报告。
-
SonarQube(代码扫描工具)
SonarQube可以扫描静态代码,而且还可以通过Jacoco生成的测试报告进行分析及展示。