Custom Temp Folders in JUnit 5

It is always instructive to look at well-known open source projects to learn from the implementation used. Sometimes you look at the unit tests and start to wonder. A serious problem with large and old frameworks is the time-consuming adaptation to changing environments.

Frameworks that were implemented in Java 8 and used test frameworks such as JUnit 3 still often contain constructs that could have been replaced years ago by more elegant and, in particular, much shorter code fragments. Shorter, more compact code means faster changes and therefore a shorter time-to-market.

While the actual code still changes relatively quickly, time seems to be frozen in the unit tests. The problem with the tests is their sheer volume and unfortunately all too often the lack of knowledge of whatever these tests were supposed to test at some point. As with the production code, the test code remains unaffected by technological change. Newer libraries are used but their possibilities are not utilized.

A well-known open source project contains a large number of unit tests that write data to the file system. An important component of all these tests is the creation of the output directories.

    private static final File OUT_DIR = new File("target/test-output");

    @BeforeEach
    public void setUp() throws IOException
    {
        OUT_DIR.mkdirs();
    }

As you can easily see from the source code, this project uses Maven and JUnit 5. Before each test is started, the test-output directory is created in the Maven target directory.

JUnit 5 uses the @TempDir annotation to create temporary directories and assign them to Path and File instances. Tthe following code creates a directory in the system-specific temp directory.

@TempDir(cleanup = CleanupMode.NEVER)
private File outDir;

The above two-liner would therefore be sufficient to remove many @BeforeEach methods in the project in question. The cleanup attribute is also specified here so that the output files are not deleted according to the current project behavior.

However, this article is about how the JUnit 5 extension mechanism can be used to adapt the mechanism for temp directories. The files should therefore not be stored in a temp directory, but in a subdirectory of the Maven target directory.

The JUnit 5 framework helps out here with a TempDirFactory. The @TempDir annotation can be given its own implementation of this factor with each call. The following class TargetTempDirFactory is our implementation for this task.

class TargetTempDirFactory implements TempDirFactory {
    private static final TARGET = Path.of("target");

    @Override
    public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext) throws Exception {
        Path target = elementContext.findAnnotation(TargetTempDir.class)
                .map(TargetTempDir::value).map(Path::of)
                .filter(p -> !p.isAbsolute()).map(TARGET::resolve).orElse(TARGET);
        return Files.createDirectories(target);
    }
}

The name of the desired directory is read from a custom annotation @TargetTempDir and then prefixed with the target directory. In the case of an absolute path, this value is ignored and the output files end up directly in the target directory. The behavior can be changed here, but it is an edge case that does not exist in the project in question. If the target path has been determined, the directories are created with Files#createDirectories if required.

Now only the annotation @TargetTempDir is missing, which should ensure that the specified directories are created.

@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@TempDir(factory = TargetTempDirFactory.class, cleanup = CleanupMode.NEVER)
public @interface TargetTempDir {
    String value();
}

This annotation is a meta-annotation that is itself annotated with @TempDir. This means that only @TargetTempDir has to be specified and not @TempDir as well. Our custom TargetTempDirFactory is used via the factory attribute on the @TempDir annotation. The original test code finally becomes the following two-liner.

@TargetTempDir("test-output")
private File outDir;

The reduction to a separate TempDirFactory not only reduces the developer’s mental load, but also offers many further development options.

In addition, the base directory of the test outputs can now be changed for all tests at the same time. For example, to the system-specific temp directory. Outputs that are not needed later could be deleted directly after the test and outputs that are not needed at all could be written to a directory in a fake file system.

In conclusion, modern features in libraries such as JUnit 5 simplify the code base immensely. They just need to be used.

Leave a Comment