How to test if JSON path does not include a specific element, or if the element is present it is null?

JavaUnit TestingSpring MvcJsonpathMockmvc

Java Problem Overview


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 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>

Thanks in advance.

[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())

Java Solutions


Solution 1 - Java

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())

Solution 2 - Java

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())

Solution 3 - Java

@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? ;)

Solution 4 - Java

doesNotHaveJsonPath for checking that it is not in json body

Solution 5 - Java

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"));
  }

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionZobayer HasanView Question on Stackoverflow
Solution 1 - Javabenezra444View Answer on Stackoverflow
Solution 2 - JavaWim DeblauweView Answer on Stackoverflow
Solution 3 - JavaPaolof76View Answer on Stackoverflow
Solution 4 - JavaM.Sinan ŞahinView Answer on Stackoverflow
Solution 5 - JavaBrad ParksView Answer on Stackoverflow