Spring 引导中的 REST API 的 JUnit 测试失败
Failing JUnit test for REST API in Spring Boot
我在 运行 针对 Spring Boot REST 控制器的 JUnit 测试时遇到异常。我通过 Postman 测试了 API 并且它按预期工作。不确定我在 JUnit 测试中缺少什么。
ProductController.java
@RestController
@RequestMapping("/api")
public class ProductController {
@Inject
private ProductRepository productRepository;
//URI: http://localhost:8080/api/products/50
@RequestMapping(value = "/products/{productId}", method = RequestMethod.GET)
public ResponseEntity<?> getProduct(@PathVariable Long productId) {
verifyProductExists(productId);
Product product = productRepository.findOne(productId);
return new ResponseEntity<>(product, HttpStatus.OK);
}
protected void verifyProductExists(Long productId) throws ResourceNotFoundException {
Product product = productRepository.findOne(productId);
if (product == null) {
throw new ResourceNotFoundException("Product with id " + productId + " not found...");
}
}
}
ResourceNotFoundException.java
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ResourceNotFoundException() {
}
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
通过邮递员:
http://localhost:8080/api/products/1 -> Returns 200 with Product data in JSON format
http://localhost:8080/api/products/999 -> Returns 404 with Exception data in JSON format
ProductRestClientTest.java
@RunWith(SpringJUnit4ClassRunner.class)
public class ProductRestClientTest {
static final String VALID_PRODUCT_API_URI = "http://localhost:8080/api/products/35";
static final String INVALID_PRODUCTS_API_URI = "http://localhost:8080/api/products/555";
private RestTemplate restTemplate;
@Before
public void setUp() {
restTemplate = new RestTemplate();
}
/*
Testing Happy Path scenario
*/
@Test
public void testProductFound() {
ResponseEntity<?> responseEntity = restTemplate.getForEntity(VALID_PRODUCT_API_URI, Product.class);
assert (responseEntity.getStatusCode() == HttpStatus.OK);
}
/*
Testing Error scenario
*/
@Test(expected = ResourceNotFoundException.class)
public void testProductNotFound() {
ResponseEntity<?> responseEntity = restTemplate.getForEntity(INVALID_PRODUCTS_API_URI, Product.class);
assert (responseEntity.getStatusCode() == HttpStatus.NOT_FOUND);
}
@After
public void tearDown() {
restTemplate = null;
}
}
在 运行 以上 JUnit 测试时出现异常
Tests run: 2, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.759 sec <<< FAILURE! - in com.study.spring.boot.rest.ProductRestClientTest
testProductNotFound(com.study.spring.boot.rest.ProductRestClientTest) Time elapsed: 0.46 sec <<< ERROR!
java.lang.Exception: Unexpected exception, expected<com.study.spring.boot.rest.ResourceNotFoundException> but was<org.springframework.web.client.HttpClientErrorException>
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:312)
at com.study.spring.boot.rest.ProductRestClientTest.testProductNotFound(ProductRestClientTest.java:42)
重点是您返回的不是异常,而是包含正文和 http 状态代码的 http 消息。在这种情况下,您会得到一个 404 代码,但没有人在异常中翻译此代码。
为了获得所需的异常,您需要指示 restTemplate 在遇到 404 时抛出 ResourceNotFoundException。
基本上你需要一个错误处理程序:
RestTemplate restclient = new RestTemplate();
restclient.setErrorHandler(新的 MyResponseErrorHandler());
希望对您有所帮助。
测试的问题是 RestTemplate
的 404 响应触发了 DefaultResponseErrorHandler
方法 handleError(ClientHttpResponse response)
。
在您的情况下(返回您的 404 状态代码 -> 客户端错误)它会导致 HttpClientErrorException
:
HttpStatus statusCode = getHttpStatusCode(response);
switch (statusCode.series()) {
case CLIENT_ERROR:
throw new HttpClientErrorException(statusCode, response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
至少有两个解决方案:
要么禁用测试中的默认错误处理,要么增强您的 setUp()
方法,例如:
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
protected boolean hasError(HttpStatus statusCode) {
return false;
}});
并从负面测试中删除 (expected = ResourceNotFoundException.class)
子句。因为在获得响应后断言 404 并期待异常不会一起工作。
或使用MockMvc。它提供了更复杂的东西,并默认跳过 DefaultResponseErrorHandler。
例如,您的测试可能如下所示:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class ProductRestClientTestWithMockMvc {
private static final String PRODUCT_API_URI = "http://localhost:8080/api/products/{productId}";
private MockMvc mockMvc = null;
@Autowired
private WebApplicationContext webApplicationContext;
@Before
public void before() throws Exception {
mockMvc = webAppContextSetup(webApplicationContext).build();
}
@After
public void after() throws Exception {
mockMvc = null;
}
/*
* Testing Happy Path scenario
*/
@Test
public void testProductFound() throws Exception {
final MockHttpServletRequestBuilder builder = get(PRODUCT_API_URI, 35);
final ResultActions result = mockMvc.perform(builder);
result.andExpect(status().isOk());
}
/*
* Testing Error scenario
*/
@Test
public void testProductNotFound() throws Exception {
final MockHttpServletRequestBuilder builder = get(PRODUCT_API_URI, 555);
final ResultActions result = mockMvc.perform(builder);
result.andExpect(status().isNotFound());
}
}
我在 运行 针对 Spring Boot REST 控制器的 JUnit 测试时遇到异常。我通过 Postman 测试了 API 并且它按预期工作。不确定我在 JUnit 测试中缺少什么。
ProductController.java
@RestController
@RequestMapping("/api")
public class ProductController {
@Inject
private ProductRepository productRepository;
//URI: http://localhost:8080/api/products/50
@RequestMapping(value = "/products/{productId}", method = RequestMethod.GET)
public ResponseEntity<?> getProduct(@PathVariable Long productId) {
verifyProductExists(productId);
Product product = productRepository.findOne(productId);
return new ResponseEntity<>(product, HttpStatus.OK);
}
protected void verifyProductExists(Long productId) throws ResourceNotFoundException {
Product product = productRepository.findOne(productId);
if (product == null) {
throw new ResourceNotFoundException("Product with id " + productId + " not found...");
}
}
}
ResourceNotFoundException.java
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ResourceNotFoundException() {
}
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
通过邮递员:
http://localhost:8080/api/products/1 -> Returns 200 with Product data in JSON format
http://localhost:8080/api/products/999 -> Returns 404 with Exception data in JSON format
ProductRestClientTest.java
@RunWith(SpringJUnit4ClassRunner.class)
public class ProductRestClientTest {
static final String VALID_PRODUCT_API_URI = "http://localhost:8080/api/products/35";
static final String INVALID_PRODUCTS_API_URI = "http://localhost:8080/api/products/555";
private RestTemplate restTemplate;
@Before
public void setUp() {
restTemplate = new RestTemplate();
}
/*
Testing Happy Path scenario
*/
@Test
public void testProductFound() {
ResponseEntity<?> responseEntity = restTemplate.getForEntity(VALID_PRODUCT_API_URI, Product.class);
assert (responseEntity.getStatusCode() == HttpStatus.OK);
}
/*
Testing Error scenario
*/
@Test(expected = ResourceNotFoundException.class)
public void testProductNotFound() {
ResponseEntity<?> responseEntity = restTemplate.getForEntity(INVALID_PRODUCTS_API_URI, Product.class);
assert (responseEntity.getStatusCode() == HttpStatus.NOT_FOUND);
}
@After
public void tearDown() {
restTemplate = null;
}
}
在 运行 以上 JUnit 测试时出现异常
Tests run: 2, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.759 sec <<< FAILURE! - in com.study.spring.boot.rest.ProductRestClientTest
testProductNotFound(com.study.spring.boot.rest.ProductRestClientTest) Time elapsed: 0.46 sec <<< ERROR!
java.lang.Exception: Unexpected exception, expected<com.study.spring.boot.rest.ResourceNotFoundException> but was<org.springframework.web.client.HttpClientErrorException>
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:312)
at com.study.spring.boot.rest.ProductRestClientTest.testProductNotFound(ProductRestClientTest.java:42)
重点是您返回的不是异常,而是包含正文和 http 状态代码的 http 消息。在这种情况下,您会得到一个 404 代码,但没有人在异常中翻译此代码。 为了获得所需的异常,您需要指示 restTemplate 在遇到 404 时抛出 ResourceNotFoundException。 基本上你需要一个错误处理程序:
RestTemplate restclient = new RestTemplate(); restclient.setErrorHandler(新的 MyResponseErrorHandler());
希望对您有所帮助。
测试的问题是 RestTemplate
的 404 响应触发了 DefaultResponseErrorHandler
方法 handleError(ClientHttpResponse response)
。
在您的情况下(返回您的 404 状态代码 -> 客户端错误)它会导致 HttpClientErrorException
:
HttpStatus statusCode = getHttpStatusCode(response);
switch (statusCode.series()) {
case CLIENT_ERROR:
throw new HttpClientErrorException(statusCode, response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
至少有两个解决方案:
要么禁用测试中的默认错误处理,要么增强您的 setUp()
方法,例如:
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
protected boolean hasError(HttpStatus statusCode) {
return false;
}});
并从负面测试中删除 (expected = ResourceNotFoundException.class)
子句。因为在获得响应后断言 404 并期待异常不会一起工作。
或使用MockMvc。它提供了更复杂的东西,并默认跳过 DefaultResponseErrorHandler。
例如,您的测试可能如下所示:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class ProductRestClientTestWithMockMvc {
private static final String PRODUCT_API_URI = "http://localhost:8080/api/products/{productId}";
private MockMvc mockMvc = null;
@Autowired
private WebApplicationContext webApplicationContext;
@Before
public void before() throws Exception {
mockMvc = webAppContextSetup(webApplicationContext).build();
}
@After
public void after() throws Exception {
mockMvc = null;
}
/*
* Testing Happy Path scenario
*/
@Test
public void testProductFound() throws Exception {
final MockHttpServletRequestBuilder builder = get(PRODUCT_API_URI, 35);
final ResultActions result = mockMvc.perform(builder);
result.andExpect(status().isOk());
}
/*
* Testing Error scenario
*/
@Test
public void testProductNotFound() throws Exception {
final MockHttpServletRequestBuilder builder = get(PRODUCT_API_URI, 555);
final ResultActions result = mockMvc.perform(builder);
result.andExpect(status().isNotFound());
}
}