Самый типичный побочный эффект – это взаимодействие с файлами. В основном это либо чтение файлов, либо запись в них. В этом уроке вы узнаете, как справиться с такими побочными эффектами при тестировании.
С чтением разбираться значительно проще, поэтому с него и начнем.
Чтение файлов
В большинстве случаев чтение файлов не доставляет особых проблем. Оно ничего не изменяет и выполняется локально, в отличие от сетевых запросов. Это значит, что при наличии необходимого файла и нужных прав, вероятность случайных ошибок крайне низка.
При тестировании функций, читающих файлы, должно выполняться ровно одно условие — функция должна позволять менять путь до файла. В таком случае достаточно создать файл нужной структуры в фикстурах:
# Функция читает файл со списком пользователей системы и возвращает их имена
# В Linux это файл /etc/passwd
user_names = read_user_names(path='/etc/passwd')
В тестах читать /etc/passwd нельзя, потому что содержимое этого файла зависит от окружения, в котором запущены тесты. Для тестирования нужно создать файл аналогичной структуры в фикстурах и указать его при запуске функции:
def test_read_user_names():
# fixtures/passwd
passwd_path = 'fixtures/passwd'
user_names = read_user_names(passwd_path);
assert user_names == # Ожидаемый результат
Запись файлов
С записью файлов уже сложнее. Главная проблема — это отсутствие гарантированной идемпотентности. Это значит, что повторный вызов функции, записывающей файлы, может вести себя не как первый вызов. Например, он может завершаться с ошибкой или приводить к другим результатам.
Разберемся, почему так происходит. Представьте, что мы пишем тесты на функцию log(message)
, которая дописывает все переданные в нее сообщения в файл:
log = Logger('development.log')
log('first message');
# Смотрим содержимое файла
# cat development.log
# first message
log('second message')
# cat development.log
# first message
# second message
Это значит, что каждый запуск тестов будет немного другим. При первом запуске тестов создается файл для хранения логов. Затем он начнет заполняться. Это приводит к целой пачке проблем:
- Скорее всего, создание файла внутри этой функции — это особый случай, который нужно тестировать отдельно. Повторные запуски тестов перестанут проверять эту ситуацию
- Будет сложно написать предсказуемый тест. Придется дополнительно придумывать неочевидные схемы — например, проверять только последнюю строку в файле. Такой подход понижает качество теста
- Это не особенно критичная проблема, но в процессе запуска тестов появляется файл, который постоянно растет в размерах
При правильной организации тестов каждый тест работает в идентичном окружении на каждом запуске. Чтобы добиться такой организации, можно удалять файл после выполнения каждого теста. В большинстве ситуаций такое решение работает нормально, но все же не во всех.
Выполнение кода тестов — это не атомарная операция. Нет никакой гарантии, что код после тестов выполнится. Есть много причин, по которым этого может не произойти — от внезапного отключения электроэнергии до ошибок в самом Pytest:
import pytest
import os
filepath = 'some/file/path'
# Будет вызываться для каждого теста
@pytest.fixture(autouse=True)
def clean_file():
if os.path.isfile(filepath):
os.remove(filepath)
yield
Даже ручное удаление файлов — это сложное решение, которое требует постоянного контроля происходящего. Придется все время об этом помнить. Чтобы не приходилось этим заниматься, программисты опираются на особенности работы файловой системы в Linux — возможность создания временных директорий.
Временные директории в Python можно создавать двумя способами:
- С помощью стандартной библиотеки tempfile
- С помощью фикстуры tmp_path фреймворка pytest или любых других средств тестового фреймворка
После применения создается временная директория с уникальным именем, затем все действия происходят внутри нее. Каждое создание такой директории гарантирует уникальное имя. Удалять такие директории не нужно, потому что операционная система сама подчищает их:
def test_create_file(tmp_path):
d = tmp_path / "sub"
d.mkdir()
p = d / "hello.txt"
p.write_text('content')
assert p.read_text() == 'content'
assert len(list(tmp_path.iterdir())) == 1
assert 0
Виртуальная файловая система
Это еще один способ тестировать код, работающий с файловыми системами.
С помощью специальной библиотеки во время тестов создается виртуальная файловая система. Она автоматически подменяет реальную файловую систему для всех модулей, работающих с файловой системой.
Это значит, что тестируемую функцию трогать не надо. Эта функция продолжает думать, что она работает с реальным диском. Вся конфигурация при этом задается снаружи:
# Этот способ дает идемпотентность из коробки
def my_fakefs_test(fs):
# "fs" – фикстура для управления виртуальной файловой системой
fs.create_file('/var/data/xx1.txt')
assert os.path.exists('/var/data/xx1.txt')
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.