Search code examples
javaspring-mvcspring-mvc-test

Spring MVC custom Formatter works in Test but fails in Browser


I have a controller: (as per Spring WebMVC @ModelAttribute parameter-style)

@GetMapping("/time/{date}")
@ResponseStatus(OK)
public LocalDate getDate(
        @ModelAttribute("date") LocalDate date
) {
    return date;
}

LocalDateFormatter encodes LocalDates from strings "now", "today" and typical "yyyy-MM-dd"-formatted strings, and decodes dates back to strings

public class LocalDateFormatter implements Formatter<LocalDate> {}

I have tested this controller via Spring Test. The test PASSES.

I set up a conversion service and mocked an MVC with it:

var conversion = new DefaultFormattingConversionService();
conversion.addFormatterForFieldType(LocalDate.class, new LocalDateFormatter());

mockMvc = MockMvcBuilders
        .standaloneSetup(TimeController.class)
        .setConversionService(conversionRegistry)
        .build();

Test is parameterized and looks like this:

@ParameterizedTest
@MethodSource("args")
void getDate(String rawDate, boolean shouldConvert) throws Exception {
    var getTime = mockMvc.perform(get("/time/" + rawDate));

    if (shouldConvert) {
        // Date is successfully parsed and some JSON is returned
        getTime.andExpect(content().contentType(APPLICATION_JSON_UTF8));
    } else {
        // Unsupported rawDate
        getTime.andExpect(status().is(400));
    }
}

Here are the parameters:

private static Stream<Arguments> args() {
    // true if string should be parsed
    return Stream.of(
            Arguments.of("now", true),
            Arguments.of("today", true),
            Arguments.of("thisOneShouldNotWork", false),
            Arguments.of("2014-11-27", true)
    );
}

As I've said, test passes.

But when launched from browser, a 400 error is received on ANY request.

How I've tried integrating conversion into Spring MVC (none of this worked):

  • Overriding WebMvcConfigurer's method:

    public class ServletConfig implements WebMvcConfigurer {
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addFormatter(new LocalDateFormatter());
            // ALSO TRIED
            registry.addFormatterForFieldType(LocalDate.class, new LocalDateFormatter());
        }
    }
    
  • Registering a FormattingConversionService

    @Bean
    public FormattingConversionService conversionService() {
        var service = new FormattingConversionService();
        service.addFormatter(new LocalDateFormatter());
        return service;
    }
    

Can someone tell me what's wrong?

P.S. I'm aware that this is not the best way to work with dates, but since it says in Spring reference that this should work, I wanted to try it out.


Solution

  • Define this bean for spring boot:

    @Bean
        public Formatter<LocalDate> localDateFormatter() {
            return new Formatter<LocalDate>() {
                @Override
                public LocalDate parse(String text, Locale locale) throws ParseException {
                    if ("now".equals(text))
                        return LocalDate.now();
                    return LocalDate.parse(text, DateTimeFormatter.ISO_DATE);
                }
    
                @Override
                public String print(LocalDate object, Locale locale) {
                    return DateTimeFormatter.ISO_DATE.format(object);
                }
            };
        }
    

    if you use Spring MVC define like this:

    @Configuration
    @ComponentScan
    @EnableWebMvc
    public class ServletConfig implements WebMvcConfigurer {
    
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addFormatter(new Formatter<LocalDate>() {
                @Override
                public LocalDate parse(String text, Locale locale) throws ParseException {
                    if ("now".equals(text))
                        return LocalDate.now();
                    return LocalDate.parse(text, DateTimeFormatter.ISO_DATE);
                }
    
                @Override
                public String print(LocalDate object, Locale locale) {
                    return DateTimeFormatter.ISO_DATE.format(object);
                }
            });
        }
    }
    

    Don't forget implement today function as parameter.