Incorrect serialization of ZonedDateTime in JUnit tests. Date returns as Double

Viewed 336

I have controller that returns the next DTO as JSON response

    public class CustomerInfo {
    private String name;
    private Integer customerId;
    private ZonedDateTime startDate;
    private ZonedDateTime paymentDueDate;
    private String updatedBy;
    private ZonedDateTime updatedDate;
}

Example of controller code:

@GetMapping("/{guid}/details")
@ApiOperation(value = "Get Customer info")
public CustomerInfo getBillRunInfo(@PathVariable String guid) {
    return customerProcessorService.getCustomerInfo(guid);
}

So, I started with unit tests for controllers, please, take a look on code bellow:

@TestPropertySource("classpath:application-test.properties")
@SpringBootTest
public class BoardProcessorControllerTests {

    private static final String GUID = "02b268c1-071f-11ec-b9de-8660bd1c678a";
    private static final String BILLRUN_NAME = "Telco Bill Run of Day 30/08/2021";
    private static final Integer GENERATION_ID = 1069892;
    private static final ZonedDateTime START_DATE = ZonedDateTime.parse("2013-01-01T01:01:00+05:00");
    private static final ZonedDateTime PAYMENT_DUE_DATE = ZonedDateTime.parse("2021-03-23T10:43:32.010069453+02:00");;
    private static final String UPDATED_BY = "gl_anechkin";
    private static final ZonedDateTime UPDATED_DATE = ZonedDateTime.parse("2021-03-23T10:43:32.010069453+02:00");
    private static final String EXTERNAL_SYSTEM_REFERENCE = "Jira-1";

    private BoardProcessorController boardProcessorController;
    private StepProcessorService stepProcessorService;
    private BillRunProcessorService billRunProcessorService;
    private MockMvc mockMvc;

    @BeforeEach
    public void before() {

        billRunProcessorService = mock(BillRunProcessorService.class);
        stepProcessorService = mock(StepProcessorService.class);
        boardProcessorController = new BoardProcessorController(billRunProcessorService, stepProcessorService);
        mockMvc = MockMvcBuilders.standaloneSetup(boardProcessorController)
            .build();
    }

    @Test
    void getBillRunInfoSuccessful() throws Exception {
        when(billRunProcessorService.getBillRunInfo(GUID))
            .thenReturn(createBillRunInfoResponse());

        mockMvc.perform(get(createBillRunInfoUrl()))
            .andExpect(status().isOk())
            .andExpect(jsonIsEquals("get_billrun_response_01.json"));

    }

    @Test
    void getBillRunInfoNoFound() throws Exception {
        when(billRunProcessorService.getBillRunInfo(GUID))
            .thenThrow(new ServiceException(ServiceCode.BILLRUN_NOT_FOUND_BY_GUID,"Failed to get Bill Run ID by external reference: " + GUID));

        mockMvc.perform(get(createBillRunInfoUrl()))
            .andExpect(status().isNotFound());

    }

    private BillRunInfo createBillRunInfoResponse() {
        BillRunInfo billRunInfo = new BillRunInfo();
        billRunInfo.setName(BILLRUN_NAME);
        billRunInfo.setGenerationId(GENERATION_ID);
        billRunInfo.setStartDate((START_DATE));
        billRunInfo.setPaymentDueDate(PAYMENT_DUE_DATE);
        billRunInfo.setUpdatedBy(UPDATED_BY);
        billRunInfo.setUpdatedDate(UPDATED_DATE);
        billRunInfo.setExternalSystemReference(EXTERNAL_SYSTEM_REFERENCE);

        return billRunInfo;
    }

    private String createBillRunInfoUrl() {
        return "/v1/bill-runs/" + GUID + "/details";
    }

    private ResultMatcher jsonIsEquals(String fileName) {
        return mvcResult -> {
            String actualJson = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
            actualJson = normalize(actualJson);

            String expectedJson = getPayloadFromResources(fileName);
            expectedJson = normalize(expectedJson);

            Assertions.assertEquals(expectedJson, actualJson);
        };
    }

