1

I'm testing a @RestContoller in Spring Boot which has a @PostMapping method and the method @RequestBody is validated using @Valid annotation. To test it, I'm using MockMvc and in order to populate request body content I'm using Jackson ObjectMapper; however, when the model is passed, the test fails:

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/user/register
       Parameters = {}
          Headers = [Content-Type:"application/json"]
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = com.springboottutorial.todoapp.controller.UserController
           Method = public org.springframework.http.ResponseEntity<java.lang.String> com.springboottutorial.todoapp.controller.UserController.register(com.springboottutorial.todoapp.dao.model.User)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = org.springframework.http.converter.HttpMessageNotReadableException

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

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

java.lang.AssertionError: Status 
Expected :200
Actual   :400

User Model:

@Entity
@Table(name = "users",
        uniqueConstraints = @UniqueConstraint(columnNames = {"EMAIL"}))
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private long id;

@Column(name = "FIRST_NAME")
@NotNull
private String firstName;

@Column(name = "LAST_NAME")
@NotNull
private String lastName;

@Column(name = "EMAIL")
@NotNull
@Email
private String emailAddress;

@Column(name = "PASSWORD")
@NotNull
private String password;

@Column(name = "CREATED_AT")
@NotNull
@Convert(converter = LocalDateTimeConveter.class)
private LocalDateTime createdAt;

@Column(name = "UPDATED_AT")
@NotNull
@Convert(converter = LocalDateTimeConveter.class)
private LocalDateTime updatedAt;

public User(@NotNull String firstName, @NotNull String lastName,
                @NotNull @Email String emailAddress, @NotNull String password,
                @NotNull LocalDateTime createdAt, @NotNull LocalDateTime updatedAt) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.emailAddress = emailAddress;
        this.password = password;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }

//setters and getters: omitted

UserController:

@RestController
@RequestMapping("/api/user")
public class UserController {

    @Autowired
    UserService userService;

    @PostMapping("/register")
    public ResponseEntity<String> register(@RequestBody @Valid User user){
        userService.createUser(user);
        return ResponseEntity.ok().build();
    }
}

UserControllerTest:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
public class UserControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void whenRequestValid_thenReturnStatusOk() throws Exception{
        User user = new User("John", "QPublic", "john.public@gmail.com",
                "123456789", LocalDateTime.now(), LocalDateTime.now());      
        mockMvc.perform(MockMvcRequestBuilders.post("/api/user/register")
                .content(new ObjectMapper().writeValueAsString(user))
                .contentType(MediaType.APPLICATION_JSON)
                )
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

When I build JSON string manually, the test passes:

String json = "{\n" +
                "\t\"firstName\" : \"John\",\n" +
                "\t\"lastName\" : \"QPublic\",\n" +
                "\t\"password\" : \"123456789\",\n" +
                "\t\"createdAt\" : \"2016-11-09T11:44:44.797\",\n" +
                "\t\"updatedAt\" : \"2016-11-09T11:44:44.797\",\n" +
                "\t\"emailAddress\" : \"john.public@gmail.com\"\n" +
                "}";

        mockMvc.perform(MockMvcRequestBuilders.post("/api/user/register")
                .content(json)
                .contentType(MediaType.APPLICATION_JSON)
                )
                .andExpect(MockMvcResultMatchers.status().isOk());
Shahin
  • 253
  • 1
  • 3
  • 13
  • Did you run the application for checking if the endpoint is working with Postman or another similar tool ?. Please include your `pom.xml` in your answer – eHayik Aug 03 '19 at 17:50
  • 2
    Did you try printing out the string returned by `writeValueAsString` to validate it? (I _very_ strongly suspect you're getting a "timestamp" format, and don't use `Local*` for anything except user calendars; use `Instant` instead.) – chrylis -cautiouslyoptimistic- Aug 03 '19 at 18:10
  • @EduardoEljaiek Yes, it runs properly when I use postman tool. – Shahin Aug 03 '19 at 18:19
  • @chrylis Thanks for your suggestion! LocalDateTime parsed to days, month, years when mapped to JSON string so the conflict came up. I made LocalDateTime nullable and this time it worked out. I'm trying to come up with a solid solution and post the answer. – Shahin Aug 04 '19 at 04:55

4 Answers4

2

Spring is not necessarily providing you with a "vanilla" ObjectMapper instance. By having Spring inject the ObjectMapper instance into the test instead of creating an ObjectMapper with the default constructor, you will get an instance that matches your actual run-time environment, provided that your Spring profile for your unit tests is set up correctly.

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
public class UserControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void whenRequestValid_thenReturnStatusOk() throws Exception{
        User user = new User("John", "QPublic", "john.public@gmail.com",
                "123456789", LocalDateTime.now(), LocalDateTime.now());      
        mockMvc.perform(MockMvcRequestBuilders.post("/api/user/register")
                .content(objectMapper.writeValueAsString(user))
                .contentType(MediaType.APPLICATION_JSON)
                )
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}
AlexElin
  • 1,044
  • 14
  • 23
jlar310
  • 527
  • 4
  • 14
0

This is how I implement my rest controllers test. Maybe It could help you.

I've this abstract class to encapsulate common tests functionalities regarding to the JSON mapping.

    import lombok.SneakyThrows;
    import lombok.val;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.MediaType;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
    import org.springframework.mock.http.MockHttpOutputMessage;

    import java.nio.charset.StandardCharsets;
    import java.util.Arrays;

    import static org.junit.Assert.assertNotNull;

    public abstract class RestControllerTest {

        private final MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
                MediaType.APPLICATION_JSON.getSubtype(),
                StandardCharsets.UTF_8);

        private HttpMessageConverter messageConverter;

        protected MediaType getContentType() {
            return contentType;
        }

        @Autowired
        void setConverters(HttpMessageConverter<?>[] converters) {
            messageConverter = Arrays.stream(converters)
                    .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)
                    .findAny()
                    .orElse(null);

            assertNotNull("the JSON message converter must not be null",
                    messageConverter);
        }

        @SneakyThrows
        protected String toJson(Object data) {
            val mockHttpOutputMessage = new MockHttpOutputMessage();
            messageConverter.write(
                    data, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
            return mockHttpOutputMessage.getBodyAsString();
        }
    }

You could use it in your test class like this

    @WebMvcTest(UserController.class)
    @AutoConfigureMockMvc(addFilters = false)
    public class UserControllerTest extends RestControllerTest {

        @Autowired
        MockMvc mockMvc;

        @Test
        public void whenRequestValid_thenReturnStatusOk() throws Exception{
            User user = new User("John", "QPublic", "john.public@gmail.com",
                    "123456789", LocalDateTime.now(), LocalDateTime.now());      
            mockMvc.perform(MockMvcRequestBuilders.post("/api/user/register")
                    .content(toJson(user))
                    .contentType(MediaType.APPLICATION_JSON)
                    )
                    .andExpect(MockMvcResultMatchers.status().isOk());
        }
    }

I hope it works for you

eHayik
  • 2,981
  • 1
  • 21
  • 33
0

As chrylis mentioned in comments, the problem occurred due to Java 8 Date & Time API and Jackson serialization conflicts. By default, ObjectMapper doesn't understand the LocalDateTime data type, so I need to add com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency to my maven which is a datatype module to make Jackson recognize Java 8 Date & Time API data types. A stackoverflow question and a blog post helped me to figure out what actually my problem was.

Shahin
  • 253
  • 1
  • 3
  • 13
0

If LocalDateTime is not a problem, I think we should implementequals in User class.

WonChul Heo
  • 242
  • 1
  • 12