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 surefire offers a variety of options to execute tests in parallel, allowing you to make best use of the hardware at your disposal. But forking in particular can also help keeping the memory requirements low.
circleci/config. yml file. The parallelism key specifies how many independent executors are set up to run the steps. To run a job's steps in parallel, set the parallelism key to a value greater than 1.
Maven Dependencies In a nutshell, Surefire provides two ways of executing tests in parallel: Multithreading inside a single JVM process. Forking multiple JVM processes.
This guide will help you get started with a Java application building with Maven on CircleCI. This is an example application showcasing how to run a Java app on CircleCI 2.1. This application uses the Spring PetClinic sample project.
Here we are using the maven orb, which simplifies building and testing Java projects using Maven. The maven/test command checks out the code, builds, tests, and uploads the test result. The parameters of this command can be customized.
If you want to step through it yourself, you can fork the project on GitHub and download it to your machine. Go to the Projects page in CircleCI and click the Follow Project button next to your forked project. Finally, delete everything in .circleci/config.yml.
Go to the Projects page in CircleCI and click the Follow Project button next to your forked project. Finally, delete everything in .circleci/config.yml. Nice!
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();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With