skip to content

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%

answer

  1. TempDirFactory.createTempDirectory + close()
  2. @TempDir(factory = X.class)
  3. junit.jupiter.tempdir.factory.default
  4. Jimfs = in-memory, Path only
  5. File cannot leave the default filesystem

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.

solid answer

~40 s

Jupiter lets you replace the directory-creation strategy with a `TempDirFactory`. Implement `createTempDirectory(AnnotatedElementContext, ExtensionContext)` to return the `Path` for that declaration, and optionally `close()` to release factory-owned resources after Jupiter has deleted the directory. Select it per declaration: ```java @TempDir(factory = JimfsTempDirFactory.class) Path dir; ``` or globally with the configuration parameter `junit.jupiter.tempdir.factory.default=com.example.JimfsTempDirFactory`. Typical motivations: an **in-memory filesystem** (Jimfs) for speed and to guarantee tests never touch real disk; a **fixed parent directory** on a volume with space or the right permissions when `java.io.tmpdir` is unsuitable on CI agents; a filesystem configured for a specific OS flavour so path-separator and case-sensitivity behaviour is deterministic. The key constraint: a non-default filesystem only works when you inject a `Path`. `java.io.File` can only represent the default filesystem, so a factory returning a Jimfs path fails for `File` declarations.

code

java · 21 lines
java
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.io.TempDirFactory;
import org.junit.jupiter.api.extension.AnnotatedElementContext;
import java.io.IOException;
import java.nio.file.*;

class BuildDirTempDirFactory implements TempDirFactory {
    @Override
    public Path createTempDirectory(AnnotatedElementContext element,
                                    ExtensionContext context) throws IOException {
        Path parent = Files.createDirectories(Path.of("build", "test-temp"));
        String name = context.getRequiredTestClass().getSimpleName();
        return Files.createTempDirectory(parent, name + "-");
    }
}

class ExportTest {
    @TempDir(factory = BuildDirTempDirFactory.class)
    Path dir;
}

go deeper

for a junior

It is enough to know the default location can be customised and that this is not everyday material.

for a middle

Name TempDirFactory, the factory attribute and the global configuration parameter, and the Path-not-File constraint.

for a senior

Give concrete motivations — in-memory speed and hermeticity, deterministic filesystem semantics, awkward CI temp volumes — implement the interface confidently, and flag that production code must be Path-based.

for a principal

Treat it as a suite-wide policy lever: standardising temp-directory placement across agents or moving I/O-heavy suites in-memory, weighed against the coupling it imposes on production code's filesystem abstraction.

## Why replace the default The default strategy — `Files.createTempDirectory` under `java.io.tmpdir` — is right most of the time, but there are real reasons to override it. - **Speed and hermeticity.** An in-memory filesystem such as Google's Jimfs removes disk I/O entirely and guarantees a test cannot leave anything behind, even if cleanup fails. - **Deterministic filesystem semantics.** Jimfs can be configured to emulate Unix, Windows or macOS conventions, so tests of path handling, case sensitivity or separators behave the same on every developer machine. - **Environment constraints.** Some CI agents have a tiny or noexec `/tmp`, or a temp path so long that Windows path-length limits bite. Directing temp directories to a known volume solves it centrally instead of per test. - **Observability.** A factory can create directories under a build-output folder so a CI job can archive them as artefacts. ## The interface `org.junit.jupiter.api.io.TempDirFactory` has one method to implement and one optional: ```java Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext) throws Exception; default void close() throws IOException { } ``` `createTempDirectory` is called once per `@TempDir` declaration and returns the directory to inject; the implementation is responsible for actually creating it. The two context arguments let you vary behaviour: `elementContext` exposes the annotated field or parameter (so you can read your own annotations from it), and `extensionContext` exposes the test or class being executed (so you can name directories after the test, which is a genuinely useful trick for archived artefacts). `close()` is invoked after Jupiter has performed its own cleanup of the directory. That is where an in-memory-filesystem factory closes the `FileSystem` instance it created. Jupiter instantiates a new factory instance per declaration, so per-instance state is safe. ## A Jimfs factory ```java class JimfsTempDirFactory implements TempDirFactory { private FileSystem fileSystem; @Override public Path createTempDirectory(AnnotatedElementContext e, ExtensionContext c) throws IOException { fileSystem = Jimfs.newFileSystem(Configuration.unix()); return Files.createTempDirectory(fileSystem.getPath("/"), "junit"); } @Override public void close() throws IOException { fileSystem.close(); } } ``` ## Selecting the factory Per declaration: `@TempDir(factory = JimfsTempDirFactory.class)`. Suite-wide: the configuration parameter ``` junit.jupiter.tempdir.factory.default = com.example.JimfsTempDirFactory ``` read from `junit-platform.properties` on the test classpath, a system property, or launcher-supplied parameters. An explicit `factory` attribute overrides the global default. The sentinel `TempDirFactory.Standard` represents the built-in behaviour. ## Constraints and gotchas 1. **`Path` only for non-default filesystems.** `java.io.File` is hardwired to the default filesystem; a factory returning a Jimfs path cannot be injected into a `File`, and Jupiter fails the declaration. Since a factory is usually adopted precisely to get off the real disk, standardise on `Path`. 2. **Your production code must be filesystem-agnostic.** Code that calls `new File(...)`, `FileInputStream`, or `Paths.get(...)` internally will ignore the in-memory filesystem. Only code written against `Path`, `Files` and the `Path` handed to it works. In practice this is a design constraint the factory imposes on the code under test — often a good one, sometimes a dealbreaker. 3. **Cleanup still applies.** Jupiter deletes the returned directory according to the `CleanupMode` as usual, then calls `close()`. A factory does not opt out of cleanup. 4. **Third-party dependency.** Jimfs is not part of JUnit; it is a separate test-scoped dependency. ## When not to bother For ordinary tests the default is fine and adding a factory is complexity with no payoff. The honest interview answer names the mechanism, gives one or two real motivations, and is clear that it is a specialist tool — reached for when disk is a genuine problem (speed, hermeticity, agent constraints) or when filesystem semantics must be pinned, not by default.

  • What breaks if you point @TempDir at an in-memory filesystem but the code under test uses java.io.File internally?
    It silently escapes the in-memory filesystem or fails outright. java.io.File and FileInputStream/FileOutputStream can only address the default filesystem, so code built on them cannot see a Jimfs path; converting with path.toFile() throws UnsupportedOperationException for a non-default filesystem. Using a factory therefore requires the production code to be written against Path and Files, which is a real constraint worth checking before adopting one.
  • When is TempDirFactory.close() called relative to the directory's deletion?
    After Jupiter has finished its own cleanup of the temp directory for that declaration, honouring the configured CleanupMode. That ordering lets a factory that created a FileSystem instance close it once nothing needs the directory any more. A new factory instance is created per declaration, so holding per-instance state such as the FileSystem is safe.

saying these in an interview costs you the question

  • Claiming you must write a whole custom extension because @TempDir is not customisable
  • Injecting into java.io.File while expecting an in-memory filesystem to be used
  • Assuming a factory disables cleanup — CleanupMode still applies
  • Thinking the factory is chosen only globally, or only per annotation, when both are supported
  • Reaching for a custom factory by default rather than for a specific speed, hermeticity or environment reason

context