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

77,931

Solution 1

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

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

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

Share:
77,931
Zobayer Hasan
Author by

Zobayer Hasan

I am Zobayer, a Software Engineer working at TigerIT Bangladesh Limited. I have graduated in Computer Science &amp; Engineering from University of Dhaka, Bangladesh. Formerly I have been an active participant in ACM ICPC and I am interested in solving programming problems on various online judges around the globe. As a pastime job, I am working as a mentor for competitive programming Contests training in a private university, and I often write algorithmic blogs (http://zobayer.blogspot.com/ and http://zobayer2009.wordpress.com/) regarding solutions to programming problems. Recently I have been having opportunities for setting problems in some local and national level programming contests. I am also interested in open source development, and I love to spend time experimenting with new technologies for software development.

Updated on October 24, 2020

Comments

  • Zobayer Hasan
    Zobayer Hasan over 3 years

    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())
    
  • Zobayer Hasan
    Zobayer Hasan over 8 years
    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.
  • Paolof76
    Paolof76 over 8 years
    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
    Paolof76 over 8 years
    I added an example to help you understand what I mean. Let me see if you have more doubts
  • Zobayer Hasan
    Zobayer Hasan over 8 years
    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
    Zobayer Hasan over 8 years
    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.
  • Paolof76
    Paolof76 over 8 years
    I'm using org.springframework.test.web.servlet.result.MockMvcResultMat‌​chers.jsonPath from spring 3.2.13 and it's working like you described in the first place
  • Zobayer Hasan
    Zobayer Hasan about 8 years
    that's what I did, look at the last line of my question. [question last edited on Sep 4 '15 at 14:55]
  • Adam Bogdan Boczek
    Adam Bogdan Boczek about 7 years
    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");
  • ChrisM
    ChrisM almost 4 years
    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())
  • Pawel Zieminski
    Pawel Zieminski over 3 years
    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.