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());
    }

}