Привет, Хабр!
Сегодня разберемся с @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, который понимает три значения:
|
Режим |
Что делает |
Когда нужен |
|---|---|---|
|
|
чистит всегда |
99% юз‑кейсов |
|
|
чистит только при зеленом тесте |
дебаг фейлов, flaky‑ад |
|
|
ничего не чистит |
редкие интеграционные сценарии |
@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, где вы можете подобрать направление под свой стек и задачи.
Также рекомендуем заглянуть в календарь открытых уроков. Там всегда можно найти что‑то полезное и актуальное — и бесплатно.
ссылка на оригинал статьи https://habr.com/ru/articles/920200/
Добавить комментарий