skip to content

@TempDir

The built-in temporary-directory extension — injection targets, sharing, and cleanup guarantees. A practical question that doubles as a worked ParameterResolver example.

on this pageshow

questions

5

In JUnit 5, how do you obtain a throwaway directory for a test that needs to read and write real files, and what field or parameter types can receive it?

level: juniorimportance: must knowfreq 55%

answer

  1. @TempDir from org.junit.jupiter.api.io
  2. Path or File only
  3. field or parameter injection
  4. recursive delete afterwards
  5. one directory per declaration

basics

~20 s

Annotate a field or a method parameter of type java.nio.file.Path or java.io.File with JUnit Jupiter's @TempDir. Jupiter's built-in extension creates a fresh empty directory in the OS temp location before use and recursively deletes it, with its contents, afterwards.

solid answer

~40 s

JUnit Jupiter ships a built-in extension for this: annotate a `Path` or `File` with `@TempDir`. It works on an instance field, a static field, or as a parameter of a test method, a lifecycle method such as `@BeforeEach`, or the test-class constructor. ```java @Test void writesReport(@TempDir Path dir) throws IOException { Files.writeString(dir.resolve("report.csv"), "id,name\n"); } ``` Jupiter creates a brand-new empty directory under the platform temp location (`java.io.tmpdir` by default) and afterwards deletes that directory **recursively**, so anything the test wrote inside it disappears too. Only `Path` and `File` are supported; any other type produces an extension configuration exception. The value is isolation and hygiene: no shared scratch folder, no leftovers between runs, no hand-written `deleteOnExit` bookkeeping, and parallel-safe tests because every `@TempDir` declaration gets its own separate directory.

code

java · 27 lines
java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;

class ReportWriterTest {

    @TempDir
    Path workDir; // fresh directory for every test method

    @Test
    void writesHeader() throws IOException {
        Path out = workDir.resolve("report.csv");
        new ReportWriter().write(out);
        assertEquals("id,name", Files.readAllLines(out).get(0));
    }

    @Test
    void copiesBetweenTwoDirectories(@TempDir Path source, @TempDir Path target)
            throws IOException {
        Files.writeString(source.resolve("a.txt"), "hello");
        new Syncer().sync(source, target);
        assertTrue(Files.exists(target.resolve("a.txt")));
    }
}

go deeper

for a junior

Know the annotation name, that Path and File are the accepted types, and that the directory is created empty and deleted recursively afterwards.

for a middle

Add the injection points (field, constructor, test and lifecycle method parameters), the non-final requirement, and that each declaration yields its own directory.

for a senior

Frame it as isolation and parallel-safety, prefer Path over File for factory and non-default-filesystem support, and mention that deletion can fail if handles are left open.

for a principal

Position it as the standard for filesystem-touching tests and note the design pressure it creates: code under test should accept a root path rather than hardcoding locations, which keeps tests hermetic across CI agents.

