Search code examples
javajacksonmockitospring-restcontrollermockmvc

Controller test failed while try to POST entity with @JsonProperty annotations to endpoint annotated with @Valid on @RequestBody


I have problem with testing rest controller endpoint. I'm trying to POST entity to endpoint that is annotated with @Valid on @RequestBody field. Entity fields are annotated with validation rules and jackson annotations.

I'm trying to test that POST endpoint with unit tests.

Entity and field with annotations

public class User {
    //other fields 

    @Column(name = "PASSWORD", nullable = false)
    @NotEmpty(message = "Users password must be filled.")
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String userPassword;
}

Controller

@RestController
@RequestMapping(value = "/api/users", produces = "application/hal+json")
public class UserController {

    @PostMapping("")
    public ResponseEntity<User> addNewUser(@Valid @RequestBody User newUser) {
        User createdUser = userService.saveUserInDatabase(newUser);
        return new ResponseEntity<>(createdUser, HttpStatus.OK);
    }
}

And unit test

@RunWith(MockitoJUnitRunner.class)
public class UserControllerTest {
    //other tests 
    @Test
    public void shouldBeAbleToAddNewUser() throws Exception {
        User newUser = new User();
        newUser.setUserName("userName");
        newUser.setUserEmail("[email protected]");
        newUser.setUserPassword("secret1");
        newUser.setEnable(true);

        User createdUser = new User(1L, "userName", "[email protected]", "secret1", true);

        when(userService.saveUserInDatabase(any(User.class))).thenReturn(createdUser);

        mockMvc.perform(post("/api/users")
                    .contentType(MediaTypes.HAL_JSON_UTF8)
                    .content(jsonUser.write(newUser).getJson()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.userName", Matchers.equalToIgnoringCase(createdUser.getUserName())))
                .andExpect(jsonPath("$.userId", Matchers.is(createdUser.getUserId().intValue())))
                .andExpect(jsonPath("$.userPassword").doesNotExist());
    }
}

What is important. When entity is fetched (GET) with swagger, field "userPassword" is not serialized. It works as expected thanks to @JsonProperty annotation. When entity is POSTed with swagger is also works correctly, password is saved in database and response doesn't contain "userPassword" field. Also, if any required (annotated with @NotEmpty) field is missing, exception is thrown.

Tests that are testing fetching data, works correctly, 'userPassword' is not visible response in json

andExpect(jsonPath("$.userPassword").doesNotExist())

But when attached test is executed, it fails with wrong status (400 instead 200) and information "Users password must be filled." Password is filled but it looks like it is not pushed to request body. I did some debug and notice that when @JsonProperty is added to "userPassword" field, that field is set to "null".

//edit
jsonUser used to post new User data in JSON format is defined this way

@RunWith(MockitoJUnitRunner.class)
public class UserControllerTest {

//other variables and methods

private JacksonTester<User> jsonUser;

    @Before
    public void setup() {
        JacksonTester.initFields(this, new ObjectMapper());
        mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
    }
}

//edit
I've found solution for my problem. jsonUser.write to work must first read data from newUser (so obviously) and include json annotation. As getter is blocked by jsonproperty it is omitted during read. That is why test case din't work but POST through swagger works correctly. To solve it, I've prepared String that contain data in json format

String userDetails = "{\"userName\":\"userName\",\"userEmail\":\"[email protected]\",\"userPassword\":\"secret1\",\"enable\":true}";

mockMvc.perform(post("/api/users")
                    .contentType(MediaTypes.HAL_JSON_UTF8)
                    .content(userDetails)) 
                    .andExpect(status().isOk());

Solution

  • You set the content of the post with

    jsonUser.write(newUser).getJson()
    

    jsonUser is a JacksonTester and is given a Jackson ObjectMapper to perform serialization / deserialization. As you are using the write method to serialize your user object it will respect the

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    

    The confusion maybe the use of 'write'. The method on the JacksonTester is write() which means serialize (write the object to json) where as the JsonProperty.Access.WRITE_ONLY means only use this field during deserialize (write from the json to the object)

    And the Json you get will not include any password details. i.e. you get something like:

    {"userName":"userName","userEmail":"[email protected]","enable":true}
    

    You use this as the content for your test POST request. When Jackson comes to deserialize your json from the content of the POST request it won't find a password. This is why you see what you see.

    I understand why you're using the WRITE_ONLY option on your class, it makes sense to prevent the password being written out when it is serialized. However, that means you can't use that class to create the json that you want to send up to the server by simply serializing it.

    One approach would be to subclass your User with a TestUser which just had a password field. Then use TestUser in your JacksonTester.

    public class TestUser extends User {
        private String userPassword;
    }
    

    Then in your test class you'd replace User with TestUser

    private JacksonTester<TestUser> jsonUser;
    

    and

        TestUser newUser = new TestUser();
        newUser.setUserName("userName");
        newUser.setUserEmail("[email protected]");
        newUser.setUserPassword("secret1");
        newUser.setEnable(true);