Handling my custom exception in Spring MVC integration test

Viewed 1245

I have the following method in a controller class:

@PostMapping("employees")
  @ResponseStatus(HttpStatus.CREATED)
  public Employee addEmployee(@Valid @RequestBody Employee employee) {
    try {
      return employeeRepository.save(employee);
    } catch (DataIntegrityViolationException e) {
      e.printStackTrace();
      Optional<Employee> existingEmployee = employeeRepository.findByTagId(employee.getTagId());
      if (!existingEmployee.isPresent()) {
        //The exception root cause was not due to a unique ID violation then
        throw e;
      }
      throw new DuplicateEntryException(
          "An employee named " + existingEmployee.get().getName() + " already uses RFID tagID " + existingEmployee.get().getTagId());
    }
  }

Where the Employee class has a string field called tagId which has a @NaturalId annotation on it. (Please ignore that there is no dedicated service layer, this is a small and simple app).

Here is my custom DuplicateEntryException:

@ResponseStatus(HttpStatus.CONFLICT)
public class DuplicateEntryException extends RuntimeException {

  public DuplicateEntryException() {
    super();
  }

  public DuplicateEntryException(String message) {
    super(message);
  }

  public DuplicateEntryException(String message, Throwable cause) {
    super(message, cause);
  }

}

Thanks to the @ResponseStatus(HttpStatus.CONFLICT) line, when I manually test the method, I get the default spring boot REST message with the timestamp, status, error, message and path fields.

I'm still getting familiar with testing in Spring and I have this test:

@Test
  public void _02addEmployee_whenDuplicateTagId_thenExceptionIsReturned() throws Exception {
    Employee sampleEmployee = new Employee("tagId01", "John Doe");
    System.out.println("Employees in the database: " + repository.findAll().size()); //prints 1
    // @formatter:off
    mvc.perform(post("/employees").contentType(MediaType.APPLICATION_JSON).content(JsonUtil.toJson(sampleEmployee)))
       .andExpect(status().isConflict())
       .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
       .andExpect(jsonPath("$.message").value("An employee named John Doe already uses RFID tagID tagId01"));
    // @formatter:on

    int employeeCount = repository.findAll().size();
    Assert.assertEquals(1, employeeCount);
  }

As you can guess, there is another test that runs first, called _01addEmployee_whenValidInput_thenCreateEmployee(), which inserts an employee with the same tagID, which is used in test #2. Test #1 passes, but test #2 does not, because the HTTP response looks like this:

MockHttpServletResponse:
           Status = 409
    Error message = null
          Headers = {}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

And in the console before the above response, I see this:

Resolved Exception:
             Type = ai.aitia.rfid_employee.exception.DuplicateEntryException

So my 2nd test fails because java.lang.AssertionError: Content type not set. What causes the different behaviour compared to the manual testing? Why isn't this returned?

{
    "timestamp": "2019-01-03T09:47:33.371+0000",
    "status": 409,
    "error": "Conflict",
    "message": "An employee named John Doe already uses RFID tagID tagId01",
    "path": "/employees"
}

Update: I experienced the same thing with a different REST endpoint as well, where the test case resulted in my own ResourceNotFoundException, but the actual JSON error object was not received by the MockMvc object.

Update2: Here are my class level annotations for the test class:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = RfidEmployeeApplication.class)
@AutoConfigureMockMvc
@AutoConfigureTestDatabase
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@TestPropertySource(locations = "classpath:application-test.properties")
1 Answers

org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error fills full information for error response body, but for MockMvc it is not working. I just checked you can easily use in this case TestRestTemplate.

First just @Autowired private TestRestTemplate testRestTemplate; in test class.

and modify your test method for example:

ResponseEntity<String> response = testRestTemplate.postForEntity("/employees", sampleEmployee, String.class);
        String message = com.jayway.jsonpath.JsonPath.read(response.getBody(), "$.message");
        String expectedMessage = "An employee named John Doe already uses RFID tagID tagId01";
        Assert.assertEquals(expectedMessage, message);
        Assert.assertTrue(response.getStatusCode().is4xxClientError());
for example.
Related