Search code examples
javaspringspring-bootjunitspring-restcontroller

Failing JUnit test for REST API in Spring Boot


I am getting an exception while running a JUnit test against a Spring Boot REST controller. I tested the API through Postman and it works as expected. Not sure what I am missing in JUnit test.

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

}

Through Postman:

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

}

Exception while running above JUnit test

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)

Solution

  • The problem with the test is that with the 404 response of the RestTemplate the DefaultResponseErrorHandler method handleError(ClientHttpResponse response) is triggered.

    In your case (returning your 404 status code -> client error) it causes a HttpClientErrorException:

    HttpStatus statusCode = getHttpStatusCode(response);
        switch (statusCode.series()) {
            case CLIENT_ERROR:
                throw new HttpClientErrorException(statusCode, response.getStatusText(),
                        response.getHeaders(), getResponseBody(response), getCharset(response));
    

    There are at least two solutions for that:

    Either disabling the default error handling in your tests, maybe enhance your setUp() method like:

       restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
            protected boolean hasError(HttpStatus statusCode) {
                return false;
            }});
    

    And remove the (expected = ResourceNotFoundException.class) clause from your negative test. Because asserting 404 after getting the response and expecting a exception won't work together.

    Or use MockMvc. It offers even more sophisticated stuff and skips the DefaultResponseErrorHandler per default.

    For example your test could look like this:

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