
Как тестировать работу с файлами через tmp_path в pytest


Когда тест читает файл из настоящей папки проекта или из домашнего каталога, он начинает зависеть от чужих данных, остатков прошлых запусков и настроек конкретного компьютера. Из-за этого один и тот же тест может пройти у автора и упасть у коллеги. Для проверки функции, которая читает две строки из текстового файла, удобнее каждый раз создавать отдельные временные файлы и убирать связь с реальной файловой системой пользователя.
В pytest для этого подходит фикстура tmp_path. Она передает тесту временную директорию как объект pathlib.Path, поэтому можно работать с путями обычными операциями /, write_text() и read_text(). Ниже разберем узкую задачу: как проверить обычный файл, пустой файл и отсутствующий путь, не трогая реальные папки.
Почему тесту нужна отдельная папка
У теста на работу с файлами есть две слабые точки: входные данные и состояние окружения. Если вы храните тестовый файл рядом с кодом, кто-то может случайно изменить его содержимое, переименовать каталог или закоммитить лишнюю строку. Если тест пишет во временную папку операционной системы сам по себе, пути могут пересекаться между запусками. В результате вы проверяете не только свою функцию, но и то, что вокруг нее оказалось на диске.
tmp_path решает обе проблемы сразу. Каждый тест получает собственную временную папку, а значит, сам создает исходные данные и сам же ограничивает область изменений. Это особенно полезно для негативных сценариев: отсутствующий файл должен отсутствовать по-настоящему, а пустой файл должен быть именно пустым, а не «почти пустым» после прошлого запуска.
Еще один практический плюс — понятность. Когда весь сценарий создается внутри теста, читатель видит причину падения прямо в коде: здесь создан файл с тремя строками, здесь пустой, здесь путь вообще не создавался. Для учебной задачи это важнее, чем попытка переиспользовать один общий набор файлов на весь проект.
Функция, которую мы проверяем
Чтобы тесты были осмысленными, сначала зафиксируем поведение функции. В этом разборе она получает путь к текстовому файлу, читает его целиком, берет первые две строки и возвращает их как кортеж. Если строк меньше двух, функция поднимает ValueError. Если файла нет, Python сам поднимет FileNotFoundError при чтении.
Учебный пример. Среда: Python 3.11+, pytest, код использует
pathlib.Path. Исходные данные для трех тестов условные: файлinput.txtсо строкамиalpha,beta,gamma; пустой файлempty.txt; путьmissing.txt, который не создается.
from pathlib import Path
def read_two_lines(path: Path) -> tuple[str, str]:
lines = Path(path).read_text(encoding="utf-8").splitlines()
if len(lines) < 2:
raise ValueError("need at least two lines")
return lines[0], lines[1]
Почему здесь выбран splitlines(), а не разбиение по "\n"? Так проще избежать лишней пустой строки на конце, если файл заканчивается переводом строки. Для нашего сценария это не меняет идею теста, но делает пример аккуратнее на граничном случае с завершающим переносом.
| Сценарий | Что создаем | Ожидаемый результат |
|---|---|---|
| Обычный файл | Три строки текста | Кортеж из первых двух строк |
| Пустой файл | Файл с пустым содержимым | ValueError |
| Отсутствующий путь | Ничего не создаем | FileNotFoundError |
Как подготовить файлы через tmp_path
Смысл tmp_path не в том, что pytest «где-то хранит временные файлы», а в том, что тест получает готовый объект пути и работает с ним как с обычной директорией. Поэтому подготовка данных становится частью самого теста. Вы не ищете заранее созданные ресурсы, а собираете минимальный вход прямо перед вызовом функции.
Для обычного файла достаточно склеить путь и записать текст. Для пустого файла — записать пустую строку. Для отсутствующего пути не нужно делать ничего: просто сформируйте имя файла внутри tmp_path, но не создавайте его. Это важный момент: негативный сценарий должен быть таким же явным, как и позитивный.
Чтобы пример запускался без лишних исправлений, в тестовом модуле нужен импорт pytest. Иначе блоки с pytest.raises(...) закончатся NameError раньше, чем вы проверите поведение функции.
import pytest
def test_reads_first_two_lines(tmp_path):
file_path = tmp_path / "input.txt"
file_path.write_text("alpha\nbeta\ngamma\n", encoding="utf-8")
assert read_two_lines(file_path) == ("alpha", "beta")
def test_empty_file_raises_value_error(tmp_path):
file_path = tmp_path / "empty.txt"
file_path.write_text("", encoding="utf-8")
with pytest.raises(ValueError, match="need at least two lines"):
read_two_lines(file_path)
def test_missing_file_raises_file_not_found(tmp_path):
file_path = tmp_path / "missing.txt"
with pytest.raises(FileNotFoundError):
read_two_lines(file_path)
Если хочется сделать структуру чуть ближе к реальному проекту, можно создать подкаталог через (tmp_path / "data").mkdir() и положить файл туда. Но в этой задаче это не обязательно: важно не дерево каталогов, а то, что каждый сценарий сам определяет свои данные и не обращается к внешним папкам.
Как проверить результат и ожидаемую ошибку
У файловых тестов часто встречается перекос: позитивный сценарий проверяют подробно, а негативные — на уровне «упало и ладно». Лучше фиксировать ожидаемое поведение так же четко, как и успешный результат. Для обычного файла мы сравниваем точный кортеж ("alpha", "beta"). Это показывает и порядок строк, и то, что третья строка не участвует в ответе.
Для пустого файла нам нужно проверить не просто наличие исключения, а его смысл. Блок pytest.raises(ValueError, match="need at least two lines") защищает от ситуации, когда функция падает по другой причине, например из-за опечатки в коде. Если сообщение меняется, тест подскажет, что поменялся и контракт функции.
С отсутствующим путем, наоборот, полезно не подменять поведение без необходимости. Если ваша функция не обязана превращать системную ошибку в свою собственную, достаточно проверить FileNotFoundError. Так тест остается честным: файл реально не существует, а ошибка приходит из стандартного механизма чтения.
Практический результат такого набора тестов легко проверить. В первом сценарии функция возвращает две ожидаемые строки. Во втором и третьем сценариях тесты подтверждают два разных типа проблем: неверное содержимое и отсутствие файла. Это уже не один «общий тест на чтение файла», а три независимые проверки с разными причинами падения.
Повторяемость и независимость запусков
Главная польза такого подхода становится заметна не в первом запуске, а в пятом и десятом. Когда тесты каждый раз создают свои файлы внутри tmp_path, вам не нужно очищать рабочий каталог, вспоминать, какой файл остался после отладки, или бояться, что локальная папка пользователя будет затронута. Каждый тест запускается в своей области и не наследует мусор от соседей.
Это помогает и при отладке. В документации pytest отмечено, что временные директории создаются автоматически, а последние несколько запусков по умолчанию сохраняются. На практике это удобно, когда нужно посмотреть, что именно тест записал на диск перед падением. Но для обычной ежедневной работы важнее другое: вы пишете тесты так, будто на машине нет никаких заранее подготовленных файлов, и именно поэтому они стабильнее.
Если хотите проверить себя, задайте четыре вопроса к своему тесту. Создает ли он все нужные файлы сам? Есть ли отдельная проверка для пустого файла? Проверяется ли отсутствующий путь без обращения к реальной папке? Можно ли запустить тесты повторно без ручной уборки? Если на все четыре ответа «да», то вы уже используете tmp_path по назначению, а не просто меняете один абсолютный путь на другой.
Если хотите добрать практику по теме, в карточке курса есть программа с блоком по pytest: разработка и запуск тестов, параметризация, теги и фикстуры. Для темы с tmp_path это полезное продолжение: сначала научиться изолировать файловые сценарии, затем — выносить подготовку данных в свои фикстуры.





















