Search code examples
javajunitjunit4maven-surefire-plugincircleci

How can I take advantage of CircleCI parallelism in my Java / Maven / Surefire project?


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.


Solution

  • 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:

    1. Use a shell script to select the tests to run per-node, and then using the -Dtest parameter.
    2. Custom JUnit 4 TestRule

    Shell Script

    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:

    1. You may need to customize this script if your filenames don't follow this naming convention, your files are located somewhere else, or you need to run a different lifecycle phase.
    2. If you have a very many number of tests, you may find your -Dtest parameter exceeds the maximum length of the Linux command line.

    Junit4 TestRule

    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();