Подготовка данных
Представим, что мы разрабатываем библиотеку pydash и хотим протестировать её функции для обработки коллекций:
includes()
size()
filter()
- и другие (всего их около 20 штук)
Для работы этих функций нужна заранее подготовленная коллекция. Проще всего придумать одну, которая подойдёт для тестирования большинства или даже всех функций:
from pydash import collections
def test_includes():
coll = ['One', true, 3, 10, 'cat', {}, '', 10, False]
assert includes(coll, 3) is True
assert includes(coll, 11) is False
def test_size():
coll = ['One', true, 3, 10, 'cat', {}, '', 10, False]
assert size(coll) == 9
В примере выше видно, что в каждом тесте мы создаем одну и ту же коллекцию. А теперь представьте, что таких тестов несколько десятков - код начнёт кочевать из одного места в другое, порождая всё больше и больше копипасты.
Самый простой способ избежать этого — вынести определение коллекции на уровень модуля, вне тестовых функций:
from pydash import collections
coll = ['One', true, 3, 10, 'cat', {}, '', 10, False]
def test_includes():
assert includes(coll, 3) is True
assert includes(coll, 11) is False
def test_size():
assert size(coll) == 9
Это простое решение убирает ненужное дублирование. Но у него есть серьезный недостаток. Коллекция теперь глобальная переменная, а значит любой тест, который изменяет ее, делает ее непригодной для последующих тестов.
Но даже если бы наш код не мутировал данные, все равно у такого подхода есть слабые стороны. Представьте себе такой код:
import time
# текущее время в миллисекундах
now = int(time.time() * 1000)
def test_first_example():
print(now)
def test_second_example():
print(now)
# результат вызова тестов
pytest test_file.py
1732103716297
1732103716297
Подвох тут в том, что переменная now
инициализируется один раз, во время загрузки модуля. Весь код, определённый на уровне модуля выполняется ровно один раз.
Почему это может быть проблемой? Код в тесте работает с понятием «сейчас» и рассчитывает на то, что «сейчас» это почти моментальный снимок данного момента времени. Но в примере выше, now
начинает отставать от реального «сейчас» и чем больше тестов и чем они сложнее, тем большее отставание.
Для решения проблемы подготовки данных в pytest предлагается инструмент, который называется фикстуры. Ниже пример того, как создавать дату перед каждым тестом:
import pytest
import time
# текущее время в миллисекундах
@pytest.fixture
def now():
return int(time.time() * 1000)
def test_first_example(now):
print(now)
def test_second_example(now):
print(now)
# результат вызова тестов
pytest test_file.py
1732103749695
1732103749701
Чтобы создать фикстуру, нам нужно описать функцию, которая подготавливает наши данные и обернуть ее декоратором @pytest.fixture
. А для использования, ее нужно передать в параметры теста.
Основное назначение фикстур - подготовка независимых данных. По умолчанию фикстура создается на каждый тест заново, что и гарантирует разделение тестов.
import pytest
@pytest.fixture
def coll():
return [1, 2, 3, 4]
def test_first_example(coll):
coll.append(5)
assert coll == [1, 2, 3, 4, 5]
def test_second_example(coll):
coll.pop()
assert coll == [1, 2, 3]
Тесты могут использовать сколько угодно фикстур. Да и сами фикстуры могут использовать другие фикстуры, точно также через передачу в параметры.
import pytest
@pytest.fixture
def users():
return [{'name': 'John'}, {'name': 'Alice'}]
@pytest.fixture
def admins():
return [{'name': 'Tommas'}]
@pytest.fixture
def all(users, admins):
return user + amdins
def test_example(all, admins):
expected_admins = get_admins(all)
assert expected_admins == admins
Помимо явной передачи в параметры теста можно задать "автоиспользование" фикстуры, указав параметр autouse=True
. Такая фикстура будет автоматически использоваться всеми тестами без ее указания.
import pytest
@pytest.fixture()
def coll():
return [1, 2, 3, 4]
@pytest.fixture(autouse=True)
def setup_coll(coll):
coll[0] = 'a'
def test_first_example(coll):
assert coll == ['a', 2, 3, 4]
def test_second_example(coll):
assert coll[0] == 'a'
Автоиспользование фикстур применяется для подготовки внешних сервисов, тестовых серверов или баз данных.
Области видимости
Зачастую подготовка данных включает в себя более сложные действия, чем создание коллекции. Например, создать тестовый сервер, подключиться к базе данных и заполнить ее данными или создать пользователя через форму регистрации на сайте. Если производить эти операции перед каждым тестом, то это потребует больших затрат времени и памяти. Для решения этой проблемы у фикстур есть области видимости, scopes.
По умолчанию, у фикстур задана область видимости function
, то есть фикстура создается на каждую функцию, которая ее использует, и уничтожается при ее выполнении. Но есть и другие области:
class
- фикстура существует на весь класс с тестомmodule
- фикстура существует на весь модульpackage
- фикструра существует весь пакет с тестамиsession
- фикстура существует на всю тестовую сессию
Так, например, подготовка тестовой базы данных, которая нам нужна на протяжении всей тестовой сессии, может выглядеть как:
import pytest
@pytest.fixture(scope="session")
def db():
# здесь какой-то код, что подготавливает базу
...
@pytest.fixture()
def user():
return {"id": 42, "name": "John"}
def test_example(db, user):
save_to_db(db, user)
expected_user = get_from_db(db, id=42)
assert expected_user == user
Совместные фикстуры
Помимо указания области видимости, pytest также позволяет создавать фикстуры, которые можно использовать во всех модулях. Для этого их нужно прописать в файле conftest.py. Все фикстуры, определенные в нем, могут быть использованы любым тестом в директории tests без необходимости их импортировать, pytest обнаружит их автоматически.
tree tests/
tests/
├── conftest.py
├── test_1.py
├── test_2.py
└── test_3.py
# conftest.py
@pytest.fixture
def db(scope="session")
conn = connect('sqlite:///memory')
return conn
# test_1.py
def test_database(db):
assert is_connected(db) is True
Часто это требуется для подготовки данных, которые потребуются на протяжении всего тестирования - пользовательские сессии, соединения с базой данных, запуск внешних "тяжелых" сервисов.
Очистка данных
В общем случае нам не нужно управлять очисткой данных, после завершения тестов, pytest самостоятельно удалит все фикстуры, очистив состояние после себя. Но иногда очистка может включать в себя освобождение каких-то ресурсов: закрытие файлов, возврат базы в изначальное состояние и прочие операции, обратные подготовке. Для этого в pytest существует выражение yield
.
import pytest
# создаем менеджера для управления рассылками
@pytest.fixture
def email_manager():
return EmailManager()
@pytest.fixture
def sending_user(email_manager):
# фикстура создаст и вернет пользователя
user = email_manager.create_user()
yield user
# затем при очистке удалит пользователя из менеджера
email_manager.delete_user(user)
Основные различия, что в коде return
заменяется на yield
и любой код очистки ресурсов помещается после yield
. После выполнения тестов pytest пройдется по фикстурам и выполнит очистку данных.
Помните, что мы все еще работаем с компьютером и не застрахованы от поломок. Ошибка в неподходящем месте может оставить нашу систему в нестабильном состоянии, которое отразится на последующих тестах. Создавая фикстуры, лучше ограничивать каждую лишь одним действием, изменяющим состояние. Например, если процесс регистрации пользователя состоит из этапов создания базы, записи пользователя в базу и отправки письма, то правильно будет разделить эти этапы на отдельные фикстуры со своей подготовкой и очисткой.
Встроенные фикстуры
В программировании часто приходится решать типовые задачи, уже не раз решенные раньше. Потому мы переиспользуем код, устанавливаем готовые библиотеки и применяем шаблоны. Pytest для частых задач предоставляет встроенные фикстуры. Среди них фикстуры для работы с временными директориями, кэшем или выводом в консоль. Чтобы их использовать, достаточно импортировать в тестах библиотеку pytest
- встроенные фикстуры будут доступны сразу.
import pytest
# фикстура capsys представляет абстракцию для консольного вывода
# она перехватывает stdout и stderr, и позволяет проверять вывод программ
# именно так мы проверяли функции из начальных курсов
def hello_world():
print 'Hello, world!'
def test_output(capsys):
hello_world()
captured = capsys.readouterr()
assert captured.out == 'Hello, world!\n'
Выводы
Мы научились использовать инструмент подготовки данных - фикстуры. Узнали как создавать независимые данные для каждого теста, как использовать данные на протяжении разной длины тестирования и как их переиспользовать. Познакомились со встроенными фикстурами для типовых задач, а также как очистить результаты работы тестов и вернуть начальное состояние программы.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.