Spring Boot中Mock RestTemplate测试返回HTTP 500问题排查
我有一个Spring Boot的Controller类:
package com.tsdevelopment.springbootrest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import java.util.Arrays; import java.util.List; @RestController public class SpringController { private List<String> myList = Arrays.asList("Item 1", "Item 2"); @Autowired RestTemplate restTemplate; @GetMapping("/v1/resources") public ResponseEntity<List<String>> getAllResources(@RequestParam(required = false) String parameter){ return ResponseEntity.ok(myList); } //API to API call @GetMapping("/v1/resources/inter-api-call") public ResponseEntity<List<String>> apiCall(){ List<String> apiResponse = restTemplate.getForEntity("http://localhost:8083/v1/resources", List.class).getBody(); return ResponseEntity.ok(apiResponse); } }
对应的测试类如下:
package com.tsdevelopment.springbootrest; import io.restassured.RestAssured; import io.restassured.http.ContentType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import java.util.Arrays; import java.util.List; import org.hamcrest.Matchers; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class SpringControllerTest { @InjectMocks SpringController springController; @Mock RestTemplate restTemplate; @LocalServerPort private int port; @BeforeEach public void setUp(){ RestAssured.port = port; MockitoAnnotations.openMocks(this); } @Test public void apiCall(){ Mockito.when(restTemplate.getForEntity("http://localhost:8083/v1/resources", List.class)).thenReturn(ResponseEntity.ok(Arrays.asList("Item 1", "Item 2"))); System.out.println("RestTemplate response: " + restTemplate.getForEntity("http://localhost:8083/v1/resources", List.class)); System.out.println("Controller response: " + springController.apiCall()); RestAssured.given() .when() .get("/v1/resources/inter-api-call") .then() .statusCode(200) .body("", Matchers.equalTo("[\"Item 1\",\"Item 2\"]")); } }
执行测试时失败,控制台打印的Mock调用和直接调用Controller的结果都是正常的:
RestTemplate response: <200 OK OK,[Item 1, Item 2],[]>
Controller response: <200 OK OK,[Item 1, Item 2],[]>
但测试报错:
java.lang.AssertionError: 1 expectation failed.
Expected status code <200> but was <500>.
你用@SpringBootTest启动了完整的Spring容器,但@Mock和@InjectMocks只是在测试类内部创建了一个被Mock注入的SpringController实例,容器中的SpringController实例并没有使用你Mock的RestTemplate。当RestAssured调用接口时,实际请求的是容器里的Controller,它的RestTemplate是真实的Bean,会去调用http://localhost:8083的真实服务,而这个服务大概率未启动,所以抛出500错误。
有两种方式可以解决这个问题:
方式一:使用@MockBean替代@Mock和@InjectMocks
@MockBean会把Mock的RestTemplate注入到Spring容器中,这样容器里的SpringController就会使用这个Mock实例:
修改测试类如下:
package com.tsdevelopment.springbootrest; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; import java.util.Arrays; import java.util.List; import org.hamcrest.Matchers; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class SpringControllerTest { @MockBean // 替换@Mock和@InjectMocks RestTemplate restTemplate; @LocalServerPort private int port; @BeforeEach public void setUp(){ RestAssured.port = port; } @Test public void apiCall(){ Mockito.when(restTemplate.getForEntity("http://localhost:8083/v1/resources", List.class)) .thenReturn(ResponseEntity.ok(Arrays.asList("Item 1", "Item 2"))); RestAssured.given() .when() .get("/v1/resources/inter-api-call") .then() .statusCode(200) .body("", Matchers.equalTo("[\"Item 1\",\"Item 2\"]")); } }
方式二:使用@WebMvcTest做切片测试
如果你只需要测试Controller层,不需要启动完整容器,可以用@WebMvcTest,配合@MockBean来Mock依赖:
package com.tsdevelopment.springbootrest; import io.restassured.module.mockmvc.RestAssuredMockMvc; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import java.util.Arrays; import java.util.List; import org.hamcrest.Matchers; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; @WebMvcTest(SpringController.class) public class SpringControllerTest { @Autowired private MockMvc mockMvc; @MockBean RestTemplate restTemplate; @BeforeEach public void setUp(){ RestAssuredMockMvc.mockMvc(mockMvc); } @Test public void apiCall(){ Mockito.when(restTemplate.getForEntity("http://localhost:8083/v1/resources", List.class)) .thenReturn(ResponseEntity.ok(Arrays.asList("Item 1", "Item 2"))); RestAssuredMockMvc.given() .when() .get("/v1/resources/inter-api-call") .then() .statusCode(200) .body("", Matchers.equalTo("[\"Item 1\",\"Item 2\"]")); } }
注意这种方式需要引入rest-assured-spring-mock-mvc依赖。
内容的提问来源于stack exchange,提问作者TSSK

