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

Фикстуры Python: Автоматическое тестирование

Подготовка данных

Представим, что мы разрабатываем библиотеку 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'

Выводы

Мы научились использовать инструмент подготовки данных - фикстуры. Узнали как создавать независимые данные для каждого теста, как использовать данные на протяжении разной длины тестирования и как их переиспользовать. Познакомились со встроенными фикстурами для типовых задач, а также как очистить результаты работы тестов и вернуть начальное состояние программы.


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

  1. Pytest fixtures

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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