## The problem it solves Tests that touch the filesystem — writing a CSV, unpacking a zip fixture, exercising a file-backed cache — need a directory that is empty when the test starts and gone when it ends. Hand-rolled versions (`new File("target/scratch")`, `File.createTempFile` plus `deleteOnExit()`) leak: they survive crashes, they collide when tests run in parallel or when the same class runs twice, and one test's leftovers silently make the next test pass or fail for the wrong reason. JUnit Jupiter answers this with a built-in extension exposed through a single annotation, `org.junit.jupiter.api.io.@TempDir`. ## How you declare it There are two shapes. **Injected parameter.** Put `@TempDir` on a parameter of a `@Test`, `@ParameterizedTest`, `@RepeatedTest`, a lifecycle method (`@BeforeEach`, `@AfterEach`, `@BeforeAll`, `@AfterAll`), or the test-class constructor. Jupiter resolves the argument for you: ```java @Test void roundTrips(@TempDir Path dir) { ... } ``` **Field.** Put `@TempDir` on a field of the test class. A non-static field is populated for each test instance; a static field is populated once for the class. ```java class ArchiveTest { @TempDir Path workDir; // fresh per test @TempDir static Path sharedDir; // once per class } ``` The field does **not** have to be public — Jupiter uses reflection and makes it accessible — but it must not be `final`, because Jupiter has to assign it. ## Supported types Only `java.nio.file.Path` and `java.io.File` are supported. Ask for a `String`, a `Files` handle, or anything else and Jupiter fails the test with an `ExtensionConfigurationException` saying the annotated element must be of type `Path` or `File`. `Path` is the better default: it is the modern NIO API, it composes with `Files.*` helpers, and — importantly — it is what lets a custom `TempDirFactory` hand you a directory on a non-default filesystem such as an in-memory one. A `File` can only ever live on the real default filesystem. ## Where the directory comes from and what happens to it By default Jupiter creates the directory with `Files.createTempDirectory` under the JVM's temp location (`java.io.tmpdir`), using a `junit`-prefixed random name, so two concurrently running declarations never share a path. After the scope ends — after the test method for a method-level declaration, after the class for a class-level one — Jupiter walks the tree and deletes it **recursively**: files, nested directories, everything. You never write cleanup code for content inside the directory. Each separate `@TempDir` declaration gets a **separate** directory. If a test method takes two `@TempDir Path` parameters, you get two distinct directories, which is exactly what you want when you are testing a copy or a sync between two locations. ## Typical usage patterns - Resolve child paths from the injected root: `dir.resolve("input.json")` rather than concatenating strings. - Pass the root into the code under test as configuration (an output directory, a cache root), which usually pushes you towards making that path injectable in production code too — a small design win. - Combine with `@BeforeEach` seeding: take `@TempDir Path dir` as a `@BeforeEach` parameter, write fixture files into it, and store it in a field. ## Common mistakes Assuming the directory persists across tests when the field is non-static — it does not; each test gets a fresh one. Assuming the path is stable or predictable — it is randomized, so never hardcode it or assert on its name. Making the field `final` or of the wrong type. Writing extra deletion code in `@AfterEach`, which is redundant and can itself fail. And leaving file handles open at the end of the test, which is harmless on Linux but makes the automatic deletion fail on Windows. ## Why interviewers ask it It is a five-second answer that separates people who have actually written filesystem tests from people who have only read about them, and it opens naturally into deeper follow-ups: static versus instance scope, cleanup modes, and what happens when deletion fails.

  • Can a @TempDir field be final, and does it have to be public?
    It must not be final, because Jupiter assigns the value reflectively after constructing the test instance; a final field would already be initialized and cannot be written. Visibility does not matter — package-private, protected or private all work, since Jupiter calls setAccessible. The idiomatic declaration is a package-private or private non-final field.
  • If a test method declares two @TempDir parameters, do they point at the same directory?
    No. Every @TempDir declaration is resolved independently and gets its own freshly created directory, so two parameters give you two distinct paths. That is what makes copy, move and sync tests easy to write. Sharing only happens when you deliberately use a static field or a class-level declaration.

It is a hotel room rather than your own apartment: you get a clean, randomly-numbered room for the stay, you can make any mess you like, and housekeeping empties it completely when you check out.

saying these in an interview costs you the question

  • Claiming @TempDir accepts a String path or any type other than Path/File
  • Believing a non-static @TempDir field keeps the same directory across all test methods
  • Adding manual recursive-delete code in @AfterEach on top of @TempDir
  • Hardcoding or asserting on the generated directory name, which is randomized
  • Thinking you must add an extra dependency or @ExtendWith — the extension is built into Jupiter

context

open as a page

JUnit Jupiter's @TempDir annotation accepts a cleanup attribute with the values ALWAYS, ON_SUCCESS and NEVER. What does each do, which is the default, and why would you change it?

level: middleimportance: should knowfreq 28%

basics

~20 s

ALWAYS (the default) deletes the directory when its scope ends whatever the outcome. ON_SUCCESS deletes it only if the test passed, keeping the files for inspection when it failed. NEVER always keeps it. You can also set the default globally with the junit.jupiter.tempdir.cleanup.mode.default configuration parameter.

open as a page

In a JUnit 5 test class, what is the difference in lifetime between a temp directory injected through a non-static @TempDir field and one injected through a static @TempDir field, and when would you accept the shared one?

level: middleimportance: should knowfreq 38%

basics

~20 s

A non-static @TempDir field is created fresh before each test and deleted after it. A static field (or a @BeforeAll parameter) gives one directory for the whole class, created before the first test and deleted after the last, so all tests share it and its accumulated contents.

open as a page

A test suite that injects directories with JUnit's @TempDir passes on Linux but fails on Windows CI with an IOException saying the temp directory could not be deleted. How do you diagnose and fix that?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Almost always an unclosed file handle: Windows refuses to delete files that are still open, Linux does not. Find the stream, channel, ZipFile, watch service or memory-mapped buffer the test or the code under test leaves open and close it, normally with try-with-resources. Read-only file attributes are the other cause.

open as a page

How would you make JUnit Jupiter's @TempDir hand out directories from somewhere other than the default operating-system temp location — for example an in-memory filesystem or a fixed parent directory?

level: seniorimportance: nice to knowfreq 14%

basics

~20 s

Implement JUnit's TempDirFactory interface, whose createTempDirectory method returns the Path to use, and point at it with @TempDir(factory = MyFactory.class), or set it for the whole suite with the junit.jupiter.tempdir.factory.default configuration parameter. The injected field must be a Path, not a File.

open as a page