spring - jsonview - ObjectMapper personalizado no utilizado en la prueba
spring mvc jackson example (3)
Busqué entender por qué esto funciona de la manera en que lo hizo. Para reiterar, el proceso para que mi ObjectMapper personalizado funcione en mi prueba (suponiendo que MockMvc se está creando como independiente) es el siguiente:
- Cree una clase
WebConfig
que amplíeWebMvcConfigurerAdapter
. - En la clase
WebConfig
, cree un nuevo@Bean
que devuelva unMappingJackson2HttpMessageConverter
. EsteMappingJackson2HttpMessageConverter
tiene los cambios deseados aplicados (en mi caso, le estaba pasando unJackson2ObjectMapperBuilder
conPropertyNamingStrategy
establecido enCAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES
). - También en la clase
WebConfig
,@Override
configureMessageConverters()
y agregueMappingJackson2HttpMessageConverter
de (2) a la lista de convertidores de mensajes. - En el archivo de prueba, agregue una
@ContextConfiguration(classes = { WebConfig.class })
para informar la prueba de su@Bean
. - Utilice
@Autowired
para inyectar y acceder al@Bean
definido en (2). - En la configuración de
MockMvc
, use el método.setMessageConverters()
yMappingJackson2HttpMessageConverter
elMappingJackson2HttpMessageConverter
inyectado. La prueba ahora utilizará la configuración establecida en (2).
El archivo de prueba:
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());
}
}
Y el archivo de configuración:
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;
}
}
Al implementar mi archivo WAR generado en Tomcat 7 en XAMPP, se muestra que la estrategia de asignación de nombres se está utilizando correctamente. La razón por la que creo que esto funciona de la manera en que lo hace es porque con una configuración independiente, siempre se usa un conjunto predeterminado de convertidores de mensajes, a menos que se especifique lo contrario. Esto se puede ver en el comentario de la función setMessageConverters()
dentro de StandAloneMockMvcBuilder.java (versión 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;
}
Por lo tanto, si no se informa explícitamente a MockMvc sobre los cambios realizados en los conversores de mensajes durante la creación de MockMvc, no se utilizarán los cambios.
Estoy utilizando Spring Framework, versión 4.1.6, con servicios web de Spring y sin Spring Boot. Para aprender el marco, estoy escribiendo una API REST y estoy probando para asegurarme de que la respuesta JSON recibida al llegar a un punto final sea correcta. Específicamente, estoy tratando de ajustar PropertyNamingStrategy
ObjectMapper
para usar la estrategia de nomenclatura "en minúsculas con guiones bajos".
Estoy usando el método detallado en el blog de Spring para crear un nuevo ObjectMapper
y agregarlo a la lista de convertidores. Esto es como sigue:
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;
}
}
Luego ejecuto la siguiente prueba (utilizando JUnit, MockMvc y Mockito) para verificar mis cambios:
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());
}
}
Sin embargo, esto devuelve una respuesta de:
{"id": 1, "entityName": "expected", "entityTitle": "expected title"}
Cuando debería obtener:
{"id": 1, "entity_name": "expected", "entity_title": "expected title"}
Tengo un WebApplicationInitializer implementado que busca el paquete:
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));
}
}
Al usar mi depurador dentro de IntelliJ, puedo ver que el generador se crea y agrega, pero en algún lugar de la línea no se usa el ObjectMapper
resultante. Debo estar perdiendo algo, ¡pero todos los ejemplos que logré encontrar no parecen mencionar lo que es eso! He intentado eliminar @EnableWebMvc
e implementar WebMvcConfigurationSupport
, utilizando MappingJackson2HttpMessageConverter
como Bean y configurando ObjectMapper
como Bean en vano.
Cualquier ayuda sería muy apreciada! Por favor, hágamelo saber si se requieren otros archivos.
¡Gracias!
EDIT: Estaba haciendo un poco más de excavación y encontré this . En el enlace, el autor agrega setMessageConverters()
antes de que él / ella construya MockMvc y funcione para el autor. Hacer lo mismo me funcionó a mí también; sin embargo, no estoy seguro de si todo funcionará en producción ya que los repositorios aún no se han vaciado. Cuando lo descubra le enviaré una respuesta.
EDIT 2: ver respuesta.
Con Spring Boot 1.5.1 puedo hacer:
@RunWith(SpringRunner.class)
@AutoConfigureJsonTesters
@JsonTest
public class JsonTest {
@Autowired
ObjectMapper objectMapper;
}
para acceder al ObjectMapper configurado de la misma manera que en el tiempo de ejecución.
Mi runtime jackson está configurado así:
@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);
};
}
}
o tu puedes
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new
MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setObjectMapper( new ObjectMapper().setPropertyNamingStrategy(
PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES) );
mockMvc = MockMvcBuilders.standaloneSetup(attributionController).setMessageConverters(
mappingJackson2HttpMessageConverter ).build();