3

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")
logi0517
  • 813
  • 1
  • 13
  • 32
  • are you using a @RestController or @Controller? – stacker Jan 03 '19 at 10:49
  • yes, the former – logi0517 Jan 03 '19 at 10:55
  • like I said, that's the one I'm already using – logi0517 Jan 03 '19 at 11:02
  • have you tried @PostMapping(value="employees",produces = "application/json")? – stacker Jan 03 '19 at 11:06
  • i've just tried it, it did not make a difference – logi0517 Jan 03 '19 at 11:09
  • which annotations are you using to configure your test class? – jumb0jet Jan 03 '19 at 15:27
  • Check did you initialize your exception handler for your `DuplicateEntryException`. See example https://stackoverflow.com/a/27195332/5333879 – borino Jan 04 '19 at 09:50
  • i dont have a dedicated exception handler for it, based on the spring documentation I've read, adding the response status annotation is enough for the default exception handling. and it works too, when the rest endpoint is called normally. – logi0517 Jan 04 '19 at 10:21
  • @jumb0jet see update2 – logi0517 Jan 04 '19 at 10:21
  • `@ResponseStatus` handled by exceptionResolver, please check that `ResponseStatusExceptionResolver` presented in test context, or add on test class `@Import(ResponseStatusExceptionResolver.class)`. But i created small demo project with parts of code that you presented, all work without any additional annotations, maybe your Exception did not scanned during test creation, for this we need sources of your `RfidEmployeeApplication`, because it responsible and determine of spring beans scanning – borino Jan 08 '19 at 06:04
  • Adding that import statement to the Test class level did not help. How can I check what beans are discovered during tests? Should I maybe print them out in the main method of the application class? Because in IntelliJ the UI is different for test runs, cant seem to find it there. – logi0517 Jan 08 '19 at 10:13

1 Answers1

3

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.
borino
  • 1,720
  • 1
  • 16
  • 24
  • Big thanks for your persistent help. I wonder If I should just stop using MockMvc alltogether in favour of TestRestTemplate then. – logi0517 Jan 09 '19 at 08:33
  • Although reading up about it, it looks like it's better to keep using MockMvc whereever possible, for server side tests. – logi0517 Jan 09 '19 at 08:36