场景
单元测试是个让人很纠结的东东,dealline 催的很紧,不想写单测,但当项目复杂到一定程度需要重构时,返现没有单测,不敢随便改代码,生怕”敲一锤子,倒一大片“,但此时再补,已经有点晚了。磨刀不误砍柴功,这会偷的懒后面总要跟你算账的,所以还乖乖写吧。
单测目的
这个很重要,不清楚目的,就不知道接下来该怎么做。这样也行,那样好像也可以,埋头写一大片,很快就没了兴趣,而且还会很烦写单测。常见 web 服务大都是 controller、service、repository 三层架构。每一层都有各自的任务,基于此单测的目的也就不同。
- repository 层主要负责数据的持久化,它里面除了读和写,其他的逻辑越少越好。所以该层单测要连上数据库,将数据真正的写入并且读出,从而确认该数据持久化,以及各种查找功能正确。推荐持久化层单测内嵌 H2 数据库,它是基于内存的,不受物理机限制,而且各个单测 case 之间互无影响。
- service 层负责处理业务逻辑,很复杂的项目往往也就是复杂在这一层了,这层肯定会依赖该项目的 repository 层,还可能会依赖 Redis、kafka、mq、httpClinet 等第三方的东西。每次单测都调用第三方,哪有那么听话的第三方,每次都能正确返回结果。这层单测怎么写?把 service 层依赖的 repository 和 第三方的东西都 mock 出来,并 when 他们对应的方法,按照测试需求控制其方法返回值,从而控制 service 层的方法中代码的走向,确认自己写的逻辑是没问题的。在这一层中,一个方法往往需要好几个测试 case,一个正例,若干的异常情况。
- controller 层负责和前端交互,接收请求、校验参数、返回处理结果。最理想的单测肯定是模拟出生产环境下前端请求的执行。这是可以办到的。
三层单测有各自的目的,而且不依赖于其余两层,如果真依赖了,那就使用 mock,控制返回值。从而实现“隔离”,本层单测只需要实现本层的目的,其余的都是不需要知道的。
controller 层单测
mockMVC 可以实现在单测中模拟生产环境中前端请求的执行逻辑。
Controller:
@RestController
@RequestMapping("/trains")
public class TrainController {
private TrainService trainService;
@Autowired
public TrainController(TrainService trainService) {
this.trainService = trainService;
}
@PostMapping("")
public void create(@RequestBody @Validated ModelTrainsAddDTO req) {
TrainEntity entity = new TrainEntity();
BeanUtils.copyProperties(req, entity);
trainService.create(entity);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ModelTrainsAddDTO {
@NotBlank
private String name;
private String modelType;
}
单元测试
@WebMvcTest(TrainController.class)
class TrainControllerTest {
@MockBean
private TrainService trainService;
@Autowired
private MockMvc mockMvc;
@BeforeEach
void setUp() {
RestAssuredMockMvc.mockMvc(mockMvc);
}
@Test
void should_200_if_create_train_succeed() {
ModelTrainsAddDTO param = new ModelTrainsAddDTO("name", "model-type", "trian-fileds", "evail-files", "param");
given()
.contentType(ContentType.JSON)
.body(param)
.when()
.post("/trains")
.prettyPeek()
.then()
.statusCode(200);
verify(trainService).create(any());
}
@Test
void should_500_if_name_is_null_when_create_train() {
ModelTrainsAddDTO param = new ModelTrainsAddDTO(null, "model-type", "trian-fileds", "evail-files", "param");
given()
.contentType(ContentType.JSON)
.body(param)
.when()
.post("/trains")
.prettyPeek()
.then()
.statusCode(500);
}
}
RestAssuredMockMvc
RestAssuredMockMvc 详细使用方法在这里,需要科学上网。它采用熟悉的 given-when-then 的场景格式定义测试写法,里面有段这样的描述:
given() — specifies the HTTP request details
when() — specifies the HTTP verb as well as the route
then() — validates the HTTP response
当然也还有别的写法,比如:mockMvc.perform() 。
依赖
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>4.5.1</version>
<scope>test</scope>
</dependency>
|