Большинство тестов на одну и ту же функциональность сильно похожи друг на друга, особенно в части начальной подготовки данных. В прошлом уроке каждый тест начинался со строчки: stack = []
. Это еще не дублирование, но уже шаг в опасную сторону. В этом уроке мы поговорим о том, как избегать дублирования при подготовке одинаковых данных для разных тестов.
Допустим, мы разрабатываем библиотеку funcy, которая предоставляет функции для работы с коллекциями в Python. Среди них есть такие функции:
compact
select
flatten
Как их протестировать? Для работы этих функций нужна заранее подготовленная коллекция. Создаем нужную коллекцию, передаем ее в тестируемую функцию, смотрим результат:
def test_compact():
# Подготовим коллекцию coll
coll = ['One', True, 3, [1, 'hexlet', [0]], 'cat', {}, '', [], False]
# Используем coll для тестирования
result = compact(coll)
assert result == # тут ожидаемое значение
Затем то же самое делаем для всех остальных функций:
def test_select():
# Точно такая же коллекция, получилось дублирование
coll = ['One', True, 3, [1, 'hexlet', [0]], 'cat', {}, '', [], False]
# Используем coll для тестирования
result = select(coll)
assert result == # тут ожидаемое значение
Затем проделываем эту операцию для всех остальных тестируемых функций. Код инициализации коллекции начнет кочевать из одного места в другое, порождая все больше и больше одинакового кода.
Самый простой способ избежать этого — вынести определение коллекции на уровень модуля:
# Создание коллекции теперь описано в одном месте сразу для всех функций
coll = ['One', True, 3, [1, 'hexlet', [0]], 'cat', {}, '', [], False]
def test_compact():
result = compact(coll)
assert result == # тут ожидаемое значение
Это простое решение убирает ненужное дублирование. Однако учтите, что оно работает только в рамках одного модуля. Подобную коллекцию все равно придется определять в каждом тестовом модуле. В нашем случае это скорее плюс, а не минус.
Но далеко не всегда можно выносить данные на уровень модуля. В первую очередь это касается динамических данных. Что будет если хотя бы одна из тестовых функций поменяет эту коллекцию? У нас одна коллекция на выполнение всех тестов, поэтому если она поменяется, это внесет зависимость в тесты от порядка выполнения. Последующие тесты начнут получать измененную коллекцию. Такие ситуации в тестировании недопустимы, потому что они приводят к хрупким и тяжело отлаживаемым тестам.
Для решения этой проблемы тестовые фреймворки предоставляют хуки — специальные функции, которые запускаются до или после тестов. В Pytest их называют фикстурами. Посмотрим на примере, как создавать коллекцию перед каждым тестом:
import pytest
# Создаем фикстуру
# Запускается перед каждым тестом
@pytest.fixture
def coll(): # имя фикстуры выбирается произвольно
return ['One', True, 3, [1, 'hexlet', [0]], 'cat', {}, '', [], False]
# Pytest сам прокидывает результат вызова функции там, где функция указана в аргументе
# Имя параметра совпадает с именем фикстуры
def test_compact(coll):
result = compact(coll)
assert result == # Тут ожидаемое значение
# Не важно, что предыдущий тест сделал с коллекцией
# Здесь коллекция будет новая, так как pytest вызывает coll() заново
def test_select(coll):
result = select(coll, ...)
assert result == # Тут ожидаемое значение
Декоратор @pytest.fixture
добавляет произвольную функцию в процесс выполнения тестов. Фикстура выполняется перед теми тестами, в которых ее запросили через параметры функции. Важно, что имя аргумента совпадает с именем фикстуры.
Теперь другой пример. Представьте себе код, в котором идет работа с текущим временем:
from datetime import datetime
# одна текущая дата на все тесты
now = datetime.now()
def test_foo():
print(now)
def test_bar():
print(now)
test_foo()
# => 2021-05-19 14:09:12.068421
test_bar()
# => 2021-05-19 14:09:12.068421
В выводе две идентичные даты. Подвох тут в том, что модуль с кодом загружается в память ровно один раз. Это значит, что весь код, определенный на уровне модуля, тоже выполняется один раз.
В примере переменная now
определится до запуска всех тестов, и только затем Pytest начнет выполнять тесты. С каждым последующим тестом отставание значения переменной now
от текущего реального значения «сейчас» будет все больше и больше:
- Pytest загружает модули с тестами в память
- Переменная
now
заполняется датой, которая была на момент выполнения этого куска кода - Pytest начинает выполнение тестов, используя дату, полученную на предыдущем шаге
Почему это может быть проблемой? Код, который работает с понятием «сейчас», может рассчитывать, что «сейчас» — это почти моментальный снимок данного момента времени.
Но в примере выше, «сейчас» начинает отставать от реального «сейчас». Чем больше тестов и чем они сложнее, тем больше отставание:
import pytest
@pytest.fixture
def current_time():
return datetime.now()
def test_foo(current_time):
print(current_time)
def test_bar(current_time):
print(current_time)
# Видно, что время изменилось
# 2021-05-19 14:46:14.109220
# 2021-05-19 14:46:14.110206
https://replit.com/@hexlet/python-testing-fixtures
Фикстуры идеально подходят для выделения общих данных, нужных в разных тестах. Но если данные отличаются хотя бы немного, то использование фикстур может привести к более сложному коду, чем если бы их не было.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.