Зарегистрируйтесь, чтобы продолжить обучение

Тестирование кода, взаимодействующего с файлами Python: Продвинутое тестирование

Самый типичный побочный эффект – это взаимодействие с файлами. В основном это либо чтение файлов, либо запись в них. В этом уроке вы узнаете, как справиться с такими побочными эффектами при тестировании.

С чтением разбираться значительно проще, поэтому с него и начнем.

Чтение файлов

В большинстве случаев чтение файлов не доставляет особых проблем. Оно ничего не изменяет и выполняется локально, в отличие от сетевых запросов. Это значит, что при наличии необходимого файла и нужных прав, вероятность случайных ошибок крайне низка.

При тестировании функций, читающих файлы, должно выполняться ровно одно условие — функция должна позволять менять путь до файла. В таком случае достаточно создать файл нужной структуры в фикстурах:

# Функция читает файл со списком пользователей системы и возвращает их имена
# В 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

Виртуальная файловая система

Это еще один способ тестировать код, работающий с файловыми системами.

С помощью специальной библиотеки во время тестов создается виртуальная файловая система. Она автоматически подменяет реальную файловую систему для всех модулей, работающих с файловой системой.

Это значит, что тестируемую функцию трогать не надо. Эта функция продолжает думать, что она работает с реальным диском. Вся конфигурация при этом задается снаружи:

# Этот способ дает идемпотентность из коробки
def my_fakefs_test(fs):
    # "fs" – фикстура для управления виртуальной файловой системой
    fs.create_file('/var/data/xx1.txt')
    assert os.path.exists('/var/data/xx1.txt')

Дополнительные материалы

  1. Временные директории и файлы в Pytest

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»