How to Replace Rules in JUnit 5

Home  >>  JUnit  >>  How to Replace Rules in JUnit 5

How to Replace Rules in JUnit 5

On April 6, 2016, Posted by , In JUnit, By ,,,, , With 11 Comments

The recently published JUnit 5 (aka JUnit Lambda) alpha release caught my interest, and while skimming through the documentation I noticed that rules are gone – as well as runners and class rules. According to the user guide, these partially competing concepts have been replaced by a single consistent extension model.

Over the years, Frank and I wrote several rules to help with recurring tasks like testing SWT UIs, ignoring tests in certain environments, registering (test) OSGi services, running tests in separate threads, and some more.

Therefore, I was particularly interested in what it would take to transform existing rules to the new concept so that they could run natively on JUnit 5. To explore the capabilities of extensions, I picked two rules with quite different characteristics and tried to migrate them to JUnit 5.


The focus of these experiments is to see what concepts have changed between rules and extensions. Therefore, I chose to rewrite the JUnit 4 means with no backward compatibility in mind.

If you are interested in migrating from JUnit 4 to 5 or explore possibilities to run existing rules in JUnit 5 you may want to join the respective discussions.

The first candidate is the ConditionalIgnoreRule that works in tandem with the @ConditionalIgnore annotation. The rule evaluates a condition that needs to be specified with the annotation and based thereon decides whether the test is executed or not.

The other candidate is the built-in TemporaryFolder rule. Like the name suggests, it allows creating files and folders that are deleted when the test finishes.

Therefore it hooks in before and after the test execution to create a root directory to store files and folders in and to clean up this directory. In addition, it provides utility methods to create files and folders within the root directory.

Extensions Explained

Before going into the details of migration rules to extensions, let’s have a brief look at the new concept.

How to Replace Rules in JUnit 5 The test execution follows a certain life cycle. And each phase of that life cycle that can be extended is represented by an interface. Extensions can express interest in certain phases in that they implement the corresponding interface(s).

With the ExtendWith annotation a test method or class can express that it requires a certain extension at runtime. All extensions have a common super interface: ExtensionPoint. The type hierarchy of ExtensionPoint lists all places that extension currently can hook in.

The code below for example , applies a fictional MockitoExtension that injects mock objects:

@ExtendWith(MockitoExtension.class)
class MockTest {
  @Mock
  Foo fooMock; // initialized by extension with mock( Foo.class )
}

The MockitoExtension would provide a default constructor so that it can be instantiated by the runtime and implement the necessary extension interface(s) to be able to inject mocks into all @Mock annotated fields.

Conditional Ignore Rule Extension

A recurring pattern for rules is to provide a service in tandem with an annotation that is used to mark and/or configure test methods that wish to use the service. Here the ConditionalIgnoreRule examines all test methods that it runs with and looks for a ConditinalIgnore annotation. If such an annotation is found, its condition is evaluated and if satisfied, the test is ignored.

Here is how the ConditionalIgnoreRule may look like in action:

@Rule
public ConditionalIgnoreRule rule = new ConditionalIgnoreRule();
 
@Test
@ConditionalIgnore( condition = IsWindowsPlatform.class )
public void testSomethingPlatformSpecific() {
  // ...
}

And now, let’s see how the code should look like in JUnit 5:

@Test
@DisabledWhen( IsWindowsPlatform.class )
void testSomethingPlatformSpecific() {
  // ...
}

First you’ll note that the annotation changed its name. To match the JUnit 5 conventions that use the term disabled instead of ignored, the extension also changed its name to DisabledWhen.

Though the DisabledWhen annotation is driven by the DisabledWhenExtension, there is no sight of nothing that declares that the extension is necessary. The reason therefor is called meta annotations and they are best illustrated when looking at how DisabledWhen is declared:

@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(DisabledWhenExtension.class)
public @interface DisabledWhen {
  Class<? extends DisabledWhenCondition> value();
}

The annotation is (meta) annotated with the extension that handles it. And at runtime, the JUnit 5 test executor takes care of the rest. If an annotated test method is encountered and this annotation is in turn meta-annotated by ExtendWith, the respective extension is instantiated and included into the life cycle.

See also  JUnit 5 - A First Look at the Next Generation of JUnit

Really neat, innit? This trick also avoids an oversight when annotating a test method without specifying the corresponding rule.

Behind the scenes, the DisabledWhenExtension implements the TestExexutionCondition interface. For each test method, its sole evaluate() method is called and must return a ConditionEvaluationResult that determines if or if not a test should be executed.

The rest of the code is basically the same as before. The DisabledWhen annotation is looked up and when found, an instance of the specified condition class is created and asked whether the test should execute or not. If execution is declined a disabled ConditionEvaluationResult is returned and the framework acts accordingly.

TemporaryFolder Rule Extension

Before turning the TemporaryFolder rule into an extension, let’s have a look at what the rule consists of. First the rule provisions and cleans up a temporary folder during test setup and teardown. But it also provides the test with access to methods to create (temporary) files and folders within that root folder.

After migrating to an extension the different responsibilities become even more apparent. The following example shows how it might be used:

