Привет, Хабр!
Сегодня разберемся с @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, где вы можете подобрать направление под свой стек и задачи.
Также рекомендуем заглянуть в календарь открытых уроков. Там всегда можно найти что‑то полезное и актуальное — и бесплатно.