Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

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.4.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.4.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.

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

When the test method or class has finished execution and the temporary directory goes out of scope, the JUnit framework will attempt to recursively delete all files and directories in that directory and, finally, the temporary directory itself.

If there is 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 tutorial, 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.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE