1. Overview

When testing, we often need access to a temporary file. However, managing the creation and deletion of these files ourselves can be cumbersome.

In this quick tutorial, we’ll take a 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 officially 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.

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

3. Maven Dependencies

First of all, 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

In order 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

In fact, 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 that the content matches what was originally written. Really nice and simple!

4.2. @TempDir on an Instance Field

In this next 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 then use this single reference again in other test methods, each test would use its own 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 write 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 make sure the behavior 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

And 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 tests 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 of the subtleties we should be aware of when working with the TempDirectory extension.

5.1. Creation

The curious reader out there will most probably be wondering where these temporary files are actually created?

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

This is normally 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. Conclusion

To summarize, in this article, we’ve explored the TempDirectory Extension provided by JUnit 5.

First, we started by introducing the extension and learned what Maven dependencies we need in order 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.