Индивидуальный ObjectMapper, не используемый в тесте

Я использую Spring Framework, версия 4.1.6, с веб-службами Spring и без Spring Boot. Чтобы узнать структуру, я пишу REST API и тестирую, чтобы убедиться, что ответ JSON, полученный от попадания в конечную точку, верен. В частности, я пытаюсь настроить ObjectMapper PropertyNamingStrategy, чтобы использовать стратегию именования "нижний регистр с подчеркиванием".

Я использую метод, подробно описанный в блоге Spring, чтобы создать новый ObjectMapper и добавить его в список преобразователей. Это выглядит следующим образом:

package com.myproject.config;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import org.springframework.context.annotation.*;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        Jackson2ObjectMapperBuilder builder = jacksonBuilder();
        converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
    }

    public Jackson2ObjectMapperBuilder jacksonBuilder() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.propertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);

        return builder;
    }
}

Затем я запускаю следующий тест (используя JUnit, MockMvc и Mockito) для проверки моих изменений:

package com.myproject.controller;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.AnnotationConfigWebContextLoader;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// Along with other application imports...

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = {WebConfig.class}, loader = AnnotationConfigWebContextLoader.class)
public class MyControllerTest {

    @Mock
    private MyManager myManager;

    @InjectMocks
    private MyController myController;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        this.mockMvc = MockMvcBuilders.standaloneSetup(this.myController).build();
    }


    @Test
    public void testMyControllerWithNameParam() throws Exception {
        MyEntity expected = new MyEntity();
        String name = "expected";
        String title = "expected title";

        // Set up MyEntity with data.
        expected.setId(1); // Random ID.
        expected.setEntityName(name);
        expected.setEntityTitle(title)

        // When the MyManager instance is asked for the MyEntity with name parameter,
        // return expected.
        when(this.myManager.read(name)).thenReturn(expected);

        // Assert the proper results.
        MvcResult result = mockMvc.perform(
                get("/v1/endpoint")
                    .param("name", name))
                .andExpect(status().isOk())
                .andExpect((content().contentType("application/json;charset=UTF-8")))
                .andExpect(jsonPath("$.entity_name", is(name))))
                .andExpect(jsonPath("$.entity_title", is(title)))
                .andReturn();

        System.out.println(result.getResponse().getContentAsString());
    }
}

Однако это возвращает ответ:

{"id": 1, "entityName": "expected", "entityTitle": "expected title"}

Когда я получу:

{"id": 1, "entity_name": "expected", "entity_title": "expected title"}

У меня есть встроенный WebApplicationInitializer, который сканирует пакет:

package com.myproject.config;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class WebAppInitializer implements WebApplicationInitializer {

    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.scan("com.myproject.config");
        ctx.setServletContext(servletContext);

        ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx));
        servlet.setLoadOnStartup(1);
        servlet.addMapping("/");

        servletContext.addListener(new ContextLoaderListener(ctx));
    }
}

Используя мой отладчик в IntelliJ, я вижу, что строитель создан и добавлен, но где-то по линии результирующий ObjectMapper фактически не используется. Я должен что-то упустить, но все примеры, которые мне удалось найти, похоже, не говорят о том, что это такое! Я попытался исключить @EnableWebMvc и реализовать WebMvcConfigurationSupport, используя MappingJackson2HttpMessageConverter как Bean и установить ObjectMapper как Bean безрезультатно.

Любая помощь будет принята с благодарностью! Пожалуйста, дайте мне знать, если требуются другие файлы.

Спасибо!

РЕДАКТИРОВАТЬ: Проделал еще несколько операций по поиску и нашел этот. В ссылке автор добавляет setMessageConverters(), прежде чем он построит MockMvc, и он работает для автора. Выполнение того же сработало и для меня; однако я не уверен, что все будет работать на производстве, поскольку хранилища еще не очищены. Когда я узнаю, я отправлю ответ.

ИЗМЕНИТЬ 2: См. ответ.

Ответ 1

Я понял, почему это работает так, как это было. Чтобы повторить, процесс моего настроенного ObjectMapper для работы в моем тесте (при условии, что MockMvc создается как автономный) выглядит следующим образом:

  • Создайте класс WebConfig, который расширяет WebMvcConfigurerAdapter.
  • В классе WebConfig создайте новый @Bean, который возвращает MappingJackson2HttpMessageConverter. Этот MappingJackson2HttpMessageConverter имеет к нему необходимые изменения (в моем случае он передавал ему Jackson2ObjectMapperBuilder с PropertyNamingStrategy, установленным в CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES.)
  • Также в классе WebConfig @Override configureMessageConverters() и добавьте MappingJackson2HttpMessageConverter из (2) в список конвертеров сообщений.
  • В тестовом файле добавьте аннотацию @ContextConfiguration(classes = { WebConfig.class }), чтобы сообщить тест вашего @Bean.
  • Используйте @Autowired для ввода и доступа к @Bean, указанному в (2).
  • В настройке MockMvc используйте метод .setMessageConverters() и передайте ему введенный MappingJackson2HttpMessageConverter. Тест теперь будет использовать конфигурацию, установленную в (2).

