You are reading a translation of an old blog post published on my previous blog in French.
Never in the field of software development have so many owed so much to so few lines of code
— Gerard Meszaros, author of book XUnit Test Patterns
According to a study of 2013, JUnit is the most used library in Java (tied with Slf4j). We can trace the roots of the framework to a paper written by Kent Beck in 1989. The Smalltalk version will be published in 1994 by the same author that will be at the origin of the Java version too (with Eric Gamma). If you want to know more about the history of automated testing, I recommend these two excellent articles: Ten Years Of Test Driven Development and A Brief History of Test Frameworks.
Now, let’s check the code. We are going to rewrite a minimal version of JUnit from scratch, trying to stay as close as possible to the original code, even if some compromises will be necessary to keep this article short. The name of classes and methods will follow the same naming as the official JUnit library.
JUnit is published under the Eclipse license. The code presented here has been simplified for obvious reasons and must not be used outside this learning context. This article is based on version 4.11 of JUnit.
A First Example
Here is a small suite of basic tests:
When run with JUnit, these tests fail on the last test method. The goal is now to rerun the same tests, but without depending on the JUnit library.
Let’s Go!
If we ignore the support of our favorite IDE, the easiest way to launch JUnit on a given class is using the following line:
We are going to start our implementation with this class. So, we start by removing all import statements and create new versions of these classes (Result and JUnitCode).
The class Result groups all information that will be useful to display the result of single test execution, either “Green” or “Red.” We logically find fields to keep track of the number of executed tests and the number of failures:
For every test failure, we need to save the name of the failing test and the caught exception. It’s the job of the class Failure:
The class Description describes a single test, including the name, the annotation (@Test with the possible expected exception class), the test suite to which it belongs, etc. For our basic implementation, we will only represent the name but still keep the abstraction represented by Description.
We are done with the class Result. Now, we have to implement the second class JUnitCore, which is essentially a facade to other classes defined in the JUnit library. Here is the implementation showing the main abstractions we are going to implement just after.
The method run exposes some details for the following of this article. The method defines a single parameter of type Runner, the main class of JUnit responsible for executing all tests and report the progression through various events (starting execution, failure, completion, …). There are many supported implementations of Runner, for example, tests written using the JUnit 3 syntax, parameterized tests, theories, etc. It is also possible to implement new runners as did Spring or Mockito by relying on the annotation @RunWith. All runners satisfy the following interface:
How Runners report the result of tests execution?
The class RunNotifier implements the Observer pattern. For every possible event, the class RunNotifier offers a notification method called by the Runner instance (ex: fireTestStarted). Each registered listener is then notified and can react in consequence. In our case, the object Result listens for these events to build the final result step by step.
Here is the implementation of the class RunNotifier:
Where RunListener is defined like this:
For the code to compile again, we need to go back to the class Result to implement the missing method result.createListener():
1
We listen to events triggered by the runner.
2
We memorize every test execution.
3
We save every failure.
The Heart of JUnit: Runner
We are getting closer to the final step—the implementation of the class Runner. The official implementation is the class BlockJUnit4ClassRunner, which extends the class ParentRunner to inherit most of the logic. Both classes count more than 1000 lines of code. We will make some compromises.
Let’s get started with a first version supporting only the annotation @Test:
1
We use a utility class to find the test methods to execute.
2
We notify about the progression after every step.
3
We create a new instance of our test class before every test method execution (see explanations below).
Let’s explain these points a little more.
The class Runner uses the class TestIntrospector to extract the test methods, making sure to ignore methods with the annotation @Ignore. Here is the implementation of this utility class (inspired from Junit 4.1):
Do tests are executed in a predictable order?
The truth is that the test methods are well ordered but not by their position in our code. JUnit uses the method java.lang.Class.getDeclaredMethods() to extract the annotated methods. The Javadoc is explicit on this point: “The elements in the array returned are not sorted and are not in any particular order.”
In practice, the order was the order of methods like defined in our code, but it changes since Java 7. To ensure tests are reproducible, JUnit imposes a specific order by default. It is implemented by org.junit.internal.MethodSorter.DEFAULT, which is an instance of Comparator:
The code relies on the hashCode defined by the method String. The final order is not alphabetic, nor the one in our source code, but is still predictable and repeatable, which is essential.
The other point concerns the creation of a new instance before each execution of a test method. The motivation is described in a post by Martin Fowler and is better illustrated through an example:
With JUnit, both tests are successful, independently of their execution order. A new instance creation of our test class guarantees that every test method works on its list, without being affected by previous tests. This behavior has not been implemented by NUnit, the .Net version of JUnit, probably due to misunderstanding, and now, it’s impossible to revert without causing regression in existing test suites.
Congratulations!
Less than 300 lines of code have been necessary to make our tests run again. The result is identical: we still have the same number of passing tests and only one failing test.
Here is the final version also supporting the annotations @Before and @After:
What about IDE support?
Let’s consider Eclipse and its plugin Java Development Tools (JDT) that implement the JUnit support. This plugin reuses the JUnit library and exploits the extension points supported by the class RunNotifier. The plugin implements a custom listener that will still consolidate the test results but also updates the JUnit view in the Eclipse UI.
Try for yourself!
We took a few shortcuts in our implementation:
The determination of the runner to use is more complex than a simple class instanciation. To know more: org.junit.runner.JUnitCore, org.junit.runner.Computer, org.junit.runner.Request.
Our Runner implementation doesn’t reflect the complexity found in the real runners, which must, for example, support many other annotations such as @BeforeClass and @AfterClass but also assumptions, categories, … Why not have a look at the JUnit source code to discover how these features have been implemented.
The tests can be run in parallel. The actual implementations of the classes introduced in this article are thread-safe. When some objects could not be made immutable, we have to use concurrency building blocks defined in the package java.util.concurrent: AtomicInteger, CopyOnWriteArrayList, Executors, …
To Remember
Only a few hundred lines of code can have a major impact in the software development landscape.
The design of well-defined abstractions (Result, Failure, Description) ensures the code is simple to grasp and extend.
The use of design patterns brings a lot of flexibility required for the library to be used in many contexts.
About the author
Julien Sobczak works as a software developer for Scaleway, a French cloud provider. He is a passionate reader who likes to see the world differently to measure the extent of his ignorance. His main areas of interest are productivity (doing less and better), human potential, and everything that contributes in being a better person (including a better dad and a better developer).