⬆️ ⬇️

Unit Testing Json Serialization in Spring Boot





Introduction



One of the main tasks of each web service is to return the model to the client side, and in this case, Spring Boot provides a convenient level of abstraction, allowing the developer to remain at the level of working with models, and leave the model’s serialization process outside the source code of the program. But what if serialization itself becomes part of the application's business logic and, therefore, requires coverage with test scenarios?



This article will discuss one of the scenarios when we may need to take into account the peculiarities of the business logic of the application during serialization (cash rounding scenario), which we will use as an example the serialization mechanism in Spring Boot, as well as a possible testing method.



Formulation of the problem



Let our web service be responsible for providing customer expense information, and we need to provide data with configurable accuracy. The logical solution would be to make all the transformations of the model to the periphery of the service, while maintaining the visibility of the application of rounding logic.

')

Consider a possible solution.



Imagine the controller of our application, which will return the desired model.



@RestController public class AccountController { //        , //       . @Autowired private AccountService accountService; @RequestMapping(value = "/account/{clientId}", method = RequestMethod.GET, produces = "application/json") public Account getAccount(@PathVariable long clientId) throws Exception { Account result = accountService.getAccount(clientId); //  ,    - //      ,    json, //    . return result; } } 


Now look at our model.



 public class Account { private Long clientId; //    Spring Boot   FasterXML/jackson, //    API  ,   . // ,       //     MoneySerializer @JsonSerialize(using = MoneySerializer.class) private BigDecimal value; //    getter'  setter'    } 


You may have already had to deal with other annotations for customization. A feature of this annotation is the ability to define your service, responsible for the serialization of the annotated field of the model.



Before we look at what a serializer is, let's complicate the task: let the rounding parameters be configurable via some internal service that abstracts any manifestation of dynamic parameter resolution.



This complication is our key point that we want to consider. As we could see from the implementation of the model, the API of our framework takes the serialization class in the argument, which means the serializer life cycle passes under the control of the serializer framework. This raises the question of what to do if we want to embed dependency from the context of our application into the serializer? To do this, consider the implementation of the above serializer.



 //     , //  Jackson   Spring, // API    //    Spring DI @JsonComponent public class MoneySerializer extends JsonSerializer<BigDecimal> { //  ,   , //    Spring Boot    Bean'. private RoundingHolder roundingHolder; @Autowired public MoneySerializer(RoundingHolder roundingHolder) { this.roundingHolder = roundingHolder; } //       , // ,   ,      - //      . @Override public void serialize(BigDecimal value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeNumber(value.setScale(roundingHolder.getPrecision(), roundingHolder.getRoundingMode())); } } 


Our service is ready, but as responsible developers, we want to make sure that the kitchen we assembled works.



Let's go to testing



Let's see what the framework API offers for testing.



 //     ,   //    Spring,       . //     JsonTest,      //      , //    JSON-. @JsonTest @ContextConfiguration(classes = {AccountSerializationTest.Config.class}) @RunWith(SpringRunner.class) public class AccountSerializationTest { //  ,      //     ObjectMapper,    . //       . //    , //      . @Autowired private ObjectMapper objectMapper; @Test public void testAccountMoneyRounding() throws Exception { Account account = new Account(); account.setClientId(1L); account.setValue(BigDecimal.valueOf(1.123456789)); String expectedResult = "{\"clientId\":1,\"value\":\1.123\}"; // ,          JSON, //     -. assertEquals(expectedResult, objectMapper.writeValueAsString(account)); } //   MoneySerializer   API  //    ,       //    Jackson.   ,   , //   Spring , ,  //      . @TestConfiguration public static class Config { @Bean public static RoundingHolder roundingHolder() { RoundingHolder roundingHolder = Mockito.mock(RoundingHolder.class); //   ,         Mockito.when(roundingHolder.getMathContext()).thenReturn(new MathContext(3, RoundingMode.HALF_EVEN)); return roundingHolder; } } } 


Let us dwell on this point in more detail. To serialize and deserialize models in Jackson, the ObjectMapper class is used . This is exactly the context object that is responsible for transforming the models, therefore, to make sure how the model is presented, you need to check how it is handled by the ObjectMapper from the context.



If you want to create your own custom ObjectMapper, you can find the following typical example: ObjectMapper mapper = new ObjectMapper . However, let's look at how Spring Boot creates an instance of this class by default. To do this, refer to the source code auto-configuration JacksonAutoConfiguration , responsible for creating the object:



 @Bean public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { return builder.createXmlMapper(false).build(); } 


And if we go further and look into build () , we find that for serialization work, which we could get used to when working with the default mapper (such as injecting services into a custom serializer), it’s not enough just to create a Bean mapper, you should contact the provided builder . By the way, in the documentation itself for Spring Boot this is indicated explicitly.



In retreat I would like to add a reference to JacksonTester . As a shell representative for BDD serialization testing in the context of Mockito.



Let's sum up





Conclusion



Thanks for attention! This example will be relevant for the current version of Spring Boot 2.1.x, and for earlier versions up to 1.4.x. Also, the technique is suitable for situations with de-serialization of the model. Look under the hood of your main frameworks for a better understanding of the mechanism of the application and responsibly approach to testing.

Source: https://habr.com/ru/post/452188/



All Articles