16

We're using JUnit 4 to test: we have classes that don't are a subclass of TestCase, and they have public methods annotated with @Test. We have one file with many @Test methods. It would be nice to be able to run a subset of them via Ant from the command line, in the style of this recipe for JUnit 3:

ant runtest -Dtest=MyTest -Dtests=testFoo,testBar

http://today.java.net/pub/a/today/2003/09/12/individual-test-cases.html

I've been trying to think of ways to achieve this with Java reflection, etc. Since there doesn't seem to be any way to "hide" @Test methods or remove their annotations at runtime, the only option seems to be using the ClassLoader's defineClass method, which seems quite difficult.

P.S. The Right Thing in this situation would be to split up the file, but are there alternatives?

Thanks for your time.

guerda
  • 23,388
  • 27
  • 97
  • 146
George
  • 343
  • 1
  • 2
  • 8

4 Answers4

16

Since JUnit 4.12 we have @Category annotations to solve just that problem.

Johannes Rabauer
  • 330
  • 3
  • 15
Robert Jack Will
  • 10,333
  • 1
  • 21
  • 29
  • 2
    @Category is pretty cool, but only if you already build suites. As the article mentions at the end that doesn't fit into everyone's testing environment. They're definitely worth pointing out though, as for some lucky folks this will completely solve their problem. – Josh Gagnon Jan 20 '12 at 15:17
  • 2
    And since Surefire 2.11 (or so) we can use -Dgroups to select tests by category without building suites. I was happy to delete a lot of our test suites last week thanks to this improvement! (The parameter of Maven Surefire plugin is only documented for TestNG, but since 2.11 also works for JUnit.) – Robert Jack Will Jan 20 '12 at 22:53
  • This is so inconvenient when compared to TestNg test suite xml file. Why does Junit not allow us to select methods without annotating the test methods ? Has this inconvenience been fixed in Junit 5? – MasterJoe Apr 14 '20 at 18:43
11

guerda's solution is good. Here's what I ended up doing (it's a mix of Luke Francl's recipe, which I linked before, and some other stuff I saw on the net):

import org.junit.runner.manipulation.Filter;
import org.junit.runner.Description;

public final class AntCLFilter extends Filter {
    private static final String TEST_CASES = "tests";
    private static final String ANT_PROPERTY = "${tests}";
    private static final String DELIMITER = "\\,";
    private String[] testCaseNames;

    public AntCLFilter() {
        super();
        if (hasTestCases()) testCaseNames = getTestCaseNames();
    }

    public String describe() {
        return "Filters out all tests not explicitly named in a comma-delimited list in the system property 'tests'.";
    }

    public boolean shouldRun(Description d) {
        String displayName = d.getDisplayName();
        // cut off the method name:
        String testName = displayName.substring(0, displayName.indexOf('('));
        if (testCaseNames == null) return true;

        for (int i = 0; i < testCaseNames.length; i++)
            if (testName.equals(testCaseNames[i]))
                return true;
        return false;
    }

    /**
     * Check to see if the test cases property is set. Ignores Ant's
     * default setting for the property (or null to be on the safe side).
     **/
    public static boolean hasTestCases() {
        return
            System.getProperty( TEST_CASES ) == null ||
            System.getProperty( TEST_CASES ).equals( ANT_PROPERTY ) ?
            false : true;
    }

    /**
     * Create a List of String names of test cases specified in the
     * JVM property in comma-separated format.
     *
     * @return a List of String test case names
     *
     * @throws NullPointerException if the TEST_CASES property
     * isn't set
     **/
    private static String[] getTestCaseNames() {

        if ( System.getProperty( TEST_CASES ) == null ) {
            throw new NullPointerException( "Test case property is not set" );
        }

        String testCases = System.getProperty( TEST_CASES );
        String[] cases = testCases.split(DELIMITER);

        return cases;
    }
}

import org.junit.internal.runners.*;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;

public class FilteredRunner extends TestClassRunner {

    public FilteredRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
        Filter f = new AntCLFilter();
        try {
            f.apply(this);
        } catch (NoTestsRemainException ex) {
            throw new RuntimeException(ex);
        }
    }
}

