 |
To ignore unit test failures: "-Dmaven.test.failure.ignore=true"
Tell Maven to not run the unit tests at all: "-Dmaven.test.skip=true" |
 |
For the ten-minute tour of JUnit testing in Maven, build the "genapp" goal and experiment with the example unit tests AppTest.java and NaughtyTest.java --JS |
I have spent some time figuring out how to run junit tests for the new code I am writing for Sakai. My goal was to have maven run the unit tests as part of its build process. I will describe what I had to do to get this all working in the "osid" module.
1. First you will need to edit maven's project.xml file. In my case, I wanted to run unit tests under "osid/component" so I edited the file "osid/component/project.xml".
1. Make sure you have all of the dependencies listed that are required not only to compile the code but to also run the unit tests (i.e. you may have runtime dependencies that the compiler will not warn you about).
2. Under the "<build>" element, you will add a new element:
i. <unitTestSourceDirectory>src/test</unitTestSourceDirectory>
ii. This simply tells maven that it should compile your unit tests.
3. Next, you will add another new element under "<build>" called <unitTest>. I am attaching my project.xml for your reading pleasure. J
i. This element tells maven that you want it to run the unit tests that it compiled in the previous step.
ii. You will want to configure this element with some sub-elements.
iii. For starters, configure the <resources> element to copy all required files to the unit tests' classes directory (by default target/test-classes).
iv. Look at the example I have included. You will it does a couple of important things:
1. Copies components.xml from ${basedir}/src/webapp/WEB-INF into the root of the classpath. This will be used by another helper class which I will talk about later.
2. Copies all of my hibernate *.hbm.xml files from the source directory ${maven.src.dir}/java into the test-classes directory. These files must be available on the classpath for hibernate to work.
v. Next, you will tell maven which unit tests to run by using the "includes" and "excludes" elements.
1. See the example file. You will notice that I run all unit tests that end with **/*Test.java.
2. You will also notice that I exclude the unit test **/ApplicationContextBaseTest.java. I don't think you will have to do this in your modules. I will talk more about this class later.
4. That is pretty much it for project.xml.
1. Next, you will need to have a fully functional Spring bean factory (ApplicationContext) to run your unit tests.
1. Getting dependency injection to work for unit tests is a somewhat interesting problem. I assume until Spring and junit get together, TestCases will not be dependency injected. Until then, my solution models some examples I have found on the web.
2. I have my unit tests extend ApplicationContextBaseTest which itself extends TestCase. The sole purpose of this base class is to find all of the appropriate components.xml files and init the Spring bean factory with their bean definitions.
3. Unit tests that extend this base class can now get a handle to the bean factory and get references to managed beans.
4. You will see in the unit test that I have attached, that it has a couple of dependencies that need to be resolved:
private PersistentTypeManager typeManager; // dep inj
private IdManager idManager; // dep inj
5. In the constructor for the unit test, I simply go and ask the bean factory for these beans:
public TypeManagerTest(String name)
{
super(name);
typeManager = (PersistentTypeManager) getApplicationContext().getBean(
"org.sakaiproject.service.osid.type.PersistentTypeManager");
idManager = (IdManager) getApplicationContext().getBean(
"org.osid.id.IdManager");
}
6. My intent is to make ApplicationContextBaseTest a general purpose base class that any Sakai unit tests can extend. As it stands right now, this class is in the "osid" module and may need to be moved to a more appropriate place.
7. This class has some interesting behavior:
i. It will currently look for components.xml files in two locations:
1. Sakai-shared components.xml
a. It will try to find the "sakai-shared" components.xml by using an absolute path to the file.
b. To get this working in your local development environment, you must set the property "sakaiproject.basedir" to the physical location of the root of the sakai source tree.
c. This can be easily accomplished on the command line by using the java standard syntax. For example:
"maven -Dsakaiproject.basedir=/where/sakai/source/is/located"
d. If this property is not set, it will look in /java/projects/sakai by default. If you don't want to mess with the property, create a symlink for this path.
2. Local module's components.xml
a. It will load this bean definitions file from the classpath. Since our modifications to maven's project.xml copied the file to the root of our classpath, no additional configuration is necessary.
3. Troubleshooting
1. I have spent a number of unpleasant days banging my head against maven and searching Google. I hope this will help save some of you from the same terrible fate. J
2. Running unit tests
i. If you just type "maven" it will run the unit tests inline in the same process space. When your unit tests have problems, this default mode of operation will obscure almost ANY useful error information. Even "maven --debug" will produce very little useful information.
ii. To see the entire output from the junit runner, you must set a maven property to true:
"-Dmaven.junit.fork=true". This will cause junit to be run in a separate jvm with the kind of ouput one might expect. This has been an invaluable troubleshooting tool for me.
3. Ignoring unit tests
i. For those of you who might get build failures because unit tests are failing, there is a convenient way to tell maven to proceed with the build even though tests fail.
ii. Use this maven property to tell it to ignore unit test failures: "-Dmaven.test.failure.ignore=true".
iii. Perhaps you would rather not run the unit tests at all: "-Dmaven.test.skip=true".
That is pretty much it. I hope this helps clarify things a bit and let's see where this ApplicationContextBaseTest class should go
<?xml version="1.0" encoding="UTF-8"?>
<project>
<pomVersion>3</pomVersion>
<name>Sakai OSID Components</name>
<groupId>sakaiproject</groupId>
<id>sakai-osid-component</id>
<currentVersion>1.0</currentVersion>
<organization>
<name>Sakai Project</name>
<url>http: </organization>
<inceptionYear>2004</inceptionYear>
<dependencies>
<!-- Access to the sakai types -->
<dependency>
<groupId>sakaiproject</groupId>
<artifactId>sakai-osid-type</artifactId>
<version>1.0</version>
<type>jar</type>
</dependency>
<!-- to be able to access the Sakai utilitiy classes -->
<dependency>
<groupId>sakaiproject</groupId>
<artifactId>sakai-util</artifactId>
<version>1.0.rc2</version>
</dependency>
<dependency>
<groupId>sakaiproject</groupId>
<artifactId>sakai-component</artifactId>
<version>1.0.rc2</version>
</dependency>
<dependency>
<groupId>sakaiproject</groupId>
<artifactId>sakai-service</artifactId>
<version>1.0.rc2</version>
</dependency>
<dependency>
<groupId>OKI</groupId>
<artifactId>OkiOSID</artifactId>
<version>2.0</version>
<url>http: </dependency>
<dependency>
<groupId>commons-id</groupId>
<artifactId>commons-id</artifactId>
<version>0.1-dev</version>
<url>
http: </url>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>springframework</groupId>
<artifactId>spring-full</artifactId>
<version>1.0.1</version>
<url>http: </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>hibernate</groupId>
<artifactId>hibernate</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>jta</groupId>
<artifactId>jta</artifactId>
<version>1.0.1b</version>
<url>http: </dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-full</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>1.7.2.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/java</sourceDirectory>
<resources>
<resource>
<directory>${basedir}/src/bundle</directory>
<includes>
<include>**/*.properties</include>
</includes>
</resource>
<resource>
<directory>src/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<excludes>
<exclude>**/web.xml</exclude>
</excludes>
</resource>
</resources>
<unitTestSourceDirectory>src/test</unitTestSourceDirectory>
<unitTest>
<resources>
<resource>
<directory>${basedir}/src/webapp/WEB-INF</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
<excludes>
<exclude>**/web.xml</exclude>
</excludes>
</resource>
<resource>
<directory>${maven.src.dir}/java</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
<includes>
<include>**/*Test.java</include>
</includes>
<excludes>
<exclude>**/ApplicationContextBaseTest.java</exclude>
</excludes>
</unitTest>
</build>
</project>
As it turns out there is a Spring 1.2 class called AbstractTransactionalSpringContextTests that can help here. There are a few other classes that are related in the test package also that provide different but similar options. Basically, you need to:
1. extend AbstractTransactionalSpringContextTests instead of TestCase
2. create protected properties for your beans. example:
protected TestManager testManager = null;
3. create setter for your bean. example:
public void setTestManager(TestManager testManager) {
log.debug("setting test manager");
this.testManager = testManager;
}
4. In your constructor, call:
setPopulateProtectedVariables(true);
Now when your test case is run, you will have a single transaction that wraps each test*() method (quite helpful if you are using Hibernate) and spring will automagically do type based injection onto your test case. So this will look for a bean defined for class (TestManager) and use the setter on the test class to inject an instance of that bean. So the obvious problem is that if you have multiple beans configured for the same class as specified in your setter, Spring can't determine which it should use. Then you are back to using ctx.getBean().
So far this is making life easier, but the one snag is if I am getting a bean that is configured in spring to use a transaction proxy then I have issues. It will wrap the methods in transactions, which would be fine if the proxies detected they were part of a larger transaction and didn't try to call commit(). So I have a second spring configuration at the moment without the transaction proxy configuration. I'm sure there is a better way to handle that problem too and further streamline unit testing with Spring/Hibernate.