Component test repeatability
Many web applications work against databases, man. Databases, however, present quite a large dependency for testing, leaving one with two choices- either mock out as much as possible and avoid the database all together for as long as possible or suck it up and utilize the database. The latter choice presents a new series of challenges- how does one control the database during testing? Even better, how does one make those tests repeatable?
By far the easiest way to make your testing cake and eat it is to use a database seeding framework like any of the xDbUnits. These hip frameworks abstract a database’s dataset into XML files and then offer the developer fine grained control as to how this data is seeded into a database during testing.
For example, the following snippet is from a DbUnit XML seed file.
<word WORD_ID="1" SPELLING="pugnacious" PART_OF_SPEECH="Adjective"/>
<definition DEFINITION_ID="10"
DEFINITION="Combative in nature; belligerent."
WORD_ID="1"
EXAMPLE_SENTENCE="The pugnacious youth had no friends left to pick on."/>
<synonym SYNONYM_ID="20" WORD_ID="1" SPELLING="belligerent"/>
<synonym SYNONYM_ID="21" WORD_ID="1" SPELLING="aggressive"/>
Via DbUnit’s DatabaseTestCase, the data in the XML file is manipulated via operations such as insert, update, and delete. The specific database is configured by implementing the abstract getConnection method and the XML file is located via the getDataSet method. You can dig it below.
package test.org.aglover.words.dao;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Iterator;
import junit.framework.TestCase;
import org.aglover.words.bizobj.IDefinition;
import org.aglover.words.bizobj.IWord;
import org.aglover.words.dao.exception.FindException;
import org.aglover.words.dao.impl.WordDAOImpl;
import org.dbunit.DatabaseTestCase;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
public class DefaultWordDAOImplTest extends DatabaseTestCase {
/**
* @see org.dbunit.DatabaseTestCase#getDataSet()
*/
protected IDataSet getDataSet() throws Exception {
return new FlatXmlDataSet(
new File("test/conf/words-seed.xml"));
}
/**
* @see org.dbunit.DatabaseTestCase#getConnection()
*/
protected IDatabaseConnection getConnection() throws Exception {
final Class driverClass =
Class.forName("org.gjt.mm.mysql.Driver");
final Connection jdbcConnection =
DriverManager.getConnection(
"jdbc:mysql://localhost/words",
"words", "words");
return new DatabaseConnection(jdbcConnection);
}
public void testFindVerifyDefinition() throws Exception{
final WordDAOImpl dao = new WordDAOImpl();
final IWord wrd = dao.findWord("pugnacious");
for(Iterator iter = wrd.getDefinitions().iterator(); iter.hasNext();){
IDefinition def = (IDefinition)iter.next();
assertEquals("Combative in nature; belligerent.",
"Combative in nature; belligerent.",
def.getDefinition());
}
}
public DefaultWordDAOImplTest(String arg0) {
super(arg0);
}
}
Note though, that this trippin’ class makes the assumption that the database is located on the same machine that the test is run. This maybe a safe assumption on the desktop, but obviously this configuration can present a challenge in CI environments.
One solution is to pull out the hard coded connection strings and place them into properties files. There is however, a more effective mechanism. If DbUnit is utilized to seed a database, one can infer that the application itself then uses a database. If this is the case, it is a common best practice to avoid hard coding connection information within a code base; therefore, why not configure DbUnit to read the same file that the application under test reads? Far out!
For example, in Hibernate applications, database connection information is usually defined in the hibernate.cfg.xml file. One can easily write a utility class which parses this file and obtains the proper connection information. Even better, one can rely on Hibernate to provide the desired information as shown below.
//imports left out
public class DBUnitHibernateConfigurator {
static Configuration configuration = null;
private DBUnitHibernateConfigurator() {
super();
}
private static Configuration getConfiguration()
throws HibernateException {
if (configuration == null) {
configuration = new Configuration().configure();
}
return configuration;
}
public static IDataSet getDataSet(final String fileName)
throws ResourceNotFoundException,
DBUnitHibernateConfigurationException {
try{
return DBUnitConfigurator.getDataSet(fileName);
}catch(DBUnitConfigurationException e2){
throw new DBUnitHibernateConfigurationException(
"DBUnitConfigurationException in getDataSet", e2);
}
}
private static String getProperty(final String name)
throws HibernateException {
return getConfiguration().getProperty(name);
}
public static Properties getHibernateProperties()
throws ResourceNotFoundException,
DBUnitHibernateConfigurationException{
try{
final Properties hProp = new Properties();
hProp.put("hibernate.connection.driver_class",
DBUnitHibernateConfigurator.getProperty(
"hibernate.connection.driver_class"));
hProp.put("hibernate.connection.url",
DBUnitHibernateConfigurator.getProperty(
"hibernate.connection.url"));
hProp.put("hibernate.connection.username",
DBUnitHibernateConfigurator.getProperty(
"hibernate.connection.username"));
hProp.put("hibernate.connection.password",
DBUnitHibernateConfigurator.getProperty(
"hibernate.connection.password"));
return hProp;
}catch(HibernateException e){
throw new DBUnitHibernateConfigurationException(
"HibernateException in getHibernatePropertiesFile", e);
}
}
public static IDatabaseConnection getDBUnitConnection()
throws DBUnitHibernateConfigurationException{
try{
final Properties props =
DBUnitHibernateConfigurator.getHibernateProperties();
return DBUnitConfigurator.getDBUnitConnection(props);
}catch(DBUnitConfigurationException e1){
throw new DBUnitHibernateConfigurationException(
"DBUnitConfigurationException in getDBUnitConnection", e1);
}catch (ResourceNotFoundException e2) {
throw new DBUnitHibernateConfigurationException(
"ResourceNotFoundException in getDBUnitConnection", e2);
}
}
}
Note how the above class puts the Hibernate connection info in a Properties object, which is then converted into DbUnit’s IDatabaseConnection type in a DBUnitConfigurator class. The DbUnit connection type is then returned via the getDBUnitConnection method. DbUnit’s IDataSet type, which represents those XML files containing all the data is returned via the getDataSet method. This copasetic method frees developers from having to provide a path to a file- something that can be tricky in different environments.
Next, a custom abstract test case class can be created which requests that implementers feed the desired dataset information for a particular test case.
//left out imports...
public abstract class DefaultDBUnitHibernateTestCase extends DatabaseTestCase {
public DefaultDBUnitHibernateTestCase(String arg0) {
super(arg0);
}
protected void setUp() throws Exception {
super.setUp();
DefaultHibernateSessionFactory.
closeSessionAndEvictCache();
DefaultHibernateSessionFactory.
getInstance().getHibernateSession();
}
protected void tearDown() throws Exception {
DefaultHibernateSessionFactory.
closeSessionAndEvictCache();
super.tearDown();
}
protected IDatabaseConnection getConnection() throws Exception {
return DBUnitHibernateConfigurator.
getDBUnitConnection();
}
protected IDataSet getDataSet() throws Exception {
final String fileName = this.getDBUnitDataSetFileForSetUp();
DatabaseTestCase.assertNotNull("data set file was null", fileName);
return DBUnitHibernateConfigurator.getDataSet(fileName);
}
protected abstract String getDBUnitDataSetFileForSetUp();
}
An example resulting test case which implements DefaultDBUnitHibernateTestCase is shown below.
//imports left out
public class WordDAOImplTest extends DefaultDBUnitHibernateTestCase {
public void testUpdateWordSpelling() throws Exception{
WordDAOImpl dao = new WordDAOImpl();
IWord wrd = dao.findWord("pugnacious");
wrd.setSpelling("pugnacious-ness");
dao.updateWord(wrd);
IWord wrd2 = dao.findWord("pugnacious-ness");
assertEquals("should be id of 1", 1, wrd2.getId());
}
public void testFindVerifyDefinitionsSize() throws Exception{
WordDAOImpl dao = new WordDAOImpl();
IWord wrd = dao.findWord("pugnacious");
Set defs = wrd.getDefinitions();
assertEquals("size should be one", 1, defs.size());
}
protected String getDBUnitDataSetFileForSetUp() {
return "words-seed.xml";
}
public WordDAOImplTest(String arg0) {
super(arg0);
}
}
DbUnit offers an API (as seen above), which can be utilized effectively via composition- creating enormous opportunities for powerful combination frameworks too. With this added flexibility, testing various architectures at different layers in the Age of Aquarius becomes quite easy.
For example, developer testing of Struts applications can be challenging. A common tactic is to utilize a framework like HttpUnit, which simulates http requests; however, this can be tedious work and doesn’t offer the precision one may desire in a Struts architecture which heavily utilizes Action classes and a configuration for mapping requests.
The StrutsTestCase project was created to address this issue- with this framework one can easily isolate and test Struts’ Action classes. This project, however, requires a developer extend a base class which handles mocking of a servlet container. If a Struts application requires the use of a database you may be left in a quandary.
Via DbUnit’s API, a combination framework can be created which utilizes the seeding capabilities of DbUnit with the mocking capabilities of the StrutsTestCase project.
package com.vanward.resource.unittest.dbunit.struts;
import java.util.Properties;
import org.dbunit.DatabaseTestCase;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.operation.DatabaseOperation;
import servletunit.struts.MockStrutsTestCase;
import com.vanward.resource.unittest.dbunit.\
util.DBUnitConfigurator;
public abstract class DefaultDBUnitMockStrutsTestCase
extends MockStrutsTestCase {
public DefaultDBUnitMockStrutsTestCase(String testName) {
super(testName);
}
public void setUp() throws Exception {
super.setUp();
this.executeOperation(this.getSetUpOperation());
}
public void tearDown() throws Exception{
super.tearDown();
this.executeOperation(this.getTearDownOperation());
}
private void executeOperation(DatabaseOperation operation)
throws Exception{
if (operation != DatabaseOperation.NONE){
final IDatabaseConnection connection =
this.getConnection();
try{
operation.execute(connection, this.getDataSet());
}finally{
closeConnection(connection);
}
}
}
protected void closeConnection(IDatabaseConnection connection)
throws Exception{
connection.close();
}
protected abstract Properties getConnectionProperties();
protected abstract String getDBUnitDataSetFileForSetUp();
protected IDatabaseConnection getConnection() throws Exception {
final Properties dbPrps = this.getConnectionProperties();
DatabaseTestCase.
assertNotNull("database properties were null", dbPrps);
return DBUnitConfigurator.getDBUnitConnection(dbPrps);
}
protected DatabaseOperation getSetUpOperation() throws Exception {
return DatabaseOperation.CLEAN_INSERT;
}
protected DatabaseOperation getTearDownOperation() throws Exception {
return DatabaseOperation.NONE;
}
protected IDataSet getDataSet() throws Exception {
final String fileName = this.getDBUnitDataSetFileForSetUp();
DatabaseTestCase.assertNotNull("data set file was null", fileName);
return DBUnitConfigurator.getDataSet(fileName);
}
}
One again, you may be left with the option of hardcoding connection information or reusing existing files for this purpose! Testing a Struts application that uses Hibernate? Not a problem- just combine the new DefaultDBUnitMockStrutsTestCase with a handy dandy utility for reading Hibernate files.
For example, below is a class which implements a DefaultMerlinMockStrutsTestCase class, which combines the DbUnit ability of DefaultDBUnitMockStrutsTestCase with the handy Hibernate reader utility defined way (way!) up above.
public class ProjectListActionTest
extends DefaultMerlinMockStrutsTestCase {
public void testProjectListAction() throws Exception{
this.setRequestPathInfo("/viewProjects");
this.actionPerform();
this.verifyForward("success");
IProject[] projects = (IProject[])this.getRequest().
getAttribute("projects");
assertNotNull("object was null", projects);
}
public ProjectListActionTest(String arg0) {
super(arg0);
}
protected String getDBUnitDataSetFileForSetUp() {
return "dbunit-project-seed.xml";
}
}
Now you have one excellent trippin’ test case, making it difficult for anyone to complain they can’t test this application in a repeatable manner!
| Related odds and ends | ||
|---|---|---|
Thursday 23 Feb 2006 | Developer Testing, JUnit
Hi!
Do you have experience in using dbunit with hibernate? I am trying to test my hibernate-based services using dbunit but using dbunit the sequences used by hibernate are not updated, which results in an error. Do you have a solution for this?
Hello,
I would like to use DBUnit with StrutsTestCase.
In your example, what is DefaultMerlinMockStrutsTestCase?
DefaultMerlinMockStrutsTestCase is a custom class that essentially combines the functionality of both DbUnit and StrutsTestCase for a specific project. This class extends DefaultDBUnitMockStrutsTestCase and adds some specific information on where the Struts config file lives, etc. The DefaultDBUnitMockStrutsTestCase as coded above should be all you need to get going!
thanks you,
I trying to mixt DBUnit with a test with MockStrutsTestCase and the context spring, it is not easy.
What happens when you try and run this?
[...] Component test repeatability (The Disco Blog, 02/06) [...]
[...] is located via the getdataset method. … however, this can be tedious work and doesn????????t …http://thediscoblog.com/2006/02/23/component-test-repeatability/Read “Re: How to execute MS Access Query using JDBC?” at Java Forum…help me : I can design the [...]