- Регистрация
- 23 Август 2023
- Сообщения
- 2 962
- Лучшие ответы
- 0
- Реакции
- 0
- Баллы
- 51
Offline

Привет, Хабр!
Сегодня разберемся с @TempDir — мощным, но часто недооценённым инструментом JUnit 5 для работы с временными файлами и директориями в тестах.
Зачем вообще нужны временные каталоги?
Тесты, которые пишут на диск, имеют неприятное свойство:
Засоряют /tmp, если вы забыли подчистить.
Ломаются в CI, когда два воркера пытаются писать в один и тот же файл.
Падают на 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, где вы можете подобрать направление под свой стек и задачи.
Также рекомендуем заглянуть в календарь открытых уроков. Там всегда можно найти что‑то полезное и актуальное — и бесплатно.