自定义的 ObjectMapper 未在测试中使用

Customized ObjectMapper not used in test

我正在使用 Spring 框架,版本 4.1.6,带有 Spring 网络服务,没有 Spring 启动。为了学习该框架,我正在编写一个 REST API 并进行测试以确保从命中端点接收到的 JSON 响应是正确的。具体来说,我正在尝试调整 ObjectMapperPropertyNamingStrategy 以使用 "lower case with underscores" 命名策略。

我正在使用 the method detailed on Spring's blog 创建一个新的 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());
    }
}

但是,这个 returns 的响应是:

{"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 无济于事。

如有任何帮助,我们将不胜感激!如果需要任何其他文件,请告诉我。

谢谢!

编辑: 进行了更多挖掘,发现 this。在 link 中,作者在 he/she 构建 MockMvc 之前附加了 setMessageConverters() 并且它适用于作者。做同样的事情对我也有用;但是,我不确定是否所有内容都可以在生产中使用,因为存储库尚未清除。当我发现我会提交一个答案。

编辑 2: 查看答案。

我了解了为什么会这样工作。重申一下,让我的自定义 ObjectMapper 在我的测试中工作的过程(假设 MockMvc 被创建为独立的)如下:

  1. 创建一个扩展 WebMvcConfigurerAdapter.
  2. WebConfig class
  3. WebConfigclass中新建一个@Bean,即returns一个MappingJackson2HttpMessageConverter。这个 MappingJackson2HttpMessageConverter 应用了所需的更改(在我的例子中,它传递给它一个 Jackson2ObjectMapperBuilderPropertyNamingStrategy 设置为 CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES。)
  4. 同样在 WebConfig class、@Override configureMessageConverters() 中并将 (2) 中的 MappingJackson2HttpMessageConverter 添加到消息转换器列表中。
  5. 在测试文件中,添加一个@ContextConfiguration(classes = { WebConfig.class })注解来告知测试你的@Bean
  6. 使用@Autowired注入并访问(2)中定义的@Bean
  7. 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 in XAMPP 表明命名策略使用正确。我相信它之所以如此工作,是因为在独立设置中,除非另有说明,否则始终使用一组默认的消息转换器。这可以在 StandAloneMockMvcBuilder.java(版本 4.1.6,\org\springframework\test\web\servlet\setup\StandaloneMockMvcBuilder.java)中的 setMessageConverters() 函数的注释中看到:

   /**
     * 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 期间未明确告知对消息转换器的更改,它将不会使用这些更改。

或者你可以

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

使用 Spring Boot 1.5.1 我可以做到:

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

    @Autowired
    ObjectMapper objectMapper;
}

以与运行时相同的方式访问配置的 ObjectMapper。

我的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);
        };
    }
}

在 Spring 引导中,当对控制器层 (@WebMvcTest) 进行单元测试时,您可以访问对象映射器,因此您可以在测试用例之前对其进行修改:

@Autowired
private ObjectMapper objectMapper;

@Before
public void init(){
    objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
}