在使用@SpringBootTest测试时可以指定一个端口,如@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
或 @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
,这样在测试时会启动Spring内嵌的Http Server。 这时就可以使用一个RestTemplate
或者 TestRestTemplate
。
使用RANDOM_PORT和DEFINED_PORT的区别在于前着使用的是配置文件中的端口号(server.port
,默认值为8080),后者使用的是一个随机端口号。在进行并行测试的时候可以使用随机端口号以避免端口冲突。
看下测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WorkerControllerSpringBootTest { @Autowired private TestRestTemplate restTemplate; @Before public void setup() { System.out.println(); } @Test public void getWhenExists() { //when ResponseEntity<Worker> response = restTemplate.getForEntity("/worker/2", Worker.class); //then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isEqualTo(new Worker("raccoon", 23)); } @Test public void getWhenNotExists() { //when ResponseEntity<Worker> response = restTemplate.getForEntity("/worker/26", Worker.class); //then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(response.getBody()).isNull(); } @Test public void getByNameWhenExists() { //when ResponseEntity<Worker> response = restTemplate.getForEntity("/worker?name=HanMeimei", Worker.class); //then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isEqualTo(new Worker("HanMeimei", 16)); } @Test public void getByNameWhenNotExists() { //when ResponseEntity<Worker> response = restTemplate.getForEntity("/worker?name=LiLei", Worker.class); //then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNull(); } @Test public void add() { ResponseEntity<Worker> response = restTemplate.postForEntity("/worker", new Worker("Jerry", 12), Worker.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); } @Test public void workerFilter() { //when ResponseEntity<Worker> response = restTemplate.getForEntity("/worker/2", Worker.class); //then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getHeaders().get("X-CHOBIT-APP")).containsOnly("chobit-header"); } } |
Web Server测试
这里依然使用SpringRunner
执行测试。同时使用@SpringBootTest
注解的RANDOM_PORT
模式来得到一个内嵌的WebServer运行当前应用。
测试代码中使用RestTemplate
来触发请求,这个过程和使用外部服务器很像。
测试中的assertion现在有了一点儿变化,因为要验证的返回值由MockHttpServletResponse
变成了ResponseEntity
。
TestRestTemplate
因为使用了@SpringBootTest
注解,就可以使用@Autowired
注解来获取TestRestTemplate
的实例。这个TestRestTemplate
实例的作用和常见的RestTemplate
实例几乎没有任何区别,只是添加了一些额外的能力。实际上,可以将TestRestTemplate
视为RestTemplate
在测试环境的装饰类。
参与测试的角色关系如下图:
关于性能
也许我们会觉得第一个方案会更加有性能优势,因为它不需要加载Spring Context。事实上确实如此。但是即使加载了SpringContext,也不会造成特别恐怖的影响,因为在同一个Test Suite中,已经加载的Spring Context是可以重用的。
不过Spring Context重用也会导致一些问题,比如一些测试方法对公共的Bean做了修改就可能会影响到其他测试方法。此时可以使用@DirtiesContext
注解来要求重新加载Context。
总结
到现在为止我们从轻到重共介绍了四种SpringBoot Controller测试方案。
虽然我们的目标一直都是对Controller层进行测试,但是从第一种测试方案(Standalone MockMVC)到现在,测试的角度还是有些变化的。一开始我们只是会加载测试的Controller类,却不会加载其周边的一些角色如Filter或Advice。到现在这个方案里我们启动了内嵌的WebServer,加载了整个SpringBoot Context。
目前这个方案是提到的四个测试方案里最重的一个,也是离单元测试的概念最远的一个。
下面说几个使用建议:
- 如果在单元测试中关注Controller的逻辑,优先选择第一种方案:Standalone MockMVC方案;
- 如果要测试Web层的其它角色(Filter或Advice)的行为,优先选择第四种测试方案执行集成测试;
- 避免将单元测试和集成测试混在一起,最好分开来写。
其他
- Spring Controller测试 – 01 概述
- Spring Controller测试 – 02 Standalone MockMVC
- Spring Controller测试 – 03 WebContext & MockMVC
- Spring Controller测试 – 04 SpringBootTest & MockMVC
- Spring Controller测试 – 05 SpringBootTest & WebServer
示例代码可在CSDN下载,地址:https://download.csdn.net/download/tianxiexingyun/11065824
参考文档:https://thepracticaldeveloper.com/2017/07/31/guide-spring-boot-controller-tests/
发表评论