I have built a working Spring MVC web app utilising Hibernate and JPA with a single end point that returns JSON only successfully when called through a web browser.
I have also got passing unit tests against classes, in particular the main controller that maps the end point.
So my next step was to write some behavioural tests using the spring MVC test framework to test the JSON endpoint with all its working parts (config, controller, serivice, model and repository).
I have successfully set up a test that uses my application configuration, injects/autowires things together, then makes a 'GET' request against my end point. I can debug the calls, following things through from hitting the controller end point, through the service, into the repository, successfully getting data and returning the correct model object. However, the controller then returns a 406 not accepted response and the test fails.
I have spent 2 days working through this issue and have read just about every blog post and SO answer I can take with no suggestions working.
Here is my Controller:
@Controller
public class DataController {
@Autowired
private IService service;
@RequestMapping(
value = "/data",
method = RequestMethod.GET,
produces = "application/json"
)
public @ResponseBody
ModelData getDataByCode(@RequestParam String code) {
return service.getDataCode(code);
}
}
Service:
@Service("dataService")
public class DataService implements IService {
@Autowired
private IDataRepository dataRepository;
@Transactional
public ModelData getDataByCode(String code){
return dataRepository.getDataByCode(code);
}
}
Repository:
@Repository("dataRepository")
public class DataRepository implements IDataRepository {
@PersistenceContext
private EntityManager entityManager;
public ModelData getDataByCode(String code) throws IllegalStateException, PersistenceException {
String selectJPAQueryBase = "Select data from ModelData data where data.code = %s";
Query query = entityManager.createQuery(String.format(selectJPAQueryBase, code));
return (ModelData) query.getSingleResult();
}
}
Dependencies relevant to the core features:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>3.2.14.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>3.2.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.0.1.Final</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>3.2.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.2.14.RELEASE</version>
</dependency>
<dependency>
<groupId>net.sourceforge.jtds</groupId>
<artifactId>jtds</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-java</artifactId>
<version>1.2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-junit</artifactId>
<version>1.2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.2.14.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>0.8.1</version>
<scope>test</scope>
</dependency>
JPA context:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">
<context:annotation-config />
<!-- where spring will start to look for classes that are annotated, allow this to find all annotated classes -->
<context:component-scan base-package="com.app"/>
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="punit"/>
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="true"/>
</bean>
</property>
<property name="jpaPropertyMap">
<map>
<entry key="hibernate.dialect" value="org.hibernate.dialect.SQLServer2008Dialect"/>
<entry key="hibernate.hbm2ddl.auto" value="validate"/>
<entry key="hibernate.format_sql" value="true"/>
</map>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="net.sourceforge.jtds.jdbc.Driver"/>
<property name="url" value="jdbc:jtds:sqlserver://0.0.0.0:1433/dbname"/>
<property name="username" value="username"/>
<property name="password" value="password"/>
</bean>
</beans>
Web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/jpaContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dataByCode</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/config/servlet-config.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dataByCode</servlet-name>
<url-pattern>/data</url-pattern>
</servlet-mapping>
</web-app>
Servlet config:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">
<!-- allows POJO objects to just use annotations for simply hooking into spring framework -->
<mvc:annotation-driven/>
<!-- where spring will start to look annotated classes, keep this to controller level -->
<context:component-scan base-package="com.app"/>
<!-- load properties files and similar -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="properties"/>
</bean>
<!-- basic resolver to handle jsp pages, usually linked to returning html -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
<property name="order" value="2"/>
</bean>
</beans>
The failing test:
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("classpath:jpaContext.xml")
public class GetDataCodeIntTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build();
}
@Test
public void getDataByCode() throws Exception {
String code = "1234";
this.mockMvc.perform(MockMvcRequestBuilders.get("/data").param("code", code))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(code));
}
}
Key thing here is that the application works perfectly through the browser, but fails using the spring mvc test framework at the point where the controller returns a response.
UPDATE:
The solution was to inject my servlet-config.xml along with the jpaContext into the @ContextConfiguration
, the test ran perfectly after that.
Interestingly I couldn't just load the servlet-config.xml from the classpath however and I didnt want a test specific version to maintain, so I moved it into my resources folder. Whether that was a good idea I'm undecided, but it works from the classpath now.