Then I annotated my test class with:

@RunWith(FilteredRunner.class)
public class MyTest {

and put the following in my ant buildfile:

<target name="runtest"
        description="Runs the test you specify on the command line with -Dtest="
        depends="compile, ensure-test-name">
    <junit printsummary="withOutAndErr" fork="yes">
        <sysproperty key="tests" value="${tests}" />
        <classpath refid="classpath" />
        <formatter type="plain" usefile="false" />
        <batchtest>
            <fileset dir="${src}">
                <include name="**/${test}.java" />
            </fileset>
        </batchtest>
    </junit>
</target>

the key line there being the sysproperty tag.

And now I can run

ant runtest -Dtest=MyTest -Dtests=testFoo,testBar

as desired. This works with JUnit 4.1 --- in 4.4, subclass from JUnit4ClassRunner, and in 4.5 and later, subclass from BlockJUnit4ClassRunner.

LPL
  • 16,827
  • 6
  • 51
  • 95
George
  • 343
  • 1
  • 2
  • 8
  • OK, that's much more elegant than my solution :) – guerda Aug 06 '09 at 06:34
  • I've been struggling with this same (or at least very similar) problem for a few days and there's one thing that keeps bothering me. What if you want to use your FilteredRunner with Powermock, which also requires it's own @RunWith(PowermockRunner.class) annotation? – Josh Gagnon Jan 20 '12 at 15:26
  • Great answer, but outdated now. I think BlockJUnit4ClassRunner needs to be used instead of TestClassRunner – Illidanek Jun 03 '14 at 16:23
8

Create your own TestClassMethodsRunner (it's not documentated or I don't find it now).
A TestClassMethodsRunner executes all TestCases and you can set up a filtered TestClassMethodsRunner.

All you have to do is override the TestMethodRunner createMethodRunner(Object, Method, RunNotifier) method. This is a simple an hacky solution:

public class FilteredTestRunner extends TestClassMethodsRunner {

    public FilteredTestRunner(Class<?> aClass) {
        super(aClass);
    }

    @Override
    protected TestMethodRunner createMethodRunner(Object aTest, Method aMethod, RunNotifier aNotifier) {
        if (aTest.getClass().getName().contains("NOT")) {
            return new TestMethodRunner(aTest, aMethod, aNotifier, null) {
                @Override
                public void run() {
                    //do nothing with this test.
                }
            };
        } else {
            return super.createMethodRunner(aTest, aMethod, aNotifier);
        }
    }

}

With this TestRunner, you execute all Tests that don't contain the string "NOT". Others will be ignored :) Just add the @RunWith annotation with your TestRunner class to your test.

@RunWith(FilteredTestRunner.class)
public class ThisTestsWillNOTBeExecuted {
   //No test is executed.
}

@RunWith(FilteredTestRunner.class)
public class ThisTestsWillBeExecuted {
   //All tests are executed.
}

In the createMethodRunner method you can check the current test against a list of tests that must be executed or introduce new criterias.

Good luck with this!

Hints for a nicer solution are appreciated!

Peter Wippermann
  • 4,125
  • 5
  • 35
  • 48
guerda
  • 23,388
  • 27
  • 97
  • 146
  • This is so inconvenient when compared to TestNg test suite xml file. Why does Junit not allow us to select methods without annotating the test methods ? Has this inconvenience been fixed in Junit 5? – MasterJoe Apr 14 '20 at 18:44
1

There is a simpler way for the common case where you need to run only one test method, without having to go through the hassle of creating a custom Runner or Filter:

public class MyTestClass {

  public static void main(final String[] args) throws Exception {
    final JUnitCore junit = new JUnitCore();
    final String singleTest = // Get the name of test from somewhere (environment, system property, whatever you want).
    final Request req;
    if (singleTest != null) {
      req = Request.method(MyTestClass.class, singleTest);
    } else {
      req = Request.aClass(MyTestClass.class);
    }
    final Result result = junit.run(req);
    // Check result.getFailures etc.
    if (!result.wasSuccessful()) {
      System.exit(1);
    }
  }

  // Your @Test methods here.

}
tsuna
  • 1,836
  • 14
  • 21