103

I have been writing some simple unit testing routines for a simple spring web application. When I add @JsonIgnore annotation on a getter method of a resource, the resulting json object does not include the corresponding json element. So when my unit test routine tries to test if this is null (which is the expected behavior for my case, I don't want the password to be available in json object), test routine runs into an exception:

java.lang.AssertionError: No value for JSON path: $.password, exception: No results for path: $['password']

This is the unit test method I wrote, testing the 'password' field with is(nullValue()) method:

@Test
public void getUserThatExists() throws Exception {
    User user = new User();
    user.setId(1L);
    user.setUsername("zobayer");
    user.setPassword("123456");

    when(userService.getUserById(1L)).thenReturn(user);

    mockMvc.perform(get("/users/1"))
            .andExpect(jsonPath("$.username", is(user.getUsername())))
            .andExpect(jsonPath("$.password", is(nullValue())))
            .andExpect(jsonPath("$.links[*].href", hasItem(endsWith("/users/1"))))
            .andExpect(status().isOk())
            .andDo(print());
}

I have also tried it with jsonPath().exists() which gets a similar exception stating that the path doesn't exist. I am sharing some more code snippets so that the whole situation becomes more readable.

The controller method I am testing looks something like this:

@RequestMapping(value="/users/{userId}", method= RequestMethod.GET)
public ResponseEntity<UserResource> getUser(@PathVariable Long userId) {
    logger.info("Request arrived for getUser() with params {}", userId);
    User user = userService.getUserById(userId);
    if(user != null) {
        UserResource userResource = new UserResourceAsm().toResource(user);
        return new ResponseEntity<>(userResource, HttpStatus.OK);
    } else {
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
}

I am using spring hateos resource assembler for converting entity to resource objects and this is my resource class:

public class UserResource extends ResourceSupport {
    private Long userId;
    private String username;
    private String password;
    
    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @JsonIgnore
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

I understand why this is getting an exception, also in a way, the test is successful that it could not find the password field. But what I want to do is, run this test to ensure that the field is not present, or if present, it contains null value. How can I achieve this?

There is a similar post in stack overflow: Hamcrest with MockMvc: check that key exists but value may be null

In my case, the field may be non existent as well.

For the record, these are the versions of test packages I am using:

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <version>2.0.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path-assert</artifactId>
        <version>2.0.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-all</artifactId>
        <version>1.10.19</version>
        <scope>test</scope>
    </dependency>

[EDIT] To be more precise, say, you have to write a test for an entity where you know some of the fields need to be null or empty or should not even exists, and you don't actually go through the code to see if there is a JsonIgnore added on top of the property. And you want your tests to pass, how can I do this.

Please feel free to tell me that this is not practical at all, but still would be nice to know.

[EDIT] The above test succeeds with the following older json-path dependencies:

    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <version>0.9.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path-assert</artifactId>
        <version>0.9.1</version>
        <scope>test</scope>
    </dependency>

[EDIT] Found a quickfix that works with latest version of jayway.jasonpath after reading the documentation of spring's json path matcher.

.andExpect(jsonPath("$.password").doesNotExist())
Archmede
  • 1,592
  • 2
  • 20
  • 37
Zobayer Hasan
  • 2,187
  • 6
  • 22
  • 38

5 Answers5

207

I had the same problem with the newer version. It looks to me that the doesNotExist() function will verify that the key is not in the result:

.andExpect(jsonPath("$.password").doesNotExist())
benezra444
  • 2,086
  • 1
  • 10
  • 3
  • 1
    that's what I did, look at the last line of my question. [question last edited on Sep 4 '15 at 14:55] – Zobayer Hasan Feb 01 '16 at 13:00
  • 1
    if you use AssertJ (e.g. in a Spring Boot app) this is the way of checking it `assertThat(this.json.write(entity)).doesNotHaveJsonPathValue("@.keyl");` – Adam Bogdan Boczek Feb 06 '17 at 08:24
  • Additionally, to check if no properties exist in the json (in other words, the json is an empty object: `{}`), you can use `.andExpect(jsonPath("$.*").doesNotExist())` – ChrisM May 28 '20 at 19:16
12

There is a difference between the property that is present, but having null value, and the property not being present at all.

If the test should fail only when there is a non-null value, use:

.andExpect(jsonPath("password").doesNotExist())

If the test should fail as soon as the property is present, even with a null value, use:

.andExpect(jsonPath("password").doesNotHaveJsonPath())
Wim Deblauwe
  • 25,113
  • 20
  • 133
  • 211
4

@JsonIgnore is behaving as expected, not producing the password in the json output, so how could you expect to test something that you are explicitly excluding from the output?

The line:

.andExpect(jsonPath("$.property", is("some value")));

or even a test that the property is null:

.andExpect(jsonPath("$.property").value(IsNull.nullValue()));

correspond to a json like:

{
...
"property": "some value",
...
}

where the important part is the left side, that is the existence of "property":

Instead, @JsonIgnore is not producing the porperty in the output at all, so you can't expect it not in the test nor in the production output. If you don't want the property in the output, it's fine, but you can't expect it in test. If you want it empty in output (both in prod and test) you want to create a static Mapper method in the middle that is not passing the value of the property to the json object:

Mapper.mapPersonToRest(User user) {//exclude the password}

and then your method would be:

@RequestMapping(value="/users/{userId}", method= RequestMethod.GET)
public ResponseEntity<UserResource> getUser(@PathVariable Long userId) {
    logger.info("Request arrived for getUser() with params {}", userId);
    User user = Mapper.mapPersonToRest(userService.getUserById(userId));
    if(user != null) {
        UserResource userResource = new UserResourceAsm().toResource(user);
        return new ResponseEntity<>(userResource, HttpStatus.OK);
    } else {
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
}

At this point, if your expectations are for Mapper.mapPersonToRest to return a user with a null password, you can write a normal Unit test on this method.

P.S. Of course the password is crypted on the DB, right? ;)

Paolof76
  • 889
  • 1
  • 9
  • 23
  • Yes JsonIgnore is performing as expected. So what would be the best practice for testing entities with JsonIgnore over some fields. Assume this, say you want to write a test, you know which fields should be empty or null (or should not even exists) but you are not going to open the source code and read if there is actually a JsonIgnore annotation over them. and obviously you want your test to pass, not fail with an exception. Oh and ofcourse the password is hashed. Not that it matters, just a test project. – Zobayer Hasan Sep 04 '15 at 13:00
  • What you want to do is take care that the object UserResource is not returning a password, right? Let's say you have the new UserResourceAsm().toResource(user) returning the user, right? In this case, you should test not at SpringMVC level, but just perform a normal Unit Test that is checking the user.getPassword() is null. Hope this clarify! – Paolof76 Sep 04 '15 at 13:14
  • I added an example to help you understand what I mean. Let me see if you have more doubts – Paolof76 Sep 04 '15 at 13:25
  • yes it does! The reason I asked this is because I saw something similar on a tutorial I followed. But the tutorial author used an older version of hamcrest / mockito. Could this be a reason why he was managed to get test success? – Zobayer Hasan Sep 04 '15 at 13:27
  • I have tested, and I think I am correct about the version. With json-path and json-path-assert version 0.9.1, my test passes. Test for null succeeds even if the field does not exists. However, with the newer versions, I think what you described in your answer regarding the use of Mapper is the preferred way to go. I'm just learning the ropes for unit testing. – Zobayer Hasan Sep 04 '15 at 13:33
  • I'm using org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath from spring 3.2.13 and it's working like you described in the first place – Paolof76 Sep 04 '15 at 13:48
  • I found out I accidentally down-voted this answer. I don't know how this happened. It's too late to simply undo it now. @Paolof76 could you edit the answer so I could fix that? Sorry. – Pawel Zieminski Oct 02 '20 at 23:48
0

doesNotHaveJsonPath for checking that it is not in json body

M.Sinan Şahin
  • 307
  • 3
  • 6
0

I wanted to reuse the same code I use for testing for the parameter being supplied, and for it missing, and this is what I came up with

  @Test
  void testEditionFoundInRequest() throws JsonProcessingException {
    testEditionWithValue("myEdition");
  }

  @Test
  void testEditionNotFoundInRequest() {
    try {
      testEditionWithValue(null);
      throw new RuntimeException("Shouldn't pass");
    } catch (AssertionError | JsonProcessingException e) {
      var msg = e.getMessage();
      assertTrue(msg.contains("No value at JSON path"));
    }
  }


  void testEditionWithValue(String edition) {   
   var HOST ="fakeHost";
   var restTemplate = new RestTemplate();
   var myRestClientUsingRestTemplate = new MyRestClientUsingRestTemplate(HOST, restTemplate);
   MockRestServiceServer mockServer;
   ObjectMapper objectMapper = new ObjectMapper();
   String id = "userId";
   var mockResponse = "{}";

   var request = new MyRequest.Builder(id).edition(null).build();
   mockServer = MockRestServiceServer.bindTo(restTemplate).bufferContent().build();

   mockServer
        .expect(method(POST))

        // THIS IS THE LINE I'd like to say "NOT" found
        .andExpect(jsonPath("$.edition").value(edition))
        .andRespond(withSuccess(mockResponse, APPLICATION_JSON));

    var response = myRestClientUsingRestTemplate.makeRestCall(request);
  } catch (AssertionError | JsonProcessingException e) {
    var msg = e.getMessage();
    assertTrue(msg.contains("No value at JSON path"));
  }
Brad Parks
  • 66,836
  • 64
  • 257
  • 336