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?
answer
- @TempDir from org.junit.jupiter.api.io
- Path or File only
- field or parameter injection
- recursive delete afterwards
- one directory per declaration
basics
~20 sAnnotate 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 sJUnit 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 linesimport 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
Know the annotation name, that Path and File are the accepted types, and that the directory is created empty and deleted recursively afterwards.
Add the injection points (field, constructor, test and lifecycle method parameters), the non-final requirement, and that each declaration yields its own directory.
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.
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