@ExtendWith(TemporaryFolderExtension.class)
class InputOutputTest
  private TemporaryFolder tempFolder;

  @Test
  void testThatUsesTemporaryFolder() {
    F‌ile f‌ile = tempFolder.newF‌ile();
    // ...
  }
}

The TemporaryFolderExtension hooks into the test execution life cycle in order to provision and clean up the temporary folder and also to supply all TemporaryFolder fields with an instance of this type. Whereas the TemporaryFolder gives access to methods to create files and folders within a root folder.

In order to inject TemporaryFolders, the extension implements the InstancePostProcessor interface. Its postProcessTestInstance method is called right after a test instance is created. Within that method it has access to the test instance via the TestExtensionContext parameter and can inject a TemporaryFolder into all matching fields.

For the unlikely event that a class declares multiple TemporaryFolder fields, each field is assigned a new instance and each of them has its own root folder.

All injected TemporaryFolder instances created in this process are held in a collection so that they an be accessed later when it’s time to clean up.

To clean up after the test was executed, another extension interface needs to be implemented: AfterEachExtensionPoint. Its sole afterEach method is called after each test is done. And the TemporaryFolderExtension implementation hereof cleans up all known TemporaryFolder instances.

Now that we are on par with the features of the TemporaryFolder rule, there is also new feature to support: method level dependency injection.
In JUnit 5, methods are now permitted to have parameters.
This means that our extension should not only be able to inject fields but also method parameters of type TemporaryFolder.
A test that wishes to create temporary files could request to have a TemporaryFolder injected like in the following example:

class InputOutputTest {
  @Test
  @ExtendWith(TemporaryFolderExtension.class)
  void testThatUsesTemporaryFolder( TemporaryFolder tempFolder ) {
    F‌ile f‌ile = tempFolder.newF‌ile();
    // ...
  }
}

By implementing the MethodParameterResolver interface, an extension can participate in resolving method parameters. For each parameter of a test method the extenion’s supports() method is called to decide if it can provide a value for the given parameter. In case of the TemporaryFolderExtension the implementation checks whether the parameter type is a TemporaryFolder and returns true in this case. If a broader context is necessary, the supports() method is also provided with the current method invocation context and extension context.

Now that the extension decided to support a certain parameter, its resolve() method must provide a matching instance. Again, the surrounding contexts are provided. The TemporaryFolderExtension simply returns a unique TemporaryFolder instance that knows the (temporary) root folder and provides methods to create files and sub-folders therein.

Note however, that it is considered an error to declare a parameter that cannot be resolved. Consequently, if a parameter without a matching resolver is encountered an exception is raised.

Storing State in Extensions

As you may have noticed, the TemporaryFolderExtension maintains its state (i.e. the list of temporary folders it has created) currently a simple field. While the tests have shown that this works in practice, the documentations nowhere states that the same instance is used throughout invoking the different extensions. Hence, if JUnit 5 changes its behavior at this point, state may well be lost during these invocations.

See also  JUnit Rules

The good news is that JUnit 5 provides a means to maintain state of extensions called Stores. As the documentation puts it, they provide methods for extensions to save and retrieve data.

The API is similar to that of a simplified Map and allows to store key-value pairs, get the value associated with a given key, and remove a given key. Keys and values both can be arbitrary objects. The store can be reached through the TestExtensionContext that is passed as a parameter to each extension method (e.g. beforeEach, afterEach).Each TestExtensionContext instance encapsulates the context in which the current test is being executed.

In beforeEach, for example, a value would be stored within the extension context like this:

@Override
public void beforeEach( TestExtensionContext context ) {
  context.getStore().put( KEY, ... );
}

And could later be retrieved like this:

@Override
public void afterEach( TestExtensionContext context ) {
  Store store = context.getStore();
  Object value = store.get( KEY );
  // use value...
}

To avoid possible name clashes, stores can be created for a certain namespaces. The context.getStore() method used above obtains a store for the default namespace. To get a store for a specific namespace, use

