Spring 5 MVC Test with MockMvc, test-context.xml, and annotation-based WebAppConfig (ie, in Java)
Spring 5 MVC Test with MockMvc, test-context.xml, and annotation-based WebAppConfig (ie, in Java)
版本(Spring启动不):
Spring: 5.2.16
web-app / servlet API: 4.0
JUnit: 5.8
Spring MVC 测试不适用于 returns ResponseEntity<ReturnStatus>
的控制器端点,其中 ReturnStatus
是具有适当 getters/setters 的 POJO。触发的异常表明 JSON 转换不适用于 ReturnStatus
。我的研究表明未加载 WebApplicationContext 基于注释的 Java 配置(因此无法识别 Jackson JSON 转换器)。奇怪的是,在 Tomcat 中的非测试部署中,控制器端点工作正常,大概是因为 war 文件中的 web.xml
由 Tomcat.[=34 解析=]
问题:
我如何调整此应用程序的 Spring MVC 测试设置,以便正确加载 WebApplicationContext 基于注释的 Java 配置?例如,这可以在端点测试逻辑(即 JUnit 测试)中明确完成吗?
异常:
14:33:57,765 WARN DefaultHandlerExceptionResolver:199 - Resolved [org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class com.acme.myapp.io.ReturnStatus] with preset Content-Type 'null']
14:33:57,765 DEBUG TestDispatcherServlet:1131 - Completed 500 INTERNAL_SERVER_ERROR
Spring MVC 应用包含以下配置:
test-context.xml
,其中包含 Spring 用于访问数据存储的 bean 配置:
web.xml
,声明并映射 DispatcherServlet
与 WebApplicationContext 的相关设置。
- Java
WebMvcConfigurer
实现中基于注解的配置。
相关摘录自test-context.xml
:
<context:component-scan base-package="com.acme.myapp"/>
<jpa:repositories base-package="com.acme.myapp.repos"/>
<context:property-placeholder location="classpath:/application.properties" />
<!-- Data persistence configuration -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="${db.showSql}" />
<property name="databasePlatform" value="${db.dialect}" />
<property name="generateDdl" value="${db.generateDdl}" />
</bean>
</property>
<property name="packagesToScan">
<list>
<value>com.acme.myapp.dao</value>
</list>
</property>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${db.driver}" />
<property name="url" value="${db.url}" />
<property name="username" value="${db.user}" />
<property name="password" value="${db.pass}" />
<property name="initialSize" value="2" />
<property name="maxActive" value="5" />
<property name="accessToUnderlyingConnectionAllowed" value="true"/>
</bean>
<!-- Set JVM system properties here. We do this principally for hibernate logging. -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject">
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass" value="java.lang.System" />
<property name="targetMethod" value="getProperties" />
</bean>
</property>
<property name="targetMethod" value="putAll" />
<property name="arguments">
<util:properties>
<prop key="org.jboss.logging.provider">slf4j</prop>
</util:properties>
</property>
</bean>
web.xml
的相关摘录(其中 application-context.xml
是我们 test-context.xml
的生产版本):
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>central-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.myapp.MyAppWebAppConfig</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>central-dispatcher</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
摘自 Java WebMvcConfigurer
的实现(即我们合并 Jackson JSON 转换器的地方):
@EnableWebMvc
@Configuration
@ComponentScan(basePackages = { "com.acme.myapp.controllers" })
public class MyAppWebAppConfig implements WebMvcConfigurer
{
private static final Logger logger = LoggerFactory.getLogger(MyAppWebAppConfig.class);
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters)
{
logger.debug("extendMessageConverters ...");
converters.add(new StringHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter(new MyAppObjectMapper()));
}
}
控制器端点如下所示(根位于 /patients
):
@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ReturnStatus> readPatient(
@PathVariable("id") long id
)
{
ReturnStatus returnStatus = new ReturnStatus();
returnStatus.setVersionId("1.0");
...
return new ResponseEntity<ReturnStatus>(returnStatus, httpStatus);
}
使用 JUnit5 和 MockMvc,端点测试如下所示:
@SpringJUnitWebConfig(locations={"classpath:test-context.xml"})
public class PatientControllerTest
{
private MockMvc mockMvc;
@BeforeEach
public void setup(WebApplicationContext wac) {
this.mockMvc = webAppContextSetup(wac).build();
}
@Test
@DisplayName("Read Patient from /patients API.")
public void testReadPatient()
{
try {
mockMvc.perform(get("/patients/1").accept(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
谢谢!
这里有一些选项,可能并不详尽:
- 根据之前的评论,我们可以简单地在
test-context.xml
中使用 <mvc:annotation-driven>
指令。例如:
<bean id="myappObjectMapper" class="com.acme.myapp.MyAppObjectMapper"/>
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<constructor-arg ref="myappObjectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
实际上,该指令避免了加载 MyAppWebAppConfig
的需要,因为 <mvc:annotation-driven>
实际上是 [=45= 中注释 @EnableWebMvc
的 XML 等价物].
- 实施
WebApplicationInitializer
以便在 Java 中有效地执行我们在 web.xml
中配置的内容。例如:
public class MyAppWebApplicationInitializer implements WebApplicationInitializer
{
@Override
public void onStartup(ServletContext container)
{
XmlWebApplicationContext appCtx = new XmlWebApplicationContext();
appCtx.setConfigLocation("classpath:application-context.xml");
container.addListener(new ContextLoaderListener(appCtx));
AnnotationConfigWebApplicationContext dispatcherCtx = new AnnotationConfigWebApplicationContext();
dispatcherCtx.register(MyAppWebAppConfig.class);
ServletRegistration.Dynamic registration = container.addServlet("central-dispatcher", new DispatcherServlet(dispatcherCtx));
registration.setLoadOnStartup(1);
registration.addMapping("/api/*");
}
}
对于此解决方案,我们从项目中删除 web.xml
;可能我们也应该参数化对 application-context.xml
的引用。
请注意,当我 运行 JUnit5 测试时,似乎 Spring 没有 实例 MyAppWebApplicationInitializer
,而是 Spring 为 JUnit5 加载的上下文是由 @SpringJUnitWebConfig
注释引用的上下文。因此,我建议将与测试相关的配置与 test-context.xml
放在一起,并为生产保留 WebApplicationInitializer
。
我确定还有其他选择,但我只探索了这两种方法。
版本(Spring启动不):
Spring: 5.2.16
web-app / servlet API: 4.0
JUnit: 5.8
Spring MVC 测试不适用于 returns ResponseEntity<ReturnStatus>
的控制器端点,其中 ReturnStatus
是具有适当 getters/setters 的 POJO。触发的异常表明 JSON 转换不适用于 ReturnStatus
。我的研究表明未加载 WebApplicationContext 基于注释的 Java 配置(因此无法识别 Jackson JSON 转换器)。奇怪的是,在 Tomcat 中的非测试部署中,控制器端点工作正常,大概是因为 war 文件中的 web.xml
由 Tomcat.[=34 解析=]
问题:
我如何调整此应用程序的 Spring MVC 测试设置,以便正确加载 WebApplicationContext 基于注释的 Java 配置?例如,这可以在端点测试逻辑(即 JUnit 测试)中明确完成吗?
异常:
14:33:57,765 WARN DefaultHandlerExceptionResolver:199 - Resolved [org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class com.acme.myapp.io.ReturnStatus] with preset Content-Type 'null']
14:33:57,765 DEBUG TestDispatcherServlet:1131 - Completed 500 INTERNAL_SERVER_ERROR
Spring MVC 应用包含以下配置:
test-context.xml
,其中包含 Spring 用于访问数据存储的 bean 配置:web.xml
,声明并映射DispatcherServlet
与 WebApplicationContext 的相关设置。- Java
WebMvcConfigurer
实现中基于注解的配置。
相关摘录自test-context.xml
:
<context:component-scan base-package="com.acme.myapp"/>
<jpa:repositories base-package="com.acme.myapp.repos"/>
<context:property-placeholder location="classpath:/application.properties" />
<!-- Data persistence configuration -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="${db.showSql}" />
<property name="databasePlatform" value="${db.dialect}" />
<property name="generateDdl" value="${db.generateDdl}" />
</bean>
</property>
<property name="packagesToScan">
<list>
<value>com.acme.myapp.dao</value>
</list>
</property>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${db.driver}" />
<property name="url" value="${db.url}" />
<property name="username" value="${db.user}" />
<property name="password" value="${db.pass}" />
<property name="initialSize" value="2" />
<property name="maxActive" value="5" />
<property name="accessToUnderlyingConnectionAllowed" value="true"/>
</bean>
<!-- Set JVM system properties here. We do this principally for hibernate logging. -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject">
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass" value="java.lang.System" />
<property name="targetMethod" value="getProperties" />
</bean>
</property>
<property name="targetMethod" value="putAll" />
<property name="arguments">
<util:properties>
<prop key="org.jboss.logging.provider">slf4j</prop>
</util:properties>
</property>
</bean>
web.xml
的相关摘录(其中 application-context.xml
是我们 test-context.xml
的生产版本):
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>central-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.myapp.MyAppWebAppConfig</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>central-dispatcher</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
摘自 Java WebMvcConfigurer
的实现(即我们合并 Jackson JSON 转换器的地方):
@EnableWebMvc
@Configuration
@ComponentScan(basePackages = { "com.acme.myapp.controllers" })
public class MyAppWebAppConfig implements WebMvcConfigurer
{
private static final Logger logger = LoggerFactory.getLogger(MyAppWebAppConfig.class);
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters)
{
logger.debug("extendMessageConverters ...");
converters.add(new StringHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter(new MyAppObjectMapper()));
}
}
控制器端点如下所示(根位于 /patients
):
@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ReturnStatus> readPatient(
@PathVariable("id") long id
)
{
ReturnStatus returnStatus = new ReturnStatus();
returnStatus.setVersionId("1.0");
...
return new ResponseEntity<ReturnStatus>(returnStatus, httpStatus);
}
使用 JUnit5 和 MockMvc,端点测试如下所示:
@SpringJUnitWebConfig(locations={"classpath:test-context.xml"})
public class PatientControllerTest
{
private MockMvc mockMvc;
@BeforeEach
public void setup(WebApplicationContext wac) {
this.mockMvc = webAppContextSetup(wac).build();
}
@Test
@DisplayName("Read Patient from /patients API.")
public void testReadPatient()
{
try {
mockMvc.perform(get("/patients/1").accept(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
谢谢!
这里有一些选项,可能并不详尽:
- 根据之前的评论,我们可以简单地在
test-context.xml
中使用<mvc:annotation-driven>
指令。例如:
<bean id="myappObjectMapper" class="com.acme.myapp.MyAppObjectMapper"/>
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<constructor-arg ref="myappObjectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
实际上,该指令避免了加载 MyAppWebAppConfig
的需要,因为 <mvc:annotation-driven>
实际上是 [=45= 中注释 @EnableWebMvc
的 XML 等价物].
- 实施
WebApplicationInitializer
以便在 Java 中有效地执行我们在web.xml
中配置的内容。例如:
public class MyAppWebApplicationInitializer implements WebApplicationInitializer
{
@Override
public void onStartup(ServletContext container)
{
XmlWebApplicationContext appCtx = new XmlWebApplicationContext();
appCtx.setConfigLocation("classpath:application-context.xml");
container.addListener(new ContextLoaderListener(appCtx));
AnnotationConfigWebApplicationContext dispatcherCtx = new AnnotationConfigWebApplicationContext();
dispatcherCtx.register(MyAppWebAppConfig.class);
ServletRegistration.Dynamic registration = container.addServlet("central-dispatcher", new DispatcherServlet(dispatcherCtx));
registration.setLoadOnStartup(1);
registration.addMapping("/api/*");
}
}
对于此解决方案,我们从项目中删除 web.xml
;可能我们也应该参数化对 application-context.xml
的引用。
请注意,当我 运行 JUnit5 测试时,似乎 Spring 没有 实例 MyAppWebApplicationInitializer
,而是 Spring 为 JUnit5 加载的上下文是由 @SpringJUnitWebConfig
注释引用的上下文。因此,我建议将与测试相关的配置与 test-context.xml
放在一起,并为生产保留 WebApplicationInitializer
。
我确定还有其他选择,但我只探索了这两种方法。