Тестовый файл:

package com.myproject.controller;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.AnnotationConfigWebContextLoader;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// Along with other application imports...

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = {WebConfig.class})
public class MyControllerTest {

    /**
     * Note that the converter needs to be autowired into the test in order for
     * MockMvc to recognize it in the setup() method.
     */
    @Autowired
    private MappingJackson2HttpMessageConverter jackson2HttpMessageConverter;

    @Mock
    private MyManager myManager;

    @InjectMocks
    private MyController myController;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        this.mockMvc = MockMvcBuilders
            .standaloneSetup(this.myController)
            .setMessageConverters(this.jackson2HttpMessageConverter) // Important!
            .build();
    }


    @Test
    public void testMyControllerWithNameParam() throws Exception {
        MyEntity expected = new MyEntity();
        String name = "expected";
        String title = "expected title";

        // Set up MyEntity with data.
        expected.setId(1); // Random ID.
        expected.setEntityName(name);
        expected.setEntityTitle(title)

        // When the MyManager instance is asked for the MyEntity with name parameter,
        // return expected.
        when(this.myManager.read(name)).thenReturn(expected);

        // Assert the proper results.
        MvcResult result = mockMvc.perform(
                get("/v1/endpoint")
                    .param("name", name))
                .andExpect(status().isOk())
                .andExpect((content().contentType("application/json;charset=UTF-8")))
                .andExpect(jsonPath("$.entity_name", is(name))))
                .andExpect(jsonPath("$.entity_title", is(title)))
                .andReturn();

        System.out.println(result.getResponse().getContentAsString());
    }
}

И файл конфигурации:

package com.myproject.config;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import org.springframework.context.annotation.*;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(jackson2HttpMessageConverter());
    }

    @Bean
    public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        Jackson2ObjectMapperBuilder builder = this.jacksonBuilder();
        converter.setObjectMapper(builder.build());

        return converter;
    }

    public Jackson2ObjectMapperBuilder jacksonBuilder() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); 
        builder.propertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);

        return builder;
    }
}

Развертывание моего сгенерированного WAR файла Tomcat 7 в XAMPP показывает, что стратегия именования используется правильно. Причина, по которой я считаю, что это работает так, как это происходит, заключается в том, что при автономной настройке всегда используется набор преобразователей сообщений по умолчанию, если не указано иное. Это можно увидеть в комментарии для функции setMessageConverters() в StandAloneMockMvcBuilder.java(версия 4.1.6, \org\springframework\test\web\servlet\setup\StandaloneMockMvcBuilder.java):

   /**
     * Set the message converters to use in argument resolvers and in return value
     * handlers, which support reading and/or writing to the body of the request
     * and response. If no message converters are added to the list, a default
     * list of converters is added instead.
     */
    public StandaloneMockMvcBuilder setMessageConverters(HttpMessageConverter<?>...messageConverters) {
        this.messageConverters = Arrays.asList(messageConverters);
        return this;
    }

Поэтому, если MockMvc явно не сообщается об одном изменении конвертеров сообщений во время построения MockMvc, он не будет использовать изменения.

Ответ 2

или вы можете

MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new
            MappingJackson2HttpMessageConverter();
    mappingJackson2HttpMessageConverter.setObjectMapper( new ObjectMapper().setPropertyNamingStrategy(
            PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES) );
    mockMvc = MockMvcBuilders.standaloneSetup(attributionController).setMessageConverters(
            mappingJackson2HttpMessageConverter ).build();

Ответ 3

С помощью Spring Boot 1.5.1 я могу сделать:

@RunWith(SpringRunner.class)
@AutoConfigureJsonTesters
@JsonTest
public class JsonTest {

    @Autowired
    ObjectMapper objectMapper;
}

для доступа к ObjectMapper настроен так же, как и во время выполнения.

My runtime jackson настроен следующим образом:

@Configuration
public class JacksonConfiguration {

    @Autowired
    Environment environment;

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
        return builder -> {
            builder.locale(new Locale("sv", "SE"));

            if (JacksonConfiguration.this.environment == null
                    || !JacksonConfiguration.this.environment.acceptsProfiles("docker")) {
                builder.indentOutput(true);
            }

            final Jdk8Module jdk8Module = new Jdk8Module();

            final ProblemModule problemModule = new ProblemModule();

            final JavaTimeModule javaTimeModule = new JavaTimeModule();

            final Module[] modules = new Module[] { jdk8Module, problemModule,
                javaTimeModule };
            builder.modulesToInstall(modules);
        };
    }
}