integration testing with JPA and Spring
I have a Spring/JPA web application that I would like to write some tests for. Ideally I would like to be able to:
- create the test DB schema (from the JPA annotated classes) once before the tests are run
- run each test method in it's own transaction which is rolled back when the test completes
- specify the (DbUnit) dataset to be loaded for each test either at the per-class or per-method level. The test data should be loaded after the transaction has started so that the test data will also be rolled back when the test completes
- inject Spring beans into the test class
I'm aware that Spring provides classes which can provide the transactional behaviour I'm seeking. Ideally, the final solution will look something like this
// This dataset will be used for all tests that don't override it with their own annotation
@TestData('/dbunit/dataSetDefault.xml')
public class MyTests extends ProbablySomethingFromTheSpringFramework {
@Test
void testWithDefau开发者_开发百科ltDataSet() {
// Transaction is implicitly started here
// My test code goes here
// A transaction is implicitly rolled-back here
}
@TestData('/dbunit/dataSetCustom.xml')
@Test
void testWithCustomDataSet() {
// Same as the other test
}
}
Obviously the parent class and the @TestData
are fictitious, is there something available that provides the functionality I'm looking for?
This leaves the question of how to create the test DB schema. Ideally, this would happen once before all the tests are run (by Maven). Can someone suggest how I might achieve this? I imagine it involves using something to convert the JPA annotations to DDL, then something else to load that into the test database schema.
Thanks!
Ideally I would like to be able to:
- create the test DB schema (from the JPA annotated classes) once before the tests are run
At least Hibernate makes it possible to create the database from the annotated classes, I'd imagine other JPA-implementations would work too.
- run each test method in it's own transaction which is rolled back when the test completes
See @TransactionConfiguration and the defaultRollback -value there, and AbstractTransactionalJUnit4SpringContextTests (there are also similar abstract classes for at least JUnit 3.8 and TestNG), take a good look at the See also-sections in the javadocs, they point to many very useful related classes and annotations.
- specify the (DbUnit) dataset to be loaded for each test either at the per-class or per-method level. The test data should be loaded after the transaction has started so that the test data will also be rolled back when the test completes
I haven't actually ever used DbUnit, but at least with JUnit, you can use @Before and @BeforeClass to run methods before each test and class, respectively (there's also @After and @AfterClass). If you have a class hierarchy, the @Before/@BeforeClass -annotated methods are run in extension order (baseclass first). For running sql-scripts, see for example SimpleJdbcTestUtils.
- inject Spring beans into the test class
AbstractTransactionalJUnit4SpringContextTests is ApplicationContextAware, also see @ContextConfiguration for setting things up.
Finally, here's a bit stripped down baseclass I use to extend my actual integration tests from (Spring 3, JUnit4, Hibernate as JPA-provider, if it matters):
//test-context, same as normal context, except uses H2 for in-memory database and has some stuff for faking session- and request-scope
@ContextConfiguration(locations="classpath:applicationContext-test.xml")
@TransactionConfiguration(transactionManager="txManager", defaultRollback=true)
@Transactional
public abstract class IntegrationTestBase extends AbstractTransactionalJUnit4SpringContextTests
{
@PersistenceContext
protected EntityManager em;
@Autowired
protected SomeService serviceAvailableToSubclasses;
@Before
public void beforeEachTest()
{
//fill database with testdata and whatever you need to, runs before each test in extending classes
}
@After
public void afterEachTest()
{
//Do something, if you need to, or just remove this
}
}
Extending from this, you can use @Transactional, @Autowired etc. in your deriving classes, or derive more specific abstract test-baseclasses (I have for example IntegrationSessionTestBase and IntegrationSessionNewRequestPerTestBase for different kinds of tests, needing new sessions and/or requests per test).
I have done that with a simple JPA (Hibernate) based app.
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.soebes.casestudy</groupId>
<artifactId>casestudy</artifactId>
<packaging>jar</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>Case Study Pizza Ordering</name>
<url>Pizza Ordering</url>
<properties>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hibernate-core-version>3.4.0.GA</hibernate-core-version>
<database.driverClassName>com.mysql.jdbc.Driver</database.driverClassName>
<database.url>jdbc:mysql://localhost:3306/casestudy</database.url>
<database.dialect>org.hibernate.dialect.MySQLDialect</database.dialect>
<database.root.user>root</database.root.user>
<database.root.password>root</database.root.password>
<database.user>casestudy</database.user>
<database.password>casestudy</database.password>
<database.database>casestudy</database.database>
</properties>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>hibernate3-maven-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<id>hibernate-create-schema</id>
<phase>generate-test-sources</phase>
<goals>
<goal>hbm2ddl</goal>
</goals>
<configuration>
<components>
<component>
<name>hbm2ddl</name>
<implementation>annotationconfiguration</implementation>
</component>
</components>
<componentProperties>
<configurationfile>/src/main/resources/hibernate.cfg.xml</configurationfile>
<jdk5>true</jdk5>
<packagename>com.soebes.casestudy.bo</packagename>
<console>false</console>
<outputfilename>create.sql</outputfilename>
<drop>false</drop>
<create>true</create>
<update>false</update>
<export>false</export>
<format>true</format>
</componentProperties>
</configuration>
</execution>
<execution>
<id>hibernate-drop-schema</id>
<phase>generate-test-sources</phase>
<goals>
<goal>hbm2ddl</goal>
</goals>
<configuration>
<components>
<component>
<name>hbm2ddl</name>
<implementation>annotationconfiguration</implementation>
</component>
</components>
<componentProperties>
<configurationfile>/src/main/resources/hibernate.cfg.xml</configurationfile>
<jdk5>true</jdk5>
<packagename>com.soebes.casestudy.bo</packagename>
<console>false</console>
<outputfilename>drop.sql</outputfilename>
<drop>true</drop>
<create>false</create>
<update>false</update>
<export>false</export>
<format>true</format>
</componentProperties>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.8</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>sql-maven-plugin</artifactId>
<version>1.4</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
</dependencies>
<!-- common configuration shared by all executions -->
<configuration>
<driver>${database.driverClassName}</driver>
<url>${database.url}</url>
<username>${database.root.user}</username>
<password>${database.root.password}</password>
</configuration>
<executions>
<execution>
<id>drop-database</id>
<phase>generate-test-resources</phase>
<goals>
<goal>execute</goal>
</goals>
<configuration>
<sqlCommand>
DROP DATABASE IF EXISTS casestudy;
CREATE DATABASE casestudy;
GRANT ALL ON casestudy.* TO ${database.user} IDENTIFIED BY '${database.password}';
</sqlCommand>
</configuration>
</execution>
<execution>
<id>create-database</id>
<phase>generate-test-resources</phase>
<goals>
<goal>execute</goal>
</goals>
<configuration>
<sqlCommand>
USE casestudy;
</sqlCommand>
<srcFiles>
<srcFile>${project.build.directory}/hibernate3/sql/create.sql</srcFile>
</srcFiles>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
<version>${hibernate-core-version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>3.3.0.SP1</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.4.GA</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>5.14.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
</project>
精彩评论