0

Versions (SpringBoot is not involved):

Spring: 5.2.16
web-app / servlet API: 4.0
JUnit: 5.8

Spring MVC Testing is not working for controller endpoint that returns ResponseEntity<ReturnStatus>, where ReturnStatus is a POJO with appropriate getters/setters. The exception triggered indicates that JSON conversion is not working for ReturnStatus. My research indicates that the annotation-based Java configuration for the WebApplicationContext is not loaded (and therefore the Jackson JSON converter is not recognized). Curiously, in a non-testing deployment in Tomcat, the controller endpoint works fine, presumably because the web.xml in the war-file is parsed by Tomcat.

QUESTION:
How can I adjust the setup for Spring MVC Test for this application so that the annotation-based Java configuration for the WebApplicationContext is properly loaded? Can this, for example, be done explicitly in the endpoint-test logic (ie, the JUnit test)?

Exception:

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

The Spring MVC app incorporates the following configurations:

  1. test-context.xml, which houses Spring bean-configuration for access to data store:
  2. web.xml, which declares and maps the DispatcherServlet with relevant setup for WebApplicationContext.
  3. Annotation-based configuration in Java implementation of WebMvcConfigurer.

Relevant excerpt from 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>

Relevant excerpt from web.xml (where application-context.xml is our production version of 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>

Excerpt from Java implementation of WebMvcConfigurer (ie, where we incorporate Jackson JSON converter):

@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()));
  }
}

The controller endpoint looks like this (where the root is at /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);
  }

Using JUnit5 and MockMvc, the endpoint-test looks like this:

@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();
    }
  }
}

Thanks!

1
  • I've resolved my testing configuration by simply adding suitable <mvc:annotation-driven> directive to test-context.xml. But my question still remains: when you use annotation-based Java configuration for defining WebApplicationContext, how do you get that into class to load into your JUnit5 test? Commented Sep 17, 2021 at 0:30

1 Answer 1

0

Here are some options, possibly not exhaustive:

  • Per earlier comment, we can simply use <mvc:annotation-driven> directive in test-context.xml. For example:
  <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>

Effectively, this directive obviates the need for loading MyAppWebAppConfig, as <mvc:annotation-driven> in fact is the XML-equivalent of the annotation @EnableWebMvc in Java.

  • Implement WebApplicationInitializer so that effectively does in Java what we configure into web.xml. For example:
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/*");
  }  
}

For this solution, we expunge web.xml from the project; possibly we should parameterize the reference to application-context.xml as well.

Note that when I run JUnit5 tests, it appears that Spring does not instance MyAppWebApplicationInitializer, and that instead, the Spring context loaded for JUnit5 is the one referenced by the @SpringJUnitWebConfig annotation. I therefore recommend co-locating test-related configuration with test-context.xml, and reserving WebApplicationInitializer for production.

I'm sure there are other options, but I've only explored these two approaches.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.