Testing Data Serialisation/Deserialization using JsonTest with Spring Boot

When working with data models in Java, we'll often have a Plain Old Java Object (POJO) that corresponds to the incoming request body, so we can interact with it more easily and have access to getters/setters.

Because this is a pretty integral part of interacting with other services / being interacted with, we need to make sure these models are mapped correctly.

Testing these can be done in a few ways, but often I see them not being tested as low in the test pyramid as we can do.

Fortunately, there are a few options for how we can test that serialisation (from object to string) and deserialisation (from string to object) works.

For instance, let's say we have the class:

/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;

public class TokenGrantDto {

  @JsonProperty("access_token")
  @JsonInclude(Include.NON_NULL)
  private String accessToken;

  @JsonProperty("refresh_token")
  @JsonInclude(Include.NON_NULL)
  private String refreshToken;

  private long expiresIn;

  @JsonProperty("token_type")
  @JsonInclude(Include.NON_NULL)
  private String tokenType;

  @JsonInclude(Include.NON_NULL)
  private String scope;

  @JsonInclude(Include.NON_NULL)
  private String me;

  public String getAccessToken() {
    return accessToken;
  }

  public void setAccessToken(String accessToken) {
    this.accessToken = accessToken;
  }

  public String getRefreshToken() {
    return refreshToken;
  }

  public void setRefreshToken(String refreshToken) {
    this.refreshToken = refreshToken;
  }

  @JsonInclude(Include.NON_NULL)
  @JsonProperty("expires_in")
  public Long getExpiresIn() {
    if (0 == expiresIn) {
      return null;
    }
    return expiresIn;
  }

  public void setExpiresIn(long expiresIn) {
    this.expiresIn = expiresIn;
  }

  public String getTokenType() {
    return tokenType;
  }

  public void setTokenType(String tokenType) {
    this.tokenType = tokenType;
  }

  public String getScope() {
    return scope;
  }

  public void setScope(String scope) {
    this.scope = scope;
  }

  public String getMe() {
    return me;
  }

  public void setMe(String me) {
    this.me = me;
  }
}

This then may have a unit or unit integration test like the following, which uses Jackson's ObjectMapper to verify serialisation/deserialisation:

/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class TokenGrantDtoTest {
  // note that this isn't ideal, as it wouldn't necessarily match the Spring
  // Boot configuration for ObjectMapper, so we'd want to actually make it `@Autowired`
  private static final ObjectMapper mapper = new ObjectMapper();

  private ObjectNode node;
  @Nested
  class Serialization {
    @Nested
    class HappyPath {
      @BeforeEach
      void setup() throws JsonProcessingException {
        TokenGrantDto dto = new TokenGrantDto();
        dto.setAccessToken("j.w.t");
        dto.setRefreshToken("j.w.t.2");
        dto.setExpiresIn(1234L);
        dto.setMe("https://me");
        dto.setScope("draft update");
        dto.setTokenType("Bearer");

        String actual = mapper.writeValueAsString(dto);
        node = mapper.readValue(actual, ObjectNode.class);
      }

      @Test
      void accessTokenIsMapped() {
        assertThat(node.get("access_token").textValue()).isEqualTo("j.w.t");
      }

      // ommitted for brevity

      @Test
      void expiresInIsMapped() {
        assertThat(node.get("expires_in").numberValue()).isEqualTo(1234L);
      }
    }

    // ommitted for brevity

    @Nested
    class WhenMissingProperties {
      @BeforeEach
      void setup() throws JsonProcessingException {
        TokenGrantDto dto = new TokenGrantDto();

        String actual = mapper.writeValueAsString(dto);
        node = mapper.readValue(actual, ObjectNode.class);
      }

      @ParameterizedTest
      @ValueSource(strings = {"access_token", "expires_in", "me", "scope", "token_type"})
      void propertiesAreNotMapped(String property) {
        assertThat(node.get(property)).isNull();
      }
    }
  }
}

Although this works great across Spring Boot and non-Spring Boot projects, there's an even better option in Spring Boot, because the awesome folks working on it have provided the @JsonTest type for verifying serialisation/deserialisation, which works across a few different types of JSON libraries.

In our case, we're using Jackson, so we'll replace this with the following test:

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;

@JsonTest
class TokenGrantDtoTest {

  @Autowired private JacksonTester<TokenGrantDto> jackson;

  private JsonContent<TokenGrantDto> asJson; // utility variable to

  @Nested
  class Serialization {

    @Nested
    class HappyPath {
      @BeforeEach
      void setup() throws IOException {
        TokenGrantDto dto = new TokenGrantDto();
        dto.setAccessToken("j.w.t");
        dto.setRefreshToken("j.w.t.2");
        dto.setExpiresIn(1234L);
        dto.setMe("https://me");
        dto.setScope("draft update");
        dto.setTokenType("Bearer");

        asJson = jackson.write(dto);
      }

      @Test
      void accessTokenIsMapped() {
        assertThat(asJson).hasJsonPathStringValue("access_token", "j.w.t");
      }

      @Test
      void expiresInIsMapped() {
        assertThat(asJson).extractingJsonPathNumberValue("expires_in", 1234L);
      }
    }

    @Nested
    class WhenMissingProperties {
      @BeforeEach
      void setup() throws IOException {
        TokenGrantDto dto = new TokenGrantDto();
        asJson = jackson.write(dto);
      }

      @ParameterizedTest
      @ValueSource(strings = {"access_token", "expires_in", "me", "scope", "token_type"})
      void propertiesAreNotMapped(String property) {
        assertThat(asJson).doesNotHaveJsonPath(property);
      }
    }
  }

  @Nested
  class Deserialization {
    @Test
    void whenEmptyDefaultsExpiresInReturnsNull() throws IOException {
      TokenGrantDto dto = jackson.parseObject("{}");

      assertThat(dto.getExpiresIn()).isNull();
    }
  }
}

Not only does it mean we don't have to play around with ObjectMappers, but we also get some really nice assertions available through AssertJ, which makes testing much nicer!

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#testing #spring-boot #blogumentation.

This post was filed under articles.

This post is part of the series writing-better-tests.

Related Posts

Other posts you may be interested in:

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.