skip to content

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%

answer

  1. non-static field = per test
  2. static field / @BeforeAll param = per class
  3. shared dir = order-dependent + parallel race
  4. share only for expensive read-only fixtures
  5. deleted after @AfterEach / after @AfterAll

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.

solid answer

~50 s

Scope follows the declaration site. A **non-static** `@TempDir` field, or a parameter on a `@Test`/`@BeforeEach` method, is **method-scoped**: created before each test, deleted after it, so tests never see each other's files. A **static** field, or a parameter on `@BeforeAll`, is **class-scoped**: one directory created before the class's first test and deleted after its last, shared by every test in the class. Default to method scope. It is the isolated, order-independent option and it is safe under parallel execution. Reach for class scope only when populating the directory is genuinely expensive and read-only afterwards — unpacking a large fixture archive, materialising a big dataset once. Then treat it as immutable: if tests write into a shared directory you have reintroduced inter-test coupling, made results order-dependent, and created a race if the class runs with parallel methods.

code

java · 29 lines
java
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.*;

class CorpusTest {

    @TempDir
    static Path corpus;      // created once, deleted after the last test

    @TempDir
    Path workDir;            // fresh for every test

    @BeforeAll
    static void unpackOnce() throws IOException {
        Files.writeString(corpus.resolve("big.txt"), "expensive fixture");
    }

    @Test
    void readsFixtureWithoutMutatingIt() throws IOException {
        String text = Files.readString(corpus.resolve("big.txt"));
        Files.writeString(workDir.resolve("out.txt"), text.toUpperCase());
    }

    @Test
    void alsoReadsTheSameFixture() throws IOException {
        Assertions.assertTrue(Files.exists(corpus.resolve("big.txt")));
    }
}

go deeper

for a junior

Know the headline rule: non-static equals one directory per test, static equals one per class.

for a middle

Explain the declaration sites that select each scope and why per-test isolation is the default, plus deletion happening after @AfterEach / @AfterAll.

for a senior

Lead with the coupling and parallel-execution risks of a shared directory and give the narrow justification: an expensive, immutable fixture, populated once in @BeforeAll.

for a principal

Talk about it as a test-suite property — order independence and parallelizability are the assets you are protecting; shared mutable fixtures are a decision to be made explicitly and documented, not a default.

## Scope is decided by where you declare it JUnit Jupiter derives the lifetime of a `@TempDir` from the declaration site, not from a mode attribute. **Method scope (per test).** A non-static `@TempDir` field, a `@TempDir` parameter on a `@Test`, `@ParameterizedTest` or `@RepeatedTest` method, or a `@TempDir` parameter on `@BeforeEach`/`@AfterEach`. Jupiter creates the directory before the test's lifecycle begins and deletes it after the test's lifecycle ends. Every test method therefore sees an empty directory at a different path. **Class scope (per container).** A `static @TempDir` field, or a `@TempDir` parameter on a `@BeforeAll`/`@AfterAll` method. Jupiter creates the directory once before the class's first test and deletes it after the last one. All tests in the class see the same path and, crucially, whatever the earlier tests left in it. A subtlety worth knowing: with the default per-method test-instance lifecycle, a `@BeforeEach` parameter and a non-static field in the same class are two *separate* declarations, hence two *separate* directories. If you want the `@BeforeEach` method to seed the same directory the test uses, seed the field, do not take a second parameter. ## Why method scope is the default choice Isolation is the entire point of a temp directory. With method scope you get four properties for free: 1. **Order independence.** No test can be affected by files another test wrote, so shuffling execution order cannot change results. 2. **Parallel safety.** Under `junit.jupiter.execution.parallel.enabled`, concurrently running methods write to distinct paths, so there is no interference and no need for resource locks. 3. **Diagnosability.** When one test fails, the directory contains only that test's output. 4. **Bounded growth.** Nothing accumulates across a long class. Class scope forfeits all four. Two tests writing `output.txt` into a shared directory will clobber each other; one test asserting "the directory contains exactly one file" starts failing the moment a sibling test writes a second one; and running the class in parallel turns those collisions into a genuine race condition that reproduces on CI and not locally. ## When class scope is justified The legitimate case is an expensive, immutable fixture. Examples: unzipping a 200 MB corpus, generating a synthetic dataset that takes seconds to build, or laying out a directory tree that a dozen read-only tests then traverse. Paying that cost once instead of forty times is a real saving and there is no isolation cost as long as the directory is treated as read-only after setup. The discipline that makes it safe: - Populate it exactly once, in `@BeforeAll`, and never write to it from a test. - If a test genuinely needs to write, give that test its own method-scoped `@TempDir` and copy in what it needs; a class can declare both a static and a non-static `@TempDir`. - Be explicit in a comment or naming (`sharedFixtureDir` vs `workDir`) so the next reader does not casually add a write. ## Interaction with test-class instance lifecycle Under the default per-method instance lifecycle, class-level hooks and therefore class-level `@TempDir` declarations must be static — the same static requirement that applies to `@BeforeAll`. If the class is annotated with `@TestInstance(Lifecycle.PER_CLASS)` the static requirement is lifted and a class-level directory can be declared on a non-static `@BeforeAll` parameter, but the *scoping rule itself* is unchanged: `@BeforeAll` means class scope, `@BeforeEach`/`@Test` means method scope. ## Cleanup at the boundary Deletion happens when the owning scope ends. A method-scoped directory is deleted after the test's `@AfterEach` methods have run, so `@AfterEach` code can still inspect it. A class-scoped directory is deleted after `@AfterAll`. That ordering is what lets you write an `@AfterEach` that dumps the directory listing on failure. ## How to answer in an interview State the two scopes and their triggers, say that method scope is the default because it buys isolation and parallel safety, and name the one legitimate reason to share — an expensive, read-only fixture — plus the discipline required to keep sharing safe. Mentioning the parallel-execution race is what moves the answer from textbook to practical.

  • You have a static @TempDir and the class runs with parallel test methods enabled. What can go wrong?
    Every method writes into the same directory concurrently, so tests can overwrite each other's files, observe files they did not create, and fail assertions about directory contents non-deterministically. The failures are order- and timing-dependent and typically only reproduce under CI load. The fixes are to make the shared directory strictly read-only after @BeforeAll, or to switch to method-scoped directories.
  • When exactly is a method-scoped @TempDir deleted relative to @AfterEach?
    After all @AfterEach methods for that test have completed. That ordering is deliberate: teardown code can still list the directory, copy artefacts out for diagnostics, or close resources that live inside it. A class-scoped directory is likewise deleted after @AfterAll.

saying these in an interview costs you the question

  • Saying a non-static @TempDir field is shared by all tests in the class
  • Using a static @TempDir purely to 'save time' when setup is cheap, then writing to it from tests
  • Assuming a @BeforeEach @TempDir parameter and a non-static @TempDir field are the same directory
  • Claiming the directory is deleted before @AfterEach runs
  • Sharing a writable temp directory across methods while also enabling parallel execution

context