Factor Test Code the JUnit 5 Way (That Is, Without Inheritance)

Bruno Berstel-Da Silva
Decision Optimization Center
7 min readSep 18, 2020
Photo by La-Rel Easter on Unsplash

You write Java code, hence you write JUnit tests. Easy. But you write Java code with Spring, and testing is now getting complicated. However, you can work it out: you’re not the only one, there are good resources out there. It takes a bit of an effort, but you succeed in writing JUnit tests for your Spring services. Say you want to package this effort, to avoid duplicate code, and for next time. So you write a base class that all your Spring-based test classes inherit from. In this class, you perform the required Spring configuration, you install the JUnit hooks to execute before each test, and you add a few utility methods, too. And then your colleague tells you that inheritance in JUnit test classes is a code smell…

Side note: To be fair, Spring is not the cause of the complexity here. The culprit would rather be dependency injection, that is, the ability to define beans (components, controllers, services, repositories, etc.) declaratively, and have them reference each other without having to manage their instantiation and wiring. In this picture, Spring is just one way (and admittedly a good one) to implement dependency injection, and the efforts to configure that would be required with other dependency injection frameworks, too.

Writing Spring-based Tests With JUnit 5

Imagine that you are responsible for a Spring-based µ-service that manages a stock of books. The Spring components in your µ-service can roughly be divided into:

  • The REST controllers that implement the endpoints called by the UI and/or other µ-services;
  • The services that implement your business logic;
  • The repositories that provide persistence of your entities to the database of your choice;
  • Additional out-bound components, such as a REST client to access your supplier’s inventory, or a web socket or information bus client to notify sibling µ-services.

Among your JUnit tests, you have some simple ones that check the low-level functions of your business logic implementation, such as VAT computation. But you also have some more complex ones that aim at checking your service from a more global viewpoint. For example, that when a customer buys a book, there is one less in stock after than before; that if it was the last in stock, an alarm is raised; etc. You can recognise those tests by the fact that you probably had debates with your teammates about whether they could legitimately be called “unit tests”.

The challenge for these complex tests is that you want the code of your Spring services to execute, including when they call each other, as if they had been stimulated by a user interaction. However, you do not want to set up a real database, you have nowhere to send notifications, and you do not want to really connect to your supplier’s API. This is where the SpringRunner of JUnit 4, and the SpringExtension of JUnit 5, come into play. (Actually, the latter is specific to JUnit Jupiter, one of the engines for JUnit 5.)

In short, the two axes that you have to work along are: ensure that the Spring machinery runs, including bean instantiation and wiring; and ensure that it stops at the boundaries of your code under test. The first point is usually achieved by annotating your test class with

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ComplexTestConfiguration.class)

where ComplexTestConfiguration is a Spring configuration class in which you install the services under test and you mock the rest. It may look as follows

On line 2, we install the Spring services that we want to test. Lines 3–4 configure our MongoDB repositories to be used with an embedded implementation of the database, such as Flapdoodle’s one. Lines 6–7 provide a stub to be used in lieu of the REST client to our supplier’s inventory. Similarly, lines 9–10 ensure that our notification service will run, although no message will be sent anywhere. Still, we will be able to check what this service sent to the web socket, using Mockito’s verify methods.

Introducing a Base Class for Test Classes (Wrong!)

With the ComplexTestConfiguration configuration class (tailored to your needs) and the two annotations on the test class, you can write and run some tests that check the behaviour of the services that implement your business logic from end to end. Still, as you write more and more such complex tests, you will soon notice that you are duplicating code between test classes and methods, be it set-up code to install a starting environment for your tests, or some code patterns that you would like to factor.

In my case, for example, one of the repositories had been mocked in memory and needed to be emptied before each test. Also, I had written a couple of utility methods to decode the messages sent to the web socket and check that they matched my expectations. This resulted in some code that I wanted to share between all my test classes, do I ended up with having these test classes extend the ComplexTestBase class.

As a result, the test classes could read as follows:

