This article is mainly about using spring-test for integration test. Some basic issuses of integration test are discussed for projects with following natures:
- It's a maven project
- It's also a Spring webmvc proejct
- It's developed in Eclipse.
This article is based on Spring framework 4.2 +
0. Basics
0.1 Unit test and integration test
Test target of unit test is every individual POJO, if this target object need other external objects, mock them. In a spring web/rest application, unit tests run without Spring context. JUnit test runner usually sets to someone from mock framework. such as @RunWith(MockitoJUnitRunner.class) if Mockito is used.
From "unit test"'s view, it doesn't matter if you application is a Spring mvc application. It even doesn't matter if you application uses spring or not. Because unit tests only verify a bunch of classes one by one, decouple them with the help of mock.
Test target of integration test is functions of a group of components. In a spring web/rest application, spring-test provide Spring TestContext Frame to simulate a servlet container, so your code can run in a web context without deployment to a real server. JUnit test runner usually sets to the one provided by spring-test, like @RunWith(SpringJUnit4ClassRunner.class)
0.2 JUnit, Running test in eclipse, Running test by maven
JUnit is just a test framework, it has no idea of the currently running test is a "unit test" or an "integration test". Or we can say, a JUnit test case can be a unit test, it can also be a integration test.
Eclipse has no concept of integration test but only unit test. Eclipse has no requirement about test case class names. e.g a class named "BookRestControllerAbc.java" can run as a JUnit test.
Maven uses plugin "surefire" to run "unit test", use plugin "failsafe" to run "integration" test. They have decidedly naming conventions.
Surefire plugin by default will automatically run test cases with following names:
- "**/Test*.java"
- "**/*Test.java"
- "**/*TestCase.java"
Failsafe plugin by default will automatically run test cases with following names:
- "**/IT*.java"
- "**/*IT.java"
- "**/*ITCase.java"
So the test cases can be categoried by their class names.
0.3 Watch out dir /src/main/webapp
Even it's a maven project in eclipse, eclipse and maven have their own way to manage dependencies and resources. This is the key to understand the problems like "Why I can run my test case in eclipse but will can't run the same test in maven build?" or the reverse one, "Why I can my test case with maven but can't run it in Eclipse". Usually the cause of these kinds of problems seem like can't find some resources in a certain senario.
Here is the dilemma:
In Eclipse, create a maven web application will create directory /src/main/webapp as web root.
web.xml must in directory /src/main/webapp/WEB-INF. The default spring configuration, either applicationContext.xml or dispatcher-servlet.xml required by Spring webmvc need to be in the same directory /src/main/webapp/WEB-INF.
But by default /src/main/webapp is NOT a resource directory for both maven and Eclipse.
You can succeed in "Run on Server" in Eclipse, because web root /src/main/webapp also get packaged with the help of package behavior for war in Eclipse.
But for integration tests, they will definitly fail no matter using maven or in Eclipse. Since neither applicationContext.xml nor dispatcher-servlet.xml in web/main/webapp are recognized as resources. The error message will be some kind of file not found.
If you simply move, e.g dispatcher-servlet.xml from /src/main/webapp/WEB-INF/ to maven's resource path like /src/main/resources/WEB-INF/, the application can no longer run in Eclipse. The error message will complain can not find /WEB-INF/dispatcher-servlet.xml or similar.
So here's the best way in my experience:
- Move spring configuration applicationContext.xml or dispatcher-servlet.xml to maven resources path /main/resources
- In web.xml, set context location for servlet dispatcher like below
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- specify location of spring config, don't use default --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:dispatcher-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
The final project hierarchy like below.
Since resources are now in the path recognized by both Maven and Eclipse, we can successfully run our web application in Eclipse as well as run integration test in maven command line or in Eclipse.
This probably make you stop using xml-styled spring configuration and embrace the Java-styled spring configuration for the sake of a easier life.
1. Config pom.xml for integration test
Maven has failsafe plugin for integration test.
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.19.1</version> <configuration> <encoding>UTF-8</encoding> </configuration> <!-- uncomment to bind maven lifecycle <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> --> </plugin>
Without bind to any lifecycle like above, you can run integration test with command
mvn failsafe:integration-test
2. Spring integration test case with JUnit
Cheat sheet for creating Spring integration test with JUnit.
- Set JUnit runner @RunWith(SpringJUnit4ClassRunner.class)
- Load spring config with @WebAppConfiguration and @ContextConfiguration("classpath:dispatcher-servlet.xml")
- @Autowire WebApplicationContext wac in JUnit test case class.
- Create MockMvc in JUnit's @Before method, mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
- In test case method, use MockMvc.perform(MockMvcRequestBuilders.xxx()) to send request and andExpect(MockMvcResultMatchers.yyy) to verify response.
3. possible issues
3.1 javax.servlet.ServletContext Not Found
This error message often happens when running integration test with plugin failsafe. You may see an error message like below.
Failed to instantiate [org.springframework.test.context.web.WebDelegatingSmartContextLoader]: Constructor threw exception; nested exception is java.lang.NoClassDefFoundError: javax/servlet/ServletContext
Add the following dependency can solve the problem.
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency>
Make sure the major version (3.0) should be same as specified in web.xml.
3.2 Could not obtain DataSource when using @Sql + JPA
When testing, you probably like to use memory database such as HSQL, H2 or Derby. If all the connection configurations. e.g driver, url, username and password, are in JPA /META-INF/persistence.xml file, not in spring's configuration, you may see an error like below complain couldn't obtain DataSource from transaction manager when using @Sql to insert test data before a certain test case.
java.lang.IllegalStateException: Failed to execute SQL scripts for test context ..(some text ignored).. could not obtain DataSource from transaction manager [org.springframework.orm.jpa.JpaTransactionManager] (named '').
This because the @Sql need DataSource in spring context, just add a DataSource in your spring configuration. Make sure the this datasource point to the same database as in the persistence.xml. For example, if the persistence.xml using a H2 database like below,
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> <persistence-unit name="spring-persistence-jpa-tx" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:mydb" /> <property name="javax.persistence.jdbc.user" value="sa" /> <property name="javax.persistence.jdbc.password" value="" /> <!-- Automatically drop then create table --> <property name="hibernate.hbm2ddl.auto" value="create" /> <!-- print out sql --> <property name="hibernate.show_sql" value="true"/> </properties> </persistence-unit> </persistence>
Then we need to add a DataSource bean in Spring configure to be able to use @Sql annotation. In xml-styled configuration, with spring-jdbc's help, it's easy to create a datasource using embedded database.
<?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:beans="http://www.springframework.org/schema/beans" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation=" http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> <!-- omit other beans --> <jdbc:embedded-database id="usually_is_dataSource" type="H2" database-name="mydb"> <!-- <jdbc:script location="classpath:anyfile1.sql"/> <jdbc:script location="classpath:anyfile2.sql"/> --> </jdbc:embedded-database> </beans>
Notice if using the embedded memory, the database'name is important. The database-name MUST be same as what in persistence.xml. In this demo it's 'mydb'.
4. A complete demo
In the end of the article,a demo of integration test for get resource of a REST web service is provided. It inerts test data before test running, then runs the junit test case try to access a url and check the status of the response is 200. Finally, restores the database by deleting the inserted test data.
package com.shengwang.demo.controller; // import ignored @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration({"classpath:/WEB-INF/dispatcher-servlet.xml"}) public class BookRestControllerIT { @Autowired WebApplicationContext wac; private MockMvc mockMvc; @Before public void setUp() throws Exception { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } @Test // @Sql(scripts="classpath:insertOneBook.sql") @SqlGroup ({ @Sql(statements="insert into book (book_id,title) values (1,'Test title')"), @Sql(statements="delete from book where book_id=1",executionPhase=ExecutionPhase.AFTER_TEST_METHOD) }) public void testFindBookWithExistBookId() { try { mockMvc.perform(get("/book/1")).andExpect(MockMvcResultMatchers.status().isOk()); } catch (Exception e) { fail("testFindBookWithExistBookId failed"); } } }
It passes successfully in Eclipse.
It also passes in maven command line invocation.
mvn clean test-compile failsafe:integration-test
5. More
Here provides a hello world level spring integration test case. In read usage, you should separate your unit test cases from integration test cases. That need more configuration in Eclipse (in Java Build Path) and in Maven pom.xml (with the help of plugin build-helper-maven-plugin) respectively. See another tutorial on "How to separate integration test from unit test"
Wow! The hint you gave in section 3.2 has bugged me the entire last week. You are a life saver. Thanks! :)
ReplyDelete