Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

When testing, we often need access to a temporary file. However, managing, creating and deleting these files ourselves can be cumbersome.

In this quick tutorial, we’ll look at how JUnit 5 alleviates this by providing the TempDirectory extension.

For an in-depth guide to testing with JUnit, check out our excellent Guide to JUnit 5.

2. The TempDirectory Extension

Starting with version 5.4.2, JUnit 5 provides the TempDirectory Extension. However, it’s important to note that this is still an experimental feature and that we’re encouraged to give feedback to the JUnit team.

As we’ll see later, we can use this extension to create and clean up a temporary directory for an individual test or all tests in a test class.

Usually, when using an extension, we need to register it from within a JUnit 5 test using the @ExtendWith annotation. But this is unnecessary with the TempDirectory extension, which is built-in and registered by default.

3. Maven Dependencies

First, let’s add the project dependencies we will need for our examples.

Apart from the main JUnit 5 library junit-jupiter-engine, we’ll also need the junit-jupiter-api library:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>
</dependency>

As always, we can get the latest version from Maven Central.

In addition to this, we’ll also need to add the junit-jupiter-params dependency:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>
</dependency>

Again, we can find the latest version in Maven Central.

4. Using the @TempDir Annotation

To use the TempDirectory extension, we need to make use of the @TempDir annotation. We can only use this annotation with the following two types:

  • java.nio.file.Path
  • java.io.File

If we try to use it with a different type, then an org.junit.jupiter.api.extension.ParameterResolutionException will be thrown.

Next, let’s explore several different ways of using this annotation.

4.1. @TempDir as a Method Parameter

Let’s begin by seeing how to inject a parameter annotated with @TempDir into a single test method:

@Test
void givenTestMethodWithTempDirectory_whenWriteToFile_thenContentIsCorrect(@TempDir Path tempDir) 
  throws IOException {
    Path numbers = tempDir.resolve("numbers.txt");

    List<String> lines = Arrays.asList("1", "2", "3");
    Files.write(numbers, lines);

    assertAll(
      () -> assertTrue("File should exist", Files.exists(numbers)),
      () -> assertLinesMatch(lines, Files.readAllLines(numbers)));
}

As we can see, our test method creates and writes a file called numbers.txt in the temporary directory tempDir.

We then check that the file exists and the content matches what was originally written. Nice and simple!

4.2. @TempDir on an Instance Field

In the following example, we’ll annotate a field in our test class using the @TempDir annotation:

@TempDir
File anotherTempDir;

@Test
void givenFieldWithTempDirectoryFile_whenWriteToFile_thenContentIsCorrect() throws IOException {
    assertTrue("Should be a directory ", this.anotherTempDir.isDirectory());

    File letters = new File(anotherTempDir, "letters.txt");
    List<String> lines = Arrays.asList("x", "y", "z");

    Files.write(letters.toPath(), lines);

    assertAll(
      () -> assertTrue("File should exist", Files.exists(letters.toPath())),
      () -> assertLinesMatch(lines, Files.readAllLines(letters.toPath())));
}

This time, we use a java.io.File for our temporary directory. Again, we write some lines and check they were written successfully.

If we were to use this single reference again in other test methods, each test would use its temporary directory.

4.3. A Shared Temporary Directory

Sometimes, we might want to share a temporary directory between test methods.

We can do this by declaring our field static:

@TempDir
static Path sharedTempDir;

@Test
@Order(1)
void givenFieldWithSharedTempDirectoryPath_whenWriteToFile_thenContentIsCorrect() throws IOException {
    Path numbers = sharedTempDir.resolve("numbers.txt");

    List<String> lines = Arrays.asList("1", "2", "3");
    Files.write(numbers, lines);

    assertAll(
        () -> assertTrue("File should exist", Files.exists(numbers)),
        () -> assertLinesMatch(lines, Files.readAllLines(numbers)));
}

@Test
@Order(2)
void givenAlreadyWrittenToSharedFile_whenCheckContents_thenContentIsCorrect() throws IOException {
    Path numbers = sharedTempDir.resolve("numbers.txt");

    assertLinesMatch(Arrays.asList("1", "2", "3"), Files.readAllLines(numbers));
  }

The key point here is that we use a static field, sharedTempDir, which we share between the two test methods.

In the first test, we again wrote some lines to a file called numbers.txt. Then, we check that the file and content already exist in the next test.

We also enforce the order of the tests via the @Order annotation to ensure the behaviour is always consistent.

4.4. The cleanup Option

Usually, the temporary directory created by @TempDir will be automatically deleted after the test execution. However, in some cases, we may want to preserve the temporary directory for debugging purposes.

For example, if a test fails, we may want to examine the contents of the temporary directory to see if there are any clues about the cause of the failure. In such cases, the automatic deletion of the temporary directory by Junit5 may not be desirable.

