1

I have a custom test runner I've made to run a portion of my tests so I can distribute the tests on different jenkins nodes. If all my integration tests were run it would take an hour. So I have 3 servers running 1/3rd of the tests and this only takes 20 minutes total. Here's how my suite looks:

import junit.framework.JUnit4TestAdapter;
import junit.framework.TestSuite;
import org.junit.Ignore;
import org.junit.extensions.cpsuite.ClassesFinder;
import org.junit.extensions.cpsuite.ClasspathFinderFactory;
import org.junit.extensions.cpsuite.SuiteType;
import org.junit.runner.RunWith;
import org.junit.runners.AllTests;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

@RunWith(AllTests.class)
public class DistributedIntegrationTestRunner {

    private static Logger log = LoggerFactory.getLogger(DistributedIntegrationTestRunner.class);

    public static TestSuite suite() {
        TestSuite suite = new TestSuite();

        ClassesFinder classesFinder = new ClasspathFinderFactory().create(true,
                new String[]{".*IntegrationTest.*"},
                new SuiteType[]{SuiteType.TEST_CLASSES},
                new Class[]{Object.class},
                new Class[]{},
                "java.class.path");

        int nodeNumber = systemPropertyInteger("node.number", "0");
        int totalNodes = systemPropertyInteger("total.nodes", "1");

        List<Class<?>> allTestsSorted = getAllTestsSorted(classesFinder);
        allTestsSorted = filterIgnoredTests(allTestsSorted);
        List<Class<?>> myTests = getMyTests(allTestsSorted, nodeNumber, totalNodes);
        log.info("There are " + allTestsSorted.size() + " tests to choose from and I'm going to run " + myTests.size() + " of them.");
        for (Class<?> myTest : myTests) {
            log.info("I will run " + myTest.getName());
            suite.addTest(new JUnit4TestAdapter(myTest));
        }

        return suite;
    }

    private static int systemPropertyInteger(String propertyKey, String defaultValue) {
        String slaveNumberString = System.getProperty(propertyKey, defaultValue);
        return Integer.parseInt(slaveNumberString);
    }

    private static List<Class<?>> filterIgnoredTests(List<Class<?>> allTestsSorted) {
        ArrayList<Class<?>> filteredTests = new ArrayList<Class<?>>();
        for (Class<?> aTest : allTestsSorted) {
            if (aTest.getAnnotation(Ignore.class) == null) {
                filteredTests.add(aTest);
            }
        }
        return filteredTests;
    }

    /*
    TODO: make this algorithm less naive.  Sort each test by run duration as described here: http://blog.tradeshift.com/just-add-servers/
     */
    private static List<Class<?>> getAllTestsSorted(ClassesFinder classesFinder) {
        List<Class<?>> allTests = classesFinder.find();
        Collections.sort(allTests, new Comparator<Class<?>>() {
            @Override
            public int compare(Class<?> o1, Class<?> o2) {
                return o1.getSimpleName().compareTo(o2.getSimpleName());
            }
        });
        return allTests;
    }

    private static List<Class<?>> getMyTests(List<Class<?>> allTests, int nodeNumber, int totalNodes) {
        List<Class<?>> myTests = new ArrayList<Class<?>>();

        for (int i = 0; i < allTests.size(); i++) {
            Class<?> thisTest = allTests.get(i);
            if (i % totalNodes == nodeNumber) {
                myTests.add(thisTest);
            }
        }

        return myTests;
    }
}

This sort of works, except when I try to use multiple DistibutedIntegrationTestRunners in different modules and run them all at once, "things don't work". Normally I don't post on SO with a complaint that vague, but it's very difficult to figure out more because this code does not give much feedback. This is the only logging I get:

        log.info("There are " + allTestsSorted.size() + " tests to choose from and I'm going to run " + myTests.size() + " of them.");
        for (Class<?> myTest : myTests) {
            log.info("I will run " + myTest.getName());

This happens before any of the tests are run. It would be very useful if I can get more logging than this. For example, it'd be very useful if I could print out "I'm about to run FooTest" right before that test runs. Is there any way to do this?

I've skimmed through the source code and have seen that there's a field named private final RunNotifier fNotifier; in the org.junit.internal.runners.JUnit38ClassRunner, but I'm not sure how to hook into that or if I'd be going in the right direction.

I'm using JUnit 4.10, but I can upgrade if necessary.

Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356
  • Run before - it sounds like job for aspects.. – Leos Literak Apr 22 '14 at 17:37
  • @LeosLiterak I don't understand what you're trying to tell me. – Daniel Kaplan Apr 22 '14 at 18:32
  • Could aspectj help you with this task? – Leos Literak Apr 22 '14 at 21:14
  • @LeosLiterak not in a way that I can think of that would be the right tool for the job. – Daniel Kaplan Apr 22 '14 at 21:48
  • You have tried the [Parallel Test Executor Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Parallel+Test+Executor+Plugin)? – cheffe Apr 23 '14 at 07:09
  • @cheffe Tried? No. It doesn't seem to solve this problem. "Given another job that runs tests, executes multiple runs of it concurrently by interleaving tests, achieving the parallel test execution semantics." I am interpreting this to mean, "run multiple tests in parallel". I don't want that. I want to run tests sequentially, but on different nodes. I want test [sharding](http://en.wikipedia.org/wiki/Shard_\(database_architecture\)). – Daniel Kaplan Apr 23 '14 at 17:58

1 Answers1

0

This looks like an XY problem to me.

Log files are not for testing. They are for monitoring an debugging. Once you have a failing test case, you need to switch from thinking about testing to thinking about debugging. That means running the failing test cases individually, perhaps using a debugger.

If your tests fail with unhelpful failure messages, that suggests you have poor low-level test coverage, or the diagnostic messages from your test cases are poor.

Raedwald
  • 46,613
  • 43
  • 151
  • 237
  • They *only* fail when run by the test runner. These tests are run from the client to the server. The server doesn't give me the stack trace for security reasons. – Daniel Kaplan Apr 24 '14 at 17:00