I have a Java project that uses Maven and the maven-surefire-plugin
to run JUnit 4 tests. I'm building with CircleCI. How can I enable parallelism so that my test suite runs faster?
I want to use the CircleCI parallelism, not Surefire fork and parallel execution options.
The maven-surefire-plugin supports its doesn't support parallelism, at least not in isolated fashion CircleCI supports (separate nodes for each test execution).
However, you can manually enable CircleCI-style parallelism using two methods:
-Dtest
parameter.TestRule
Create a bin
directory in your project, if you don't already have one.
In bin
, create a shell script in your project called test.sh
, with the following contents
#!/bin/bash
NODE_TOTAL=${CIRCLE_NODE_TOTAL:-1}
NODE_INDEX=${CIRCLE_NODE_INDEX:-0}
i=0
tests=()
for file in $(find ./src/test/java -name "*Test.java" | sort)
do
if [ $(($i % ${NODE_TOTAL})) -eq ${NODE_INDEX} ]
then
test=`basename $file | sed -e "s/.java//"`
tests+="${test},"
fi
((i++))
done
mvn -Dtest=${tests} test
This script will search your src/test/java
directory for all files ending in Test.java
, and add them to the -Dtest
parameter as a comma separated list, then call maven.
To enable your new test script, put the following in your circle.yml
file:
test:
override:
- ./bin/test.sh:
parallel: true
Things to note:
-Dtest
parameter exceeds the maximum length of the Linux command line.You can use a custom TestRule to do something similar to the above in Java code. This has the advantage of less CircleCI customized-configuration, but imposes some assumptions about CircleCI on your Java framework.
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assume;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
@Slf4j
final class CircleCiParallelRule implements TestRule {
@Override
public Statement apply(Statement statement, Description description) {
boolean runTest = true;
final String tName = description.getClassName() + "#" + description.getMethodName();
final String numNodes = System.getenv("CIRCLE_NODE_TOTAL");
final String curNode = System.getenv("CIRCLE_NODE_INDEX");
if (StringUtils.isBlank(numNodes) || StringUtils.isBlank(curNode)) {
log.trace("Running locally, so skipping");
} else {
final int hashCode = Math.abs(tName.hashCode());
int nodeToRunOn = hashCode % Integer.parseInt(numNodes);
final int curNodeInt = Integer.parseInt(curNode);
runTest = nodeToRunOn == curNodeInt;
log.trace("currentNode: " + curNodeInt + ", targetNode: " + nodeToRunOn + ", runTest: " + runTest);
if (!runTest) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Assume.assumeTrue("Skipping test, currentNode: " + curNode + ", targetNode: " + nodeToRunOn, false);
}
};
}
}
return statement;
}
}
(Note I am using Project Lombok (log instantiation) and Apache Commons-Lang (for StringUtils) in the above code, but these can easily be eliminated if necessary.
To enable this, in your test baseclass you can do this to balance on a test-by-test basis:
// This will load-balance across multiple CircleCI nodes
@Rule public CircleCiParallelRule className = new CircleCiParallelRule();
Or if you want to balance class-by-class, you can do this:
// This will load-balance across multiple CircleCI nodes
@ClassRule public CircleCiParallelRule className = new CircleCiParallelRule();