集成测试应该怎么写?

364 阅读4分钟

一、开始

上一篇介绍了测试的基本用法,接下来可以更进一步了,这一篇介绍基于 Spring boot test 的 WebTestClient 怎么编写集成测试。

常用的接口包括列表搜索、新增、查询详情、修改、删除和文件上传,我们接下来就通过这几个接口来详细了解对应的集成测试应该怎么写。

由于 WebTestClient 用到了 spring-webflux, 所以需要在 .gradle 文件中引入下面的依赖。

implementation 'org.springframework.boot:spring-boot-starter-webflux'

引入 webflux 依赖后,刷新 gradle,会下载相应的依赖包。

好了,准备工作已完成,开始我们集成测试之旅。

二、测试列表搜索用户

假如我们查询一个用户列表,以下是对应的代码(这里是 Mock 数据,返回2条数据,主要是用来演示测试)。

@RestController
@RequestMapping("users")
public class UserController {

    @GetMapping("")
    public Object search() {
        Map<String, String> zhangSan = Map.of("id", "zhangsan", "name", "Zhangsan");
        Map<String, String> liShi = Map.of("id", "lishi", "name", "Lishi");
        return List.of(zhangSan, liShi);
    }
}

对应的测试代码如下:

@SpringBootTest
public class SearchUsersTest {

    private WebTestClient testClient;

    @BeforeEach
    void setUp(WebApplicationContext applicationContext) { (1)
        this.testClient = MockMvcWebTestClient.bindToApplicationContext(applicationContext).build();
    }

    @Test
    void should_be_able_to_search_users() {
        testClient.get().uri("/users") (2)
                .accept(MediaType.APPLICATION_JSON) (3)
                .exchange()
                .expectStatus().isOk() (4)
                .expectHeader().contentType(MediaType.APPLICATION_JSON) (5)
                .expectBody() (6)
                .jsonPath("$[0].id").isEqualTo("zhangsan")
                .jsonPath("$[0].name").isEqualTo("Zhangsan")
                .jsonPath("$[1].id").isEqualTo("lishi")
                .jsonPath("$[1].name").isEqualTo("Lishi");
    }

}

(1). 在 setUp 方法里通过 WebApplicationContext 初始化 WebTestClient;

(2). 以 get 方式请求 /users;

(3). 请求头设置为 APPLICATION_JSON ,如果不是 json 数据,可以更改为相应的;

(4). 验证 HTTP 请求返回的状态,这里验证是否 200,其他状态码看对应的API;

(5). 验证 HTTP 请求返回的响应头,这里验证是否 APPLICATION_JSON;

(6). 验证返回的数据,用 jsonPath 直接验证数据是否正确。

到此,对集成测试有一定的了解了,接下来继续完善下面几个测试。

三、测试创建用户

通过 POST 方法创建用户,返回状态码为 201,并且返回用户的ID。

@RestController
@RequestMapping("users")
public class UserController {
  
    @PostMapping("")
    @ResponseStatus(HttpStatus.CREATED)
    public Object create(@RequestBody Map<String, Object> data) {
        System.out.println(data);
        return Map.of("id", "1");
    }

}

对应的测试代码如下:

@SpringBootTest
public class CreateUserTest {

    private WebTestClient testClient;

    @BeforeEach
    void setUp(WebApplicationContext applicationContext) {
        this.testClient = MockMvcWebTestClient.bindToApplicationContext(applicationContext).build();
    }    

    @Test    
    void should_be_able_to_create_user() {
        testClient.post().uri("/users")
                .bodyValue(Map.of("name", "ZhangSan", "age", 20))
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isCreated()
                .expectHeader().contentType(MediaType.APPLICATION_JSON)
                .expectBody()
                .jsonPath("$.id").isEqualTo("1");
        // TODO 一般在创建数据后,需要用对应的ID查询数据库中数据是否正确
        // verify user data

        // TODO 最后不要忘记删除接口创建的数据,不然在某些情况下影响其他测试
        // delete created user
    }

}

这个测试就没啥好说的,基本都可以看得懂,需要注意的是后面的2个 TODO:

1. 一定要在创建后验证数据是否正确,这里是因为创建的接口没有真实写数据库,所以我忽略了。

2. 最后不要忘记删除刚创建的数据,不然在某些情况下会影响其他测试。

四、测试查询用户详情

废话不用多说,直接上代码:

@RestController
@RequestMapping("users")
public class UserController {

    @GetMapping("/{id}")
    public Object getDetails(@PathVariable String id) {
        return Map.of("id", id, "name", "ZhangSan", "age", 20);
    }
}

对应的测试代码如下:

@SpringBootTest
public class GetUserDetailsTest {

    private WebTestClient testClient;

    @BeforeEach
    void setUp(WebApplicationContext applicationContext) {
        this.testClient = MockMvcWebTestClient.bindToApplicationContext(applicationContext).build();
    }    
    
    @Test    
    void should_be_able_to_get_user_details() {
        testClient.get().uri("/users/{id}", Map.of("id", "zhangsan"))
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectHeader().contentType(MediaType.APPLICATION_JSON)
                .expectBody()
                .jsonPath("$.id").isEqualTo("zhangsan")
                .jsonPath("$.name").isEqualTo("ZhangSan")
                .jsonPath("$.age").isEqualTo(20);
    }

}

通过 GET 方式请求 /users/zhangsan,在最后验证响应数据是否正确就好了。

五、测试修改用户

修改接口代码(这里也是 Mock 的实现,修改逻辑需要自定义)

@RestController
@RequestMapping("users")
public class UserController {

    @PutMapping("/{id}")
    public Object update(@PathVariable String id, @RequestBody Map<String, Object> payload) {
        System.out.println(payload);
        // TODO do update ...
        return Map.of("id", id);
    }

}

对应的测试代码:

@SpringBootTest
public class UpdateUserTest {

    private WebTestClient testClient;

    @BeforeEach
    void setUp(WebApplicationContext applicationContext) {
        this.testClient = MockMvcWebTestClient.bindToApplicationContext(applicationContext).build();
        // TODO init user
    }

    @Test
    void should_be_able_to_update_user() {
        testClient.put().uri("/users/{id}", Map.of("id", "zhangsan"))
                .bodyValue(Map.of("name", "LiShi", "age", 30))
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectHeader().contentType(MediaType.APPLICATION_JSON)
                .expectBody()
                .jsonPath("$.id").isEqualTo("zhangsan");
        // TODO 一般在修改数据后,需要用对应的ID查询数据库中数据是否正确
        // verify user data
    }
    
    @AfterEach
    void tearDown() {
        // TODO remove init data
    }
}

这里需要注意调用接口前需要初始化需要修改的数据,然后就是在 @AfterEach  方法里移除初始化的数据,不然也可能会影响其他测试。

六、测试删除用户

删除接口

@RestController
@RequestMapping("users")
public class UserController {

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable String id) {
        // TODO do delete ...
    }

}

对应的测试代码:

@SpringBootTest
public class DeleteUserTest {

    private WebTestClient testClient;

    @BeforeEach
    void setUp(WebApplicationContext applicationContext) {
        this.testClient = MockMvcWebTestClient.bindToApplicationContext(applicationContext).build();
        // init data
    }

    @Test
    void should_be_able_to_create_user() {
        testClient.delete().uri("/users/{id}", Map.of("id", "zhangsan"))
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isNoContent()
                .expectBody()
                .isEmpty();
    }

    @AfterEach
    void tearDown() {
        // TODO remove init data
    }

}

七、总结

这篇文章介绍了常用接口的测试编写方法,有了这些测试方法,我们就可以随时跑这些测试是否正确,如果测试失败就说明接口或者测试有问题了,就需要做相应的修改,保证测试和接口的正确性。

有了这些测试,就相当于业务接口是有测试保障的。如果日后接口有修改,就可以放心修改,有测试替我们守护一道防线。

测试不光是代码的安全网,还可以在重构的时候提供帮助。上方的测试代码是不是有很多重复,所以也需要重构,下篇继续。

如果有疑问或者需要源代码,请关注下面公众号交流或者获取。

附:

清山绿水始于尘,博学多识贵于勤。
微信公众号:「清尘闲聊」。
欢迎一起谈天说地,聊代码。