Привет, Хабр!

Сегодня разберемся с @TempDir — мощным, но часто недооценённым инструментом JUnit 5 для работы с временными файлами и директориями в тестах.

Зачем вообще нужны временные каталоги?

Тесты, которые пишут на диск, имеют неприятное свойство:

  1. Засоряют /tmp, если вы забыли подчистить.

  2. Ломаются в CI, когда два воркера пытаются писать в один и тот же файл.

  3. Падают на Windows, потому что «файл используется другим процессом».

@TempDir решает все три проблемы: JUnit создает уникальную папку, инжектирует вам Path/File, а потом — по дефолту вычищает за собой.

class ReportServiceTest {

    @Test
    void generatesCsv(@TempDir Path temp) throws IOException {
        Path report = temp.resolve("users.csv");
        new ReportService().writeCsv(report);

        assertLinesMatch(
            List.of("id,name", "1,Alice", "2,Bob"),
            Files.readAllLines(report)
        );
    }
}

Секундное дело: получили каталог, передали его в прод‑код, проверили результат, забыли. JUnit сам удалит папку после теста.

Три лица cleanup: ALWAYS, ON_SUCCESS, NEVER

У @TempDir есть параметр cleanup, который понимает три значения:

Режим

Что делает

Когда нужен

ALWAYS (дефолт)

чистит всегда

99% юз‑кейсов

ON_SUCCESS

чистит только при зеленом тесте

дебаг фейлов, flaky‑ад

NEVER

ничего не чистит

редкие интеграционные сценарии

@Test
void debugFailure(
        @TempDir(cleanup = ON_SUCCESS) Path temp
) throws Exception {
    // ...
}

Провалился ассерт — папка осталась, можно руками залезть и посмотреть артефакты.

Кстати, лобально настроить режим можно в junit-platform.properties
junit.jupiter.tempdir.cleanup.mode.default=ON_SUCCESS

С JUnit 5.11+ ON_SUCCESS наконец учитывает nested‑тесты и class‑level @TempDir.

CSV-репорты

Допустим, есть сервис, который принимает список пользователей и складывает отчет в Path.

public final class ReportService {

    public void writeCsv(Path target) {
        try (BufferedWriter w = Files.newBufferedWriter(target)) {
            w.write("id,name\n");
            users().forEach(u -> IoUtil.safeWrite(w, "%d,%s\n", u.id(), u.name()));
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /* … business logic … */
}

Тест с @TempDir — элементарен (см. первый пример). Не нужно париться о коллизиях имен: каталог уникальный.

Тестируем код, который пишет в OutputStream

Когда прод‑код получает OutputStream, еще проще: подсовываем ByteArrayOutputStream.

@Test
void writesXmlToStream() {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    new XmlWriter(out).writeString("foo");

    assertThat(out.toString(UTF_8)).isEqualTo("<tag>foo</tag>");
}

Без диска, без прав на файлы, тест бежит в микросекунды.

Jimfs — in-memory FS как тестовый дабл

Но как быть, если код жестко завязан на Path и дергает тонну Files.*? В бой пускаем [Jimfs] — файловую систему в памяти.

FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
Path tmp = fs.getPath("/tmp");           // mkdir -p /tmp
Files.createDirectories(tmp);
new ReportService().writeCsv(tmp.resolve("users.csv"));

С JUnit 5.10 появился SPI TempDirFactory. Пишем фабрику:

class JimfsTempDirFactory implements TempDirFactory {
    private final FileSystem fs = Jimfs.newFileSystem(Configuration.unix());

    @Override
    public Path createTempDirectory(Object context) throws IOException {
        return Files.createTempDirectory(fs.getPath("/"), "junit");
    }
}

И используем:

@Test
void reportInMemory(
        @TempDir(factory = JimfsTempDirFactory.class) Path dir
) { /* ... */ }

Теперь тесты не трогают реальную FS вообще.

Можно прописать junit.jupiter.tempdir.factory.default=...JimfsTempDirFactory, и все @TempDir в проекте автоматически будут Jimfs‑овые.

Прочие моменты

Symlink‑ловушки. JUnit 5.12+ предупреждает, если в tmp‑папке есть симлинк наружу: ссылка удаляется, цель — нет. Безопаснее, чем тупое rm -rf.

Параллельные тесты. Каждый @TempDir уникальный — можно смело включать junit.jupiter.execution.parallel.enabled=true.

Windows‑file‑locking. Не держите FileChannel открытым дольше, чем нужно. Даже @TempDir тут бессилен.

Когда Jimfs не подходит

  • Тестируете логику, завязанную на ACL/xattrs.

  • Нужно проверить work‑with‑network‑share.

  • Используете JNI‑вызывающую fchmod() — Jimfs это не эмулирует.

В этих случаях берите настоящий диск + @TempDir(cleanup = NEVER) и подчищайте руками в @AfterEach.


Если вы все еще вручную создаёте временные файлы в тестах, крутите deleteOnExit() и надеетесь, что CI сам как‑нибудь разберётся — пора остановиться. @TempDir в JUnit закрывает весь этот зоопарк одним аннотационным выстрелом: уникальные директории, гарантированный cleanup, встроенная поддержка in‑memory файловых систем, а с 5.10+ — еще и тонкая настройка поведения через фабрики.

Не забывайте про cleanup = ON_SUCCESS, когда отлаживаете сложные фейлы, и обязательно держите JUnit в актуальной версии: начиная с 5.11–5.12, пофикшены баги с nested‑тестами, символическими ссылками и ранним удалением директорий. Также имеет смысл завести свою TempDirFactory. Потратите один вечер — сэкономите сотни в будущем.

В целом, любой код, взаимодействующий с файловой системой, должен быть изолирован в тестах. @TempDir создает, передает, чистит. Все, что остается вам — писать логику, не думая о побочных эффектах.


Если вы всерьёз работаете с файловой системой в Java — с вас никто не требует помнить наизусть все особенности @TempDir, Jimfs, FileChannel и прочих тонкостей. Но понимать, где и как их применять, — уже почти обязательный минимум для современного инженера.

Если вы хотите разобраться глубже, укрепить архитектуру и писать тесты, которые не ломаются на CI, — загляните в наш курс «Java Developer. Basic». Программа основана на реальных кейсах, преподаватели — практики из индустрии.

Кроме того, у нас открыт каталог курсов OTUS, где вы можете подобрать направление под свой стек и задачи.

Также рекомендуем заглянуть в календарь открытых уроков. Там всегда можно найти что‑то полезное и актуальное — и бесплатно.

Комментарии (0)