    private String normalize(String expectedJson) throws IOException {
        ObjectMapper om = new ObjectMapper();
        om.registerModule(new JavaTimeModule());
        Map<String, Object> map = om.readValue(expectedJson, Map.class);
        return om.writerWithDefaultPrettyPrinter().writeValueAsString(map);
    }

    private  String getPayloadFromResources(String fileName) throws IOException {

        InputStream resourceStream = BoardProcessorController.class.getClassLoader()
            .getResourceAsStream("response/" + fileName);

        return IOUtils.toString(resourceStream, StandardCharsets.UTF_8);
    }

}

application-test.properties

spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
spring.jackson.serialization.indent_output =true
jackson.default-property-inclusion=always

spring.datasource.url=jdbc:postgresql://wiremock:5432/fake_postgres_db

As result mockMvc.perform(get()) returns me ZonedDateTime as Double(I suppose so). Expected response:

    {
  "name": "Customer Name 1",
  "customerId": 1069892,
  "startDate": "2013-01-01T01:01:00+05:00",
  "paymentDueDate": "2021-03-23T10:43:32.010069453+02:00",
  "updatedBy": "gl_anechkin",
  "updatedDate": "2021-03-23T10:43:32.010069453+02:00"
}

Actual response:

    {
  "name": "Customer Name 1",
  "customerId": 1069892,
  "startDate": "1.356984061E9",
  "paymentDueDate": "1.616478212E9",
  "updatedBy": "gl_anechkin",
  "updatedDate": "1.616478212E9"
}

I tried to debug tests and found that mvcResult.getResponse() already returns me date in incorrect format. Any suggestions?

1 Answers

Spring Web MVC has a bunch of HttpMessageConverter pre-configured, by default.

Those converters let the application convert the most common data, automatically.

If you want to know what converters are configured by default, just add this simple class to your application, and run it.

@Configuration
public class AppConfig {
    private final RequestMappingHandlerAdapter handlerAdapter;

    public AppConfig(RequestMappingHandlerAdapter handlerAdapter) {
        this.handlerAdapter = handlerAdapter;
    }

    @EventListener
    public void handleContextRefresh(ContextRefreshedEvent event) {
        System.out.println("-------");
        handlerAdapter.getMessageConverters().forEach(System.out::println);
        System.out.println("-------");
    }
}

The output of this piece of code should look like:

-------
org.springframework.http.converter.ByteArrayHttpMessageConverter@64040287
org.springframework.http.converter.StringHttpMessageConverter@82c57b3
org.springframework.http.converter.StringHttpMessageConverter@110844f6
org.springframework.http.converter.ResourceHttpMessageConverter@6f89f665
org.springframework.http.converter.ResourceRegionHttpMessageConverter@df1cff6
org.springframework.http.converter.xml.SourceHttpMessageConverter@4925f4f5
org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter@1ad926d3
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@5be82d43
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@3a43d133
org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter@39ce27f2
-------

The most interesting in the list is MappingJackson2HttpMessageConverter. Diving into its initialization, you will find this constructor

public MappingJackson2HttpMessageConverter() {
    this(Jackson2ObjectMapperBuilder.json().build());
}

that injects an ObjectMapper pre-configured. In particular, it will have two modules:

modules

JavaTimeModule is the module you need to convert the ZonedDateTime properly.

In addition, somewhere during its initialization, the ObjectMapper will be configured with the SerializationFeature.WRITE_DATES_AS_TIMESTAMPS set to false.

So to solve your problem, you just have to set a properly configured MappingJackson2HttpMessageConverter in your MockMvc instance.

The before() method should look like:

@BeforeEach
public void before() {

    ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
    objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

    this.boardProcessorController = new MyController();
    this.mockMvc = MockMvcBuilders.standaloneSetup(boardProcessorController)
            .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
            .build();
    }

And then the ZonedDateTime will be correctly serialized.

Related