The ComplexTestBase class above gathers the Spring configuration annotations, the definition of JUnit hooks, and the utility methods. It is ready for use as a superclass by test classes that want to benefit from all these. Yet, it is not the recommended way to go. Why that?

To be honest, in simple cases, gathering the boilerplate in a superclass is not a problem in my opinion. It gets the job done, in a clean way, it is readable, maintainable, etc. However, as soon as you no longer are in a simple case, you hit the single inheritance wall. Then, if you want to stick to the inheritance model, you will start piling up superclasses in an arbitrary order, introducing unwanted dependencies, and basically ruining the encapsulation and maintainability properties just mentioned.

In my case, the situation occurred when we introduced an access control system to our services. We rapidly wanted to provide testing utilities that would allow the developers of all our µ-services to write tests against various permission set-ups. Without surprise, these testing utilities included: Spring configuration, JUnit hooks, and a handful of utility methods. This is precisely why JUnit 5 switched to the extension model.

Switching to Extensions (and Annotations)

JUnit Extensions in Two Words

Technically speaking, the role of an extension is to provide JUnit hooks. This includes the typical @BeforeEach, but also hooks for other points of the testing life cycle, including the injection of parameters to test methods, which is quite powerful and elegant. In addition, extensions introduce the extension context, a clean mechanism to store and convey state information across the tests life-cycle, and even between extensions, as we will see further below.

Here are three links that helped me:

Putting Extensions Into Play

The goal here is no different from above: package and deliver Spring configuration, JUnit hooks, and utility methods to test classes. What is new is that we are going to do it in a composable way.

We start by introducing an annotation that will replace the inheritance:

This is nothing revolutionary: we simply moved the two annotations from previous section into the @ConfigureComplexTest annotation (lines 3–4). Lines 1–2 are standard in annotation definitions, except perhaps the inclusion of ANNOTATION_TYPE in the targets. This is to leverage the meta-annotation mechanism in JUnit.

Using this annotation achieves the Spring configuration just as the base class, through to the unchanged ComplexTestConfiguration class. To also provide the JUnit hooks and our utility methods, we introduce a specific JUnit extension, which we include in the annotation (look at line 3):

In our case, all of the hooks in our extension will be dealing with Spring components. The first item in our agenda is thus to grasp a link to Spring’s application context. To this end, we add a hook that is called just after the test class instance is initialised, in which we retrieve the application context from the Spring extension.

By having our extension class also implement BeforeEachCallback, we can add the hook that will execute before each test. Similar life-cycle hooks are added by implementing the corresponding interfaces.

To provide our utility methods, we will gather them in a Spring service and put this service at the disposal of test methods through a parameter. Let’s first introduce our service class. It is not very different from the base class of previous section:

The last step is to augment our extension class so that it triggers the reset() method above before each test, and it provides the service as a parameter to methods that request them.

For line 17 above to actually return our helper service, you will need to augment ComplexTestConfiguration so that it scans the ComplexTestHelper class, typically by adding its package to @ComponentScan.

As a result of the above, here is how our test class would now read:

The main differences with the superclass version are:

  • The inheritance is replaced with an annotation. Obviously, this can be repeated ad lib, while inheritance could not.
  • The inherited fields and methods are now accessed though the helper parameter.

Conclusion

While the base class pattern is ok in simple situations, it is limited by the single inheritance model of Java. With annotations and extensions, JUnit Jupiter provides a composable solution to factor Spring configuration, JUnit hooks, and utility methods. (However, the hard part remains to properly configure dependency injection, in both cases.)

On our project, we applied the above pattern to the complex tests that address the business logic of each µ-service. We also applied it to package some testing utilities of our access control system. As a result, we could easily write tests that check the business logic of a µ-service under various permission settings: the test class simply bears two @Configure... annotations, and the test method takes to helper parameters.

Special thanks to my colleague Adrien Estran for pointing out the subject, for helping me in its implementation, and for proof-reading this story.

--

--