On dbunit integration
nickarls Feb 15, 2011 3:30 AMSo, I started tinkering with junit-dbunit integration for in-container runs and I thought I'd share some code and see if its useful for someone and talk about how to improve this. It's by no means feature complete, just working out the general architechture. The code is derived from http://ctpjava.blogspot.com/2010/07/test-drive-with-arquillian-and-cdi-part.html
So let's start with the entity, nothing special here:
@Entity
public class Greeting
{
@Id @GeneratedValue
private long id;
private String data;
public long getId()
{
return id;
}
public void setId(long id)
{
this.id = id;
}
public String getData()
{
return data;
}
public void setData(String data)
{
this.data = data;
}
}
to go with it I have in src/test/resources a persistence.xml against the default datasource
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="concept">
<jta-data-source>java:/DefaultDS</jta-data-source>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
</properties>
</persistence-unit>
</persistence>
and in src/test/resources I also have Foo.xls with a single tab named "Greeting" with "Id" in A1, "Data" in B1, "1" in A2 and "Foo" in "B2"
to mark test data I have an annotation @Data, which is used like @Data(xls = "Foo.xls", ds = "java:/DefaultDS"), you get the point. It looks like
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Data
{
String xls();
String ds();
}
and it's used by a junit @Rule which looks like
public class DataRule extends TestWatchman implements MethodRule
{
private abstract class DataSeederOperation
{
protected ExcelDataSeeder dataSeeder;
public DataSeederOperation(FrameworkMethod method)
{
Data data = method.getAnnotation(Data.class);
if (null == data)
{
return;
}
dataSeeder = createDataSeeder(data.xls(), data.ds());
}
public void execute()
{
try
{
if (dataSeeder != null)
{
actions();
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
public abstract void actions();
}
public static boolean runningInAS()
{
Exception e = new Exception();
StackTraceElement[] stack = e.getStackTrace();
for (int i = stack.length - 1; i >= 0; i--)
{
if (stack[i].getClassName().contains("http11"))
{
return true;
}
}
return false;
}
@Override
public void starting(FrameworkMethod method)
{
if (!runningInAS())
{
return;
}
new DataSeederOperation(method)
{
@Override
public void actions()
{
dataSeeder.prepare();
}
}.execute();
}
@Override
public void finished(FrameworkMethod method)
{
if (!runningInAS())
{
return;
}
new DataSeederOperation(method)
{
@Override
public void actions()
{
dataSeeder.cleanup();
}
}.execute();
}
private ExcelDataSeeder createDataSeeder(String xlsName, String dsJndi)
{
DataSource dataSource = null;
if (xlsName == null || "".equals(xlsName))
{
throw new RuntimeException("xlsName cannot be null");
}
if (dsJndi == null || "".equals(dsJndi))
{
throw new RuntimeException("dsJndi cannot be null");
}
try
{
dataSource = (DataSource) new InitialContext().lookup(dsJndi);
}
catch (NamingException e)
{
throw new RuntimeException(String.format("DataSource %s was not found", dsJndi));
}
InputStream xls = getClass().getClassLoader().getResourceAsStream(xlsName);
if (xls == null)
{
throw new RuntimeException(String.format("Excel file %s was not found", xlsName));
}
try
{
return new ExcelDataSeeder(xls, dataSource.getConnection());
}
catch (Exception e)
{
throw new RuntimeException("ExcelSeeder could not be created", e);
}
}
}
which uses an ExcelDataSeeder to populate and clean out the database. There is also an unorthodox check to see if we're running on the server side or the client side, since Rules are run in both places. The ExcelDataSeeder looks like
public class ExcelDataSeeder
{
private DatabaseConnection databaseConnection;
private final InputStream xlsStream;
public ExcelDataSeeder(InputStream xlsStream, Connection connection) throws DatabaseUnitException
{
databaseConnection = new DatabaseConnection(connection);
this.xlsStream = xlsStream;
}
public void prepare()
{
try
{
setupDatabase();
fillDatabase();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
public void cleanup()
{
try
{
setupDatabase();
cleanDatabase();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
private void setupDatabase() throws IOException, SQLException, DatabaseUnitException
{
databaseConnection.getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new DefaultDataTypeFactory());
}
private void fillDatabase() throws IOException, SQLException, DatabaseUnitException
{
XlsDataSet ds = new XlsDataSet(xlsStream);
DatabaseOperation.CLEAN_INSERT.execute(databaseConnection, ds);
}
private void cleanDatabase() throws DatabaseUnitException, SQLException
{
IDataSet dataSet = databaseConnection.createDataSet();
DatabaseOperation.DELETE_ALL.execute(databaseConnection, dataSet);
}
}
and the test itself looks like
@RunWith(Arquillian.class)
public class DBTest
{
@PersistenceContext
EntityManager em;
@Resource
UserTransaction ut;
@Rule
public DataRule rule = new DataRule();
@Deployment
public static WebArchive createTestArchive()
{
return ShrinkWrap.create(WebArchive.class, "test.war").addLibrary(new File("C:/java/m2repo/org/apache/poi/poi/3.2-FINAL/poi-3.2-FINAL.jar")).addLibrary(new File("C:/java/m2repo/org/dbunit/dbunit/2.4.8/dbunit-2.4.8.jar")).addResource("Foo.xls", ArchivePaths.create("WEB-INF/classes", "Foo.xls")).addClasses(Data.class, DataRule.class, ExcelDataSeeder.class, Greeting.class).addManifestResource(EmptyAsset.INSTANCE, ArchivePaths.create("beans.xml")).addManifestResource("persistence.xml", ArchivePaths.create("persistence.xml"));
}
@Test
@Data(xls = "Foo.xls", ds = "java:/DefaultDS")
public void testPreloaded() throws NotSupportedException, SystemException
{
try
{
ut.begin();
Greeting g = new Greeting();
em.persist(g);
Assert.assertEquals(2, em.createQuery("select g from Greeting g", Greeting.class).getResultList().size());
}
finally
{
ut.rollback();
}
}
}
This should enable you to use in-container tests that have be pre-populated with excel data.
Now there are some clear points of improvement that could be done
- Re-introduce interfaces from the original article to support multiple seeding formats
- Package the db-support stuff in a jar of it's own
- Beef up the @Data annotation to have a transactional attribute that does the UT-stuff (and a rollback attribute perhaps), pre-query, post-query etc)
- Better integration so the precence of a @Data would pack the xls and the required libs into the test archive (pointers on how this could be done?)
Other suggestions are welcome!