context.getStore( Namespace.of( MY, NAME, SPACE );

A namespace is defined through an array of objects, { MY, NAME, SPACE } in this example.

The exercise to rework the TemporaryFolderExtension to use a Store is left to the reader.

Running the Code

A spike implementation of the two extensions discussed here can be found in this GitHub repository:
     https://github.com/rherrmann/junit5-experiments

The project is set up to be used in Eclipse with Maven support installed. But it shouldn’t be difficult to compile and run the code in other IDEs with Maven support.

Quite naturally at this early stage, there is no support to run JUnit 5 tests directly in Eclipse yet. Therefore, to run all tests, you may want to use the Run all tests with ConsoleRunner launch configuration. If you run into trouble please consult the Running Tests with JUnit 5 section of my previous post about JUnit 5 for a few more hints or leave a comment.

Concluding How to Replace Rules in JUnit 5

Throughout this little experiment, I got the impression that extensions are a decent and complete replacement for rules and friends in JUnit 4. And finally, using the new methods is fun and feels much more concise than the existing facilities.

If you find a use case that can’t be accomplished with extensions yet, I’m sure the JUnit 5 team will be grateful if you let them know.

But note however, that as of this writing extensions are work in progress. The API is marked as experimental and may change without prior notice. Thus it might be a bit early to actually migrate your JUnit 4 helpers right now – unless you don’t mind to adjust your code to the potentially changing APIs.

If JUnit 5 extensions have caught your interest you may also want to continue reading the respective chapter of the documentation.

 Testing with JUnit

Testing with JUnit Book

Testing with JUnit is one of the most valuable skills a Java developer can learn. No matter what your specific background, whether you’re simply interested in building up a safety net to reduce regressions of your desktop application or in improving your server-side reliability based on robust and reusable components, unit testing is the way to go.

Frank has written a book that gives a profound entry point in the essentials of testing with JUnit and prepares you for test-related daily work challenges.

  Get It Now! 

Rüdiger Herrmann
Follow me
Latest posts by Rüdiger Herrmann (see all)

11 Comments so far:

  1. […] show examples for how to use the corresponding extension points. If you can’t wait, check out this post, which shows how to port two JUnit 4 rules (conditional disable and temporary folder) to JUnit […]

  2. Jason S says:

    what are the maven coordinates for TemporaryFolderExtension ?

    I dont seem to be able to get this to work.

    • Rüdiger Herrmann says:

      There are no maven coordinates. The TemporaryFolderExtension is solely meant to demonstrate the extension facilities of JUnit 5. The source code is in the junit5-experiments repository that is also linked to at the end of the article.

      • Jason S says:

        this doesn’t seem to work with the latest 5.0.0-M3 API

        • Jason S says:

          migrated the code, below for TemporaryFolderExtension:-

          package com.logpath.tools.codeaffine;

          import static java.util.Arrays.stream;

          import java.io.IOException;
          import java.lang.reflect.Field;
          import java.util.ArrayList;
          import java.util.Collection;

          import org.junit.jupiter.api.extension.*;

          public class TemporaryFolderExtension
          implements AfterEachCallback, TestInstancePostProcessor, ParameterResolver
          {

          private final Collection tempFolders;

          public TemporaryFolderExtension() {
          tempFolders = new ArrayList();
          }

          @Override
          public void afterEach( TestExtensionContext context ) throws IOException {
          tempFolders.forEach( TemporaryFolder::cleanUp );
          }

          @Override
          public void postProcessTestInstance(Object testInstance, ExtensionContext context ) {

          stream( testInstance.getClass().getDeclaredFields() )
          .filter( field -> field.getType() == TemporaryFolder.class )
          .forEach( field -> injectTemporaryFolder( testInstance, field ) );
          }

          private void injectTemporaryFolder( Object instance, Field field ) {
          field.setAccessible( true );
          try {
          field.set( instance, createTempFolder() );
          } catch( IllegalAccessException iae ) {
          throw new RuntimeException( iae );
          }
          }

          @Override
          public boolean supports( ParameterContext parameterContext,
          ExtensionContext extensionContext )
          {

          return parameterContext.getParameter().getType() == TemporaryFolder.class;
          }

          @Override
          public Object resolve( ParameterContext parameterContext,
          ExtensionContext extensionContext )
          {
          return createTempFolder();
          }

          private TemporaryFolder createTempFolder() {
          TemporaryFolder result = new TemporaryFolder();
          result.prepare();
          tempFolders.add( result );
          return result;
          }

          }

          • Rüdiger Herrmann says:

            Hi Jason,

            thank you for updating the TemporaryFolderExtension. If you could open a pull request for the junit5-experients repository, I would try to find some time to merge the changes next week.

            Thanks again,
            Rüdiger

  3. Dan Rollo says:

    Minor typo: ‘exception’ to ‘extension’ in section: ‘TemporaryFolder Rule Extension’

    Before turning the TemporaryFolder rule into an exception,

    should be:

    Before turning the TemporaryFolder rule into an extension,

    • Rüdiger Herrmann says:

      Thank you very much, dear attentive reader, :-) I’ve fixed the typo.

  4. Jared Stewart says:

    What’s the reasoning behind replacing Rules with ExtendWith?

    The new example you have using a TemporaryFolder seems less intuitive to read than the JUnit 4 equivalent. (In JUnit 4, when the TemporaryFolder field is annotated with @Rule, it’s obvious that the field refers to a Rule. But now when I see the TemporaryFolder field in JUnit 5 I have to somehow know a-priori that it’s being populated by the extension on the class.)

    • Rüdiger Herrmann says:

      Jared, thanks for sharing your insights. I agree that with JUnit 5 the relation between TeporaryFolder and TemporaryFolderExtension (or other extension for that matter) isn’t obvious at all. However, so far, I found the increased flexibility of extensions appealing. Extensions – other than Rules – do not per se apply to the entire class. As shown above, the TemporaryFolder could also be injected as a method argument to just a single test.

      Though I haven’t used JUnit 5 in production yet, I would well give it a try if a chance arises to see how the overall concept of JUnit 5 proves itself in practice.

    • Felix says:

      I agree with Jared. This neither looks right nor intuitive.