|
JUnit testing with Maven, Hibernate, Avalon, and EclipseNote: This is a rough draft based on my experiences with trying to find a viable testing infrastructure. Overview - PAULAWhen I first got into testing, I only tested code that didn't require a database backend since I didn't want to go through the burden of figuring out how my testcases could connect to the database. Then I discovered Cactus, which allowed me to shift all of the requirements for obtaining resources like database connections and mail servers etc over to my actual web application. However, this had it's own issues, primarily being feedback time. Cactus tests, because of their very nature, take longer to run then just plain JUnit tests. You have to compile and package a special war file, deploy it to your container, start your container up, run the tests, and then stop the container. Additionally, I found that for much of my business logic that I wanted to test, there was nary a reference to anything that looked like a servlet class, filter or anything! I was using all the "in-container" testing to provide just a simple database connection! This tutorial is about how I structure my projects to allow me to run and step through JUnit tests using a live database. AssumptionsThis tutorial is based on a couple of assumptions. The first and biggest assumption is that you want to use Maven as your build tool. The second biggest assumption is that you are using Hibernate as your ORM tool. The third is that you don't mind using an Avalon container. An Avalon container provides an Inversion of Control (IoC) container that makes it easy to configure componetns. And lastly, that you want to run your unit tests from inside the confort of Eclipse! If after reading this tutorial, you have suggestions, please send them to me at epugh@upstate.com. Project.xml dependencies SetupThis approach leverages using the Avalon container and an Avalon Hibernate wrapper to provide a container to start and run Hibernate, as well as interface with Hibernate. However, the nice thing is that all of these jar files wiill be downloaded for you. Add the following Avalon related jars to your <dependencies/> section:
<dependency>
<id>avalon-framework</id>
<version>4.1.4</version>
<url>http://jakarta.apache.org/avalon</url>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>excalibur-collections</id>
<version>1.0</version>
<url>http://jakarta.apache.org/avalon</url>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>excalibur-component</id>
<version>1.1</version>
<url>http://jakarta.apache.org/avalon</url>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>excalibur-instrument</id>
<version>1.0</version>
<url>http://jakarta.apache.org/avalon</url>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>excalibur-logger</id>
<version>1.0.1</version>
<url>http://jakarta.apache.org/avalon</url>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>excalibur-pool</id>
<version>1.2</version>
<url>http://jakarta.apache.org/avalon</url>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>logkit</id>
<version>1.0.1</version>
<url>http://jakarta.apache.org/avalon/logkit/</url>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
Add the following Hibernate related jars to your <dependencies/> section:
<dependency>
<id>hibernate</id>
<version>2.0.1</version>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>hibernate:hibernate-avalon</id>
<version>0.1</version>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>odmg</id>
<version>3.0</version>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>bcel</id>
<version>5.0</version>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>dom4j</id>
<version>1.4</version>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
<dependency>
<id>cglib</id>
<version>1.0</version>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
This will set up your project to have all the jars it needs to run Hibernate, and to run Hibernate in an Avalon container. File System RequirementsThe next step is to set up the filesystem with the various dependencies that are needed. Obviously, you can tweak these locations to better match your project environment. In my project I have a /src/conf where I place all my configuration files. A table of files /src/conf/hibernate.hbm The mapping file for hibernate. /src/conf/hibernate-TEST.cfg.xml A configuration file for hibernate that doesn't use JNDI. We will use this file to start up hibernate during our unit tests. /src/conf/hibernate-WEBAPP.cfg.xml A configuration file for hibernate that uses JNDI for use in the webapp. This will be used when we build our WAR file. /src/conf/test.avalonconf.xml Component configuration file for Avalon to start up Hibernate for testing. Any other Avalon components used would be listed here. Maven.xml changesNow, we need to tell maven when to copy our hibernate-TEST.cfg.xml file, and when to copy the hibernate-WEBAPP.cfg.xml file. This is done by adding some pre and post goal processing. To tell Maven to copy the hibernate-TEST.cfg.xml file add:
<postGoal name="test:test-resources">
<echo>Moving conf data for hibernate.</echo>
<!-- copy conf data -->
<copy file="src/conf/hibernate-TEST.cfg.xml"
tofile="target\test-classes\hibernate.cfg.xml"/>
</postGoal>
To tell Maven to copy the hibernate-WEBAPP.cfg.xml file add:
<preGoal name="war:webapp">
<j:set var="war.build.dir"
value="${pom.getPluginContext('maven-war-plugin').getVariable('maven.war.build.dir')}"/>
<property name="webapp.build" value="${war.build.dir}/${pom.artifactId}"/>
<echo>Moving conf data for webapp ${pom.artifactId} to ${webapp.build}.</echo>
<!-- copy conf data -->
<copy file="src/conf/hibernate-WEBAPP.cfg.xml"
tofile="${webapp.build}\WEB-INF\classes\hibernate.cfg.xml"/>
</preGoal>
Project.xml Resources SetupThe final step in configuring your project is to tell Maven to always copy your hibernate.hbm.xml file to the /target/classes directory so that Hibernate can find them. Just add to your project.xml in the build/resources section:
<resource>
<directory>src/conf</directory>
<targetPath>/</targetPath>
<includes>
<include>hibernate.hbm.xml</include>
</includes>
</resource>
Test Maven Configuration StepsNow, to test your configuration, go to the command line and run maven test. You should now have /target/classes/hibernate.hbm.xml and /target/test-classes/hibernate.cfg.xml. Also, any dependencies you didn't have will be downloaded from the Maven repository. Setting up HibernateTestCaseI have a generic HibernateTestCase that all my JUnit tests inherit from. This allows me to centralize all the Hibernate session opening logic away from my unit tests. Somewhat like using the Servlet Filter and ThreadLocal pattern described elsewhere. The HibernateTestCase looks like this:
package com.upstate.commons.test;
import java.net.URL;
import junit.framework.TestCase;
import net.sf.hibernate.Session;
import net.sf.hibernate.avalon.HibernateService;
import org.apache.avalon.excalibur.component.ExcaliburComponentManager;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
import org.apache.avalon.framework.context.DefaultContext;
import org.apache.log.Hierarchy;
import org.apache.log.Logger;
import org.apache.log.Priority;
/**
* Test that the need Hibernate
*
* @author epugh
*/
public class HibernateTestCase extends TestCase
{
/** Compenent Manager*/
protected static ExcaliburComponentManager ecm = new ExcaliburComponentManager();
static {
try
{
DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();
URL url = Thread.currentThread()
.getContextClassLoader()
.getResource("./test.avalonconf.xml");
System.out.println("Config File URL for HibernateTestCase: " + url);
Configuration sysConfig = builder.buildFromFile(url.getFile());
Logger logger = Hierarchy.getDefaultHierarchy().getLoggerFor("ecm");
logger.setPriority(Priority.getPriorityForName("INFO"));
ecm.setLogger(logger);
ecm.contextualize(new DefaultContext());
ecm.configure(sysConfig);
ecm.initialize();
}
catch (Exception e)
{
e.printStackTrace();
}
}
/** The hibernate servce used in the tests */
protected HibernateService hibernateService;
private Session session;
/**
*
* @param testName test name
*/
public HibernateTestCase(String testName)
{
super(testName);
}
/**
*
* @see junit.framework.TestCase#setUp()
*/
public void setUp() throws Exception
{
super.setUp();
hibernateService = (HibernateService) ecm.lookup(HibernateService.ROLE);
session = hibernateService.openSession();
}
/**
*
* @see junit.framework.TestCase#tearDown()
*/
public void tearDown() throws Exception
{
super.tearDown();
session.connection().close();
session.close();
}
/**
* @return
*/
public HibernateService getHibernateService()
{
return hibernateService;
}
/**
* @return
*/
public Session getSession()
{
return session;
}
}
The meat of this class is in the <static> block where we initialize our container. If you have problems with your hibernate.hbm.xml of hibernate.cfg.xml files, they will show up here. My First Test Case...In our first test, we will demonstrate loading a series of Cat objects. Obviously, change this to fit your own ORM! Test that the test works by running maven test and checking the output!
package com.upstate.inventory.om.persist;
import java.util.List;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.upstate.commons.test.HibernateTestCase;
/**
* FirstTest
*
*@author <a href="epugh@upstate.com">Eric Pugh</a>
*/
public class FirstTest extends HibernateTestCase
{
private static Log log = LogFactory.getLog(FirstTest.class);
/**
* Defines the testcase name for JUnit.
*
*@param name the testcase's name.
*/
public FirstTest(String name)
{
super(name);
}
/**
* Start the tests.
*
*@param args the arguments. Not used
*/
public static void main(String args[])
{
junit.awtui.TestRunner.main(new String[] { FirstTest.class.getName()});
}
/**
* Creates the test suite.
*
*@return a test suite (<code>TestSuite</code>) that includes all methods
* starting with "test"
*/
public static Test suite()
{
// All methods starting with "test" will be executed in the test suite.
return new TestSuite(FirstTest.class);
}
/**
* Verify we can connect to the database and retrieve a Cat.
* @throws Exception
*/
public void testHibernate() throws Exception
{
List litter = this.getSession().find("from Cat c");
assertTrue(litter.size() > 0);
}
}
Running FirstTest from EclipseNow, we did say that you would be able to develop and run your UnitTests from Eclipse, right? Now that the configuration is all set up, run a quick "maven eclipse" to update your Eclipse project with all the jars we have added. Open up your eclipse project and right click on the project and refresh it. Now, the one thing to remember is that Eclipse doesn't know how to setup the /target directory with the proper hibernate.cfg.xml file. Therefore, you must run maven test ONCE before you attempt to run your Junit Tests. Otherwise you will get a build error like "Missing required library: inventory/target/test-classes". Once you have run the test goal once, then you are already to start running the testcases from Eclipse. Just pick your testcase in Eclipse, and choose Run -> Run As -> Junit Test. You can also step through them using the debugger instead! Note: If you change your hibernate.hbm.xml you must go back to maven and run maven test again to setup /target directory properly! Going From HereCongratulations! You have now successfully written and run a test that can be used from Junit that is Maven enabled and Eclipse friendly! From here there are a couple things that can be done: 1) Look at the mevenide project at http://mevenide.sf.net. This provides a plugin for Eclipse for using Maven. This project allows you to run Maven goals from inside of Exclipse, and could be used to keep your /target dir set up properly. 2) Remove the getSession() call from you testcases... Following the ServiceLocator pattern talked about elsewhere, structure your JUnit testcases so they only know about a ServiceLocator inherited from HibernateTestCase, not an actual Hibernate Session. ReferencesThese are sites that gave me ideas about using Hibernate and testing them. The biggest thanks goes to Leandro Rodrigo Saad Cruz and the Xingu project: http://xingu.sf.net. Thanks for the ServiceLocator pattern also is due to the AppFuse package by Matt Raible: http://raibledesigns.com/wiki/Wiki.jsp?page=AppFuse For more information on Maven: http://maven.apache.org, and the Maven Eclipse plugin: http://maven.apache.org/reference/plugins/eclipse/index.html. For more information on MevenIDE: http://mevenide.sourceforge.net. FeedbackAny feedback on what you liked, didn't like, or maybe most importantly, found confusing on this tutorial can be sent to me at epugh@upstate.com. - Eric Pugh |
||||||||