How to Replace Rules in JUnit 5
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.
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.
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() { File file = tempFolder.newFile(); // ... } }
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 TemporaryFolder
s, 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 ) { File file = tempFolder.newFile(); // ... } }
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.
The good news is that JUnit 5 provides a means to maintain state of extensions called Store
s. 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 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.
- Extras for Eclipse: Neon Update - 6. July 2016
- What’s the Difference? Creating Diffs with JGit - 16. June 2016
- Terminate and Relaunch in Eclipse - 19. April 2016
[…] 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 […]
what are the maven coordinates for TemporaryFolderExtension ?
I dont seem to be able to get this to work.
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.this doesn’t seem to work with the latest 5.0.0-M3 API
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;
}
}
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
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,
Thank you very much, dear attentive reader, :-) I’ve fixed the typo.
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.)
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.
I agree with Jared. This neither looks right nor intuitive.