文章目录
之前写过一篇单元测试相关的文章,细心的同学会发现,单元测试其实是面向后端代码层面的测试,它只能保证单个函数或单个类的行为正常,并不能保证API正常,然而后端开发人员最终需要交付的其实是一个功能正常的API,那么应该如何保证API的功能正常呢? 开发甲:我会开发完成后直接将API交给前端进行联调,联调的过程中出现问题我再处理。 开发乙:我会通过Postman工具来手动模拟用户请求,然后观察API行为以及数据是否正常,然后我才会将API交给前端进行联调。 开发甲的模式会导致联调时间变长,联调时间变长意味着前端的效率会被降低。开发乙的模式看起来比较理想,但现实情况中开发乙并不会在postman中管理及维护这些测试用例,慢慢的这些所谓的测试用例就与其实现代码脱钩了,于是当某个功能发生变化需要对相关API进行回归测试时,便只能依托于测试小哥哥"点点点测试"。 针对上述情况,其实有另一种更合适的方案:API集成测试。
以下是通过SpringBoot+Junit5完成的一个最简易的API集成测试
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>
定义了一个非常简单的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_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();
}
}
目前我接触到的公司都没有适合单元测试茁壮生长的土壤,这是因为国内的大环境导致的,许多开发者会把所有的业务逻辑直接堆积在Service层,这样一来代码的复用率极低,大量的一次性代码堆积在项目中,此时单元测试已经失去了原有的意义彻底沦落为测试覆盖率的工具,而API集成测试因为不关注API的内部变化,所以它仍然可以起到最基础的监测作用。 因此单元测试只适用于复用性较高或存在复用性的函数或类中(Util类就是一个很好的例子)。其实集成测试也是如此,如果一个API没有被外部使用,那么这个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());
}
}
这样做确实可以检测到更多的变化,但同时也僵化了测试用例,因为它关注了太多的实现细节,所以任何一个细节产生的变化都会反应到该测试用例从而导致用例失败。当这类测试用例越来越多时,重构会变成了一件几乎不可能的事情,因为重构意味着推翻原有的技术实现,推翻原有的技术实现也就意味着大规模的测试用例都将执行失败。一个好的测试用例应该允许改变实现细节,而不允许改变外部行为。
- END -