To address this issue, Junit5 provides a cleanup option for the @TempDir annotation. The cleanup option can be used to specify whether the temporary directory should be deleted automatically at the end of the test method or not.

The cleanup option can be set to one of the following CleanupMode values:

  • ALWAYS – This option specifies that the temporary directory should always be deleted automatically at the end of the test method, regardless of whether the test succeeds or fails. This is the default mode.
  • ON_SUCCESS – The temporary directory will be deleted after the test method execution only if the test succeeds.
  • NEVER – The temporary directory won’t be deleted automatically after executing the test method.

Next, let’s create a test class to verify if we set NEVER as the cleanup option’s value, the temporary directory won’t be removed after the test execution:

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TemporaryDirectoryWithCleanupUnitTest {

    private Path theTempDirToBeChecked = null;

    @Test
    @Order(1)
    void whenTestMethodWithTempDirNeverCleanup_thenSetInstanceVariable(@TempDir(cleanup = NEVER) Path tempDir) {
        theTempDirToBeChecked = tempDir;
        System.out.println(tempDir.toFile().getAbsolutePath());
    }

    @Test
    @Order(2)
    void whenTestMethodWithTempDirNeverCleanup_thenTempDirShouldNotBeRemoved() {
        assertNotNull(theTempDirToBeChecked);
        assertTrue(theTempDirToBeChecked.toFile().isDirectory());
    }

}

As the class above shows, we added the @TestInstance(TestInstance.Lifecycle.PER_CLASS) annotation to the test class so that JUnit creates only one instance of the test class and uses it for all tests. This is because we’ll set the theTempDirToBeChecked instance variable in the first test and verify if the temporary directory is still there in the second test.

If we run the test, it passes. So, it indicates during the second test’s run, the temporary directory created in the first test isn’t removed.

Further, we can see the output from the first test:

/tmp/junit16624071649911791120

The INFO log from Junit5 reminds us that the temporary directory won’t be removed after the first test:

INFO: Skipping cleanup of temp dir /tmp/junit16624071649911791120 due to cleanup mode configuration.

After all test executions, if we check the temporary directory on the filesystem, the directory still exists:

$ ls -ld /tmp/junit16624071649911791120
drwx------ 2 kent kent 40 Apr  1 18:23 /tmp/junit16624071649911791120/

5. Gotchas

Now, let’s review some subtleties we should know when working with the TempDirectory extension.

5.1. Creation

The curious reader out there will probably wonder where these temporary files are created?

Well, internally, the JUnit TemporaryDirectory class makes use of the Files.createTempDirectory(String prefix) method. Likewise, this method then uses the default system temporary file directory.

This is usually specified in the environment variable TMPDIR:

TMPDIR=/var/folders/3b/rp7016xn6fz9g0yf5_nj71m00000gn/T/

For instance, resulting in a temporary file location:

/var/folders/3b/rp7016xn6fz9g0yf5_nj71m00000gn/T/junit5416670701666180307/numbers.txt

Meanwhile, if the temporary directory cannot be created, an ExtensionConfigurationException will be thrown as appropriate. Or, as previously mentioned, a ParameterResolutionException.

5.2. Deletion

The cleanup attribute’s default value is ALWAYS. That is to say, when the test method or class has finished execution, and the temporary directory goes out of scope, the JUnit framework will attempt to delete all files and directories in that directory recursively and, finally, the temporary directory itself.

If there’s a problem during this deletion phase, an IOException will be thrown, and the test or test class will fail.

6. Customizing Temporary Directories Creation Using TempDirFactory

To customize the creation of temporary directories, we need to implement the TempDirFactory interface. This interface is located in the org.junit.jupiter.api.io package and is part of the JUnit Jupiter API module. It’s also used with the @TempDir annotation to specify a temporary directory for a test method or class.

Also, the TempDirFactory interface has a single method called createTempDir(), which creates the temporary directory. We must implement this method with your logic for creating the temporary directory. Here’s an example implementation of the TempDirFactory interface:

class TempDirFactoryUnitTest {

    @Test
    void factoryTest(@TempDir(factory = Factory.class) Path tempDir) {
        assertTrue(tempDir.getFileName().toString().startsWith("factoryTest"));
    }

    static class Factory implements TempDirFactory {

        @Override
        public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
                throws IOException {
            return Files.createTempDirectory(extensionContext.getRequiredTestMethod().getName());
        }

    }
}

In this example, the createTempDir() method creates a temporary directory using the Files.createTempDirectory() method from the Java NIO package. We can customize this method according to our requirements.

7. Conclusion

In this article, we’ve explored the TempDirectory Extension provided by JUnit 5.

First, we introduced the extension and learned what Maven dependencies we need to use it. Next, we looked at several examples of how to use the extension from within our unit tests.

Finally, we looked at several gotchas, including where the temporary files are created and what happens during deletion.

As always, the full source code of the article is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.