Wednesday, February 11, 2009

At work the time to execute all of our our growing set of JUnit tests has slowly crept up. In 2005 it took 25 minutes to build and test everything. Now it takes 65 minutes, most of which is spent running tests. So we decided to run the longest-running tests (defined at test classes which take > 30 seconds to execute) only once a week. So by default when one of these long-running test is run as part of a group of tests, it doesn't execute; to cause those long-running tests to run within Ant, you'd set a property to indicate this. However, we still wanted to run those tests in, say, Eclipse, because presumably someone who tries to run the test in Eclipse really wants it to run; one way to make this work is to check whether the value of the TERM environment variable is set to "dumb."

I created an annotation called IntegrationTest, which can only be applied to types, not methods. A nice enhancement could be to apply the annotation to methods, too, so that only certain long-running test methods are excluded or included as desired.

To mark test classes as long-running and I annotated each one like so:

@IntegrationTest
@RunWith(JUnit4ClassRunner.class)
The code for JUnit4ClassRunner is (approximately):
package com.example.build;

import java.util.logging.Level;
import java.util.logging.Logger;

import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;

public final class JUnit4ClassRunner extends BlockJUnit4ClassRunner {
private static final String TEST_TYPE_KEY = "test.type";

private static final Logger log =
Logger.getLogger(JUnit4ClassRunner.class.getName());

public JUnit4ClassRunner(Class<?> klass) throws InitializationError {
super(klass);
}

@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
if (shouldRun(method)) {
super.runChild(method, notifier);
} else {
log.info("Not running integration test: " + method.getMethod());
Description desc = Description.createTestDescription(
method.getMethod().getDeclaringClass(), "Integration test");
notifier.fireTestIgnored(desc);
}
}

protected boolean shouldRun(FrameworkMethod method) {
final TestType type = getTestType();

switch (type) {
case UNIT_ONLY:
return !isIntegrationTest(method);
case INTEGRATION_ONLY:
return isIntegrationTest(method);
case ALL:
return true;
default:
return true;
}
}

protected TestType getTestType() {
if (isIntegrationOnly()) {
return TestType.INTEGRATION_ONLY;
} else if (isEclipse() || isAllTests()) {
return TestType.ALL;
} else {
return TestType.UNIT_ONLY;
}
}

/**
* This is a bit of a hack, but seems to work. Eclipse sets the TERM
* environment variable to "dumb"; it's quite unlikely that a user would
* be running tests from the command line with their TERM set to "dumb."
*
* @return true if TERM is "dumb"
*/
protected boolean isEclipse() {
String value = System.getenv("TERM") == null ?
"" : System.getenv("TERM");
return value.equals("dumb");
}

protected boolean isIntegrationOnly() {
String value = System.getProperty(TEST_TYPE_KEY, "");
return value.equals("integration");
}

protected boolean isAllTests() {
String value = System.getProperty(TEST_TYPE_KEY, "");
return value.equals("all");
}

/**
* @param method The FrameworkMethod of some JUnit test method (i.e.
* a method annotated with @Test)
* @return true if the class in which the method is declared is annotated
* with @IntegrationTest
*/
protected boolean isIntegrationTest(FrameworkMethod method) {
Class<?> klass = method.getMethod().getDeclaringClass();
return klass.isAnnotationPresent(IntegrationTest.class);
}
}

No comments: