We’re going to create a new JUnit test runner which will be a minimal runner, i.e. has the barest essentials to run some tests. It should be noted that JUnit includes an abstract class ParentRunner which actually gives us a better starting point, but I wanted to demonstrate a starting point for a test runner which might no adhere to the style used by JUnit.
Our test runner should extend the org.junit.runner.Runner and will contain two methods from the abstract Runner class and a public constructor is required which takes the single argument of type Class, here’s the code
import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runner.notification.RunNotifier; public class MinimalRunner extends Runner { public MinimalRunner(Class testClass) { } public Description getDescription() { return null; } public void run(RunNotifier runNotifier) { } }
we’ll also need to add the dependency
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>
Before we move onto developing this into something more useful, to use our test runner on a Test class, we need to add the RunWith annotation to the class declaration, for example
import org.junit.runner.RunWith; @RunWith(MinimalRunner.class) public class MyTest { }
Okay, back to the test runner. The getDescription method should return a description which ultimately makes up the tree we’d see when running our unit tests, so we’ll be wanting to return a parent/child relationship of descriptions where the parent is the test class name and it’s children are those methods marked with the Test annotation (we’ll assume children but no deeper, i.e. no grandchildren etc.).
Spoiler alert, we will be needing the Description objects again later so let’s cache them in readiness.
public class MinimalRunner extends Runner { private Class testClass; private HashMap<Method, Description> methodDescriptions; public MinimalRunner(Class testClass) { this.testClass = testClass; methodDescriptions = new HashMap<>(); } public Description getDescription() { Description description = Description.createSuiteDescription( testClass.getName(), testClass.getAnnotations()); for(Method method : testClass.getMethods()) { Annotation annotation = method.getAnnotation(Test.class); if(annotation != null) { Description methodDescription = Description.createTestDescription( testClass, method.getName(), annotation); description.addChild(methodDescription); methodDescriptions.put(method, methodDescription); } } return description; } public void run(RunNotifier runNotifier) { } }
In the above code we create the parent (or suite) description first and then locate all methods with the @Test annotation and create test descriptions for them. These are added to the parent description and along with the Method, to our cached methodDescriptions.
Note: that we’ve not written code to handle @Before, @After or @Ignore annotations, just to keep things simple.
Obviously we’ll need to add the following imports also to the above code
import org.junit.Test; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.HashMap; // also need these two for the next bit of code import org.junit.AssumptionViolatedException; import org.junit.runner.notification.Failure;
Next up we need to actually run the tests and as you’ve probably worked out, this is where the run method comes in. There’s nothing particularly special here, we’re just going to run on a single thread through each method. Had we been handling @Before and @After then these methods would be called prior to the code in the following code’s forEach loop (but we’re keep this simple).
public void run(RunNotifier runNotifier) { try { Object instance = testClass.newInstance(); methodDescriptions.forEach((method, description) -> { try { runNotifier.fireTestStarted(description); method.invoke(instance); runNotifier.fireTestFinished(description); } catch(AssumptionViolatedException e) { Failure failure = new Failure(description, e.getCause()); runNotifier.fireTestAssumptionFailed(failure); } catch(Throwable e) { Failure failure = new Failure(description, e.getCause()); runNotifier.fireTestFailure(failure); } finally { runNotifier.fireTestFinished(description); } }); } catch(Exception e) { e.printStackTrace(); } }
In the code above we simply create an instance of the test class the loop through our previous cached methods invoking the @Test methods. The calls on the runNotifier object tell JUnit (and hence UI’s such as the IntelliJ test UI) which test has started running and whether it succeeded or failed. In the case of failure, the use of getCause() was added because otherwise (at least in my sample project) the exception showed information about the test runner code itself, which was superfluous to the actual test failure.
I’ve not added support for filtering or sortable capabilities within our code, to do this our MinimalRunner would also implement the Filterable interface for filtering and Sortable for sorting (within the org.junit.runner.manipulation package).
I’m not going to bother implementing this interface in this post as the IDE I use for Java (IntelliJ) handles this stuff for me anyway.
Code on GitHub
Code’s available on GitHub.