Because best way to understand is explaining.

Sunday, June 19, 2011

Enhancing JUnit Suites with Categories to eliminate test-suite dependency

With JUnit 3 the extra step of creating and maintaining test suites on top of unit tests never felt right. With JUnit 4 test suites became just simple annotation boilerplate:
@RunWith(Suite.class)
@Suite.SuiteClasses({
  SomeTests.class,
  SomeOtherTests.class,
  SomethingElseTests.class
})
public class SomeTestSuite {
  // this class is just a place holder for test suite annotations above
}

Next logical step would be aggregation of tests based on the information contained in tests themselves. Instead of specifying concrete tests, test suites would contain qualifiers (using annotations) to match. Those tests having matching qualifiers are included in a suite, those without are not. For example I have a qualifier JMSTest that is obviously assigned to tests that use a JMS provider.

Thus, my test suite classes will become completely decoupled from the tests and vice verse. This is actually even better than it sounds: even though there is no language dependency from tests to test suites in JUnit, functional dependency does exist: tests will not run if they are not bound to one or more test suites.

With introduction of JUnit Categories we received a qualifier support that is almost what we need:

public interface JMSTest {}


@RunWith(Categories.class)
@Categories.IncludeCategory(JMSTest.class)
@Suite.SuiteClasses({
  SomeTestSuite.class,
})
public class JMSTestSuite {}


@Category(IntegrationTest.class)
public class SomeTests {
   ....
}


@Category(JMSTest.class) 
Public class SomeOtherTests{
   ....
}


@Category(DatabaseTest.class) 
Public class SomethingElseTests{
   ....
}

This setup will run SomeOtherTests marked with JMSTest category when running JMSTestSuite. But categories didn’t eliminate the dependency from suites to tests – we still depend on test suite SomeTestSuite that explicitly references our tests.

Now, imagine you can define suite AllProjectTests that always contains all tests from the project. Then you can define category-based test suites like JMSTestSuite above and never care about maintaining your test suites again. Fortunately, we can use open source project to do just that - ClasspathSuite:

import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
@RunWith(ClasspathSuite.class)
public class AllProjectTests {}


@RunWith(Categories.class)
@Categories.IncludeCategory(JMSTest.class)
@Suite.SuiteClasses({
  AllProjectTests.class,
})
public class JMSTestSuite {}

To summarize, if you want to define a test suite that runs all database-based tests:
1. Define AllProjectTests suite using ClasspathSuite.
2. Define JUnit category DatabaseTest.
3. Define corresponding suite DatabaseTestSuite to run all tests marked with category DatabaseTest.
4. More complex category-based suites are easy to construct with JUnit category support.

2 comments:

Spina said...

I'm trying to get this working and something is wrong... I keep getting the following trace:


java.lang.Exception: Custom runner class Categories should have a public constructor with signature Categories(Class testClass)
at org.junit.internal.runners.InitializationError.(InitializationError.java:19)
at org.junit.internal.requests.ClassRequest.buildRunner(ClassRequest.java:36)
at org.junit.internal.requests.ClassRequest.getRunner(ClassRequest.java:28)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.(JUnit4TestReference.java:32)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestClassReference.(JUnit4TestClassReference.java:25)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createTest(JUnit4TestLoader.java:41)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.loadTests(JUnit4TestLoader.java:31)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:452)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


Here is my code:


@RunWith(Categories.class)
@IncludeCategory(BotTest.class)
@Suite.SuiteClasses({
AllTests.class
})
public class BotTestSuite { }

interface BotTest { }

@RunWith(ClasspathSuite.class)
public class AllTests { }

Am I doing what you intended?

Gregory Kanevsky said...

Do you have at least single test marked with category BotTest?

@Category({BotTest.class})
public class SomeOfMyTests {

@Test public void testSomething() {
....
}

}