Spring boot mockmvc works different and request JsonCreator

Viewed 51

I have simple code:

I use

id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'

Here is my DTOs:

    public class CreditDto {
       public final String title;
       public final String description;
       public final Long userId;
       public final DurationDto duration;
       public final Double sum;
       public final RateType rateType;
       public final PeriodType periodType;
       @DateTimeFormat(pattern = "yyyy-MM-dd")
       public final LocalDate startDate;

       public CreditDto(final String title,
                     final String description,
                     final Long userId,
                     final DurationDto duration,
                     Double sum,
                     final RateType rateType,
                     final PeriodType periodType,
                     final LocalDate startDate) {
           this.title = title;
           this.description = description;
           this.userId = userId;
           this.duration = duration;
           this.sum = sum;
           this.rateType = rateType;
           this.periodType = periodType;
           this.startDate = startDate;
    }
    
    public class DurationDto {
       public final int days;
       public final int months;
       public final int years;

       public DurationDto(int days,
                          int months,
                          int years) {
           this.days = days;
           this.months = months;
           this.years = years;
       }
    }

Here is my controller:

    @RestController
    @RequestMapping(path = "/api/v1/credit/",
        produces = MediaType.APPLICATION_JSON_VALUE,
        consumes = MediaType.APPLICATION_JSON_VALUE)
    public class CreditController {
       private final CreditService creditService;

       public CreditController(final CreditService creditService) {
           this.creditService = creditService;
       }

       @PostMapping
       public void addCredit(final @Valid @RequestBody CreditDto credit) {
           creditService.addCredit(credit);
       }
    }

I run my application, post json by using Postman and everything works perfectly.

Then I have a mockMvc test:

@ExtendWith(MockitoExtension.class)
public class CreditControllerTest {

    @InjectMocks
    private CreditController creditController;
    @Mock
    private CreditService creditService;

    private MockMvc mockMvc;

    @BeforeEach
    public void setup() {
        mockMvc = standaloneSetup(creditController)
                .build();
    }

    public void addCredit() throws Exception {
    //        Given
        String content = "{"title": "MonthlyFixedCredit", "description": "My awesome second daily floating credit", "userId": 125, "duration": {"days": 0, "months": 5, "years": 0}, "sum": 10000, "startDate": "2022-09-09", "rateType": "FIXED", "periodType": "MONTHLY"}";
        doNothing().when(creditService).addCredit(any(CreditDto.class));
    //        When
        final ResultActions result = mockMvc.perform(post("/api/v1/credit/")
                .contentType(MediaType.APPLICATION_JSON)
                .content(content));
    //        Then
        result.andExpect(status().isOk());

        verify(creditService, times(1)).addCredit(any(CreditDto.class));
        verifyNoMoreInteractions(creditService);
    }
}

And this test failed with:

Request processing failed; nested exception is 
org.springframework.http.converter.HttpMessageConversionException: Type definition 
error: [simple type, class org.superbank.credit.dto.CreditDto]; nested exception is 
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.superbank.credit.dto.CreditDto` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.superbank.credit.dto.CreditDto]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.superbank.credit.dto.CreditDto` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
at app//org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
at app//org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at app//javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
.........

Could someone help me to understand why actual application works fine without '@JsonCreator' or default constructor but I need to add it for mockMvc. I believed that mockMvc should work in the same way as actual spring mvc and do not need additional configuration just for unit test.

How can I make mockMvc behave the same way as actual mvc, what configuration I missed?

1 Answers

After hours of digging, the different in behaviour is due to:

JacksonAutoConfiguration is not loaded when using standaloneSetup.

auto-registration for all Module beans with all ObjectMapper beans (including the defaulted ones).

One of those module is ParameterNamesModule. This module add functionality from ParameterNamesAnnotationIntrospector to ObjectMapper,

Introspector that uses parameter name information provided by the Java Reflection API additions in Java 8 to determine the parameter name for methods and constructors.

that's why you can deserialize json to CreditDto(which does not contain default constructor) when running the application.

Solution

  1. Refer to gigi2's answer in Customized ObjectMapper not used in test, add ParameterNamesModule to ObjectMapper
    @BeforeEach
    public void setup() {
        MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new
                MappingJackson2HttpMessageConverter();
        mappingJackson2HttpMessageConverter.setObjectMapper(new ObjectMapper().registerModule(new ParameterNamesModule()));
        mockMvc = standaloneSetup(creditController).setMessageConverters(mappingJackson2HttpMessageConverter)
                .build();
    }

This is an easy fix for this case, but you may later face another error due to other Spring magic.

  1. Use @SpringBootTest and @AutoConfigureMockMvc.
    This make sure your test environment is close to your running application. But the downside is the test run slower.
Related