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

Инверсия зависимостей Python: Продвинутое тестирование

Далеко не всегда результат работы функции связан с побочным эффектом, как это было ранее в курсе. Иногда побочный эффект — это просто дополнительное действие, которое скорее мешает протестировать основную логику. В этом уроке мы рассмотрим такие примеры и обсудим, как работать с такими случаями.

Как избежать побочных эффектов в тестировании

Представьте себе функцию, которая регистрирует пользователя. Она создает запись в базе данных и отправляет приветственное письмо:

params = {
    'email': 'lala@example.com',
    'password': 'qwerty',
}
register_user(params)

Эта функция делает много всего, но главное, что нас волнует – правильная регистрация пользователя. Типичная регистрация сводится к добавлению в базу данных записи о новом пользователе. Именно это и нужно проверять – наличие новой записи в базе данных с правильно заполненными данными. А вот возврат функции нам никак не поможет.

Как правило, базу данных в тестах не прячут. В веб-фреймворках она доступна в тестовой среде и работает как обычно. Идемпотентность в ней достигается за счет транзакций. Перед тестом транзакция начинается и после теста откатывается. Благодаря этому, каждый тест запускается в идентичном окружении и не важно как он его меняет:

# Гипотетический пример

# Соединение с базой создается один раз на весь запуск тестов
@pytest.fixture(scope="session")
def connection():
    engine = create_engine(
        # тут параметры соединения
    )
    return engine.connect()

# Транзакция стартует и откатывается на каждый тест
@pytest.fixture(autouse=True)
def transaction(connection):
    connection.begin()
    yield
    connection.rollback()

# Сам тест
def test_register_user():
    # Внутри идет добавление данных в базу
    id = register_user(name='Mike')
    # Извлекаем пользователя из базы
    user = User.objects.get(id)
    assert user.name == 'Mike'

А вот с отправкой писем все сложнее. Ее точно делать нельзя, но как этого добиться? Посмотрите, как может выглядеть функция регистрации пользователя:

def register_user(**params):
    user = User(**params)
    user.save()
    send_email('registration', user)
    return user.id

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

Самый простой способ — переменная окружения, которая показывает среду выполнения:

# Выполняем этот код, только если мы не в тестовой среде
# Имя переменной может быть любым
if (os.environ['PROJECT_ENV'] != 'test'):
    send_email('registration', user)

Несмотря на простоту использования, такой подход считается плохой практикой. Формально, из-за него происходит нарушение абстракции — код начинает знать, где он выполняется. Со временем таких проверок становится все больше, код становится грязнее. Более того, если нам все же надо убедиться, что письмо отправляется с правильными данными, то мы не сможем этого сделать.

Следующий способ – поддержка режима тестирования внутри самой библиотеки. Например, где-нибудь на этапе инициализации тестов можно сделать так:

import mailer
from mailer import send_email

# До выполнения тестов
mailer.test = True

Теперь в любом другом месте, где импортируется и используется функция send_email(), реальная отправка происходить не будет:

# Ничего не происходит
send_email('registration', user)
# В отличие от варианта с переменной окружения,
# в этом случае прикладной код ни о чем не догадывается

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

Существует еще один, наиболее универсальный способ. Он основан на применении инверсии зависимостей. Программу можно изменить так, чтобы она вызывала функцию send_email() не напрямую, а принимала ее как параметр:

from mailer import send_email

# Ставим значение по умолчанию, чтобы не пришлось постоянно указывать функцию
def register_user(send=send_email, **params):
    user = User(**params)
    user.save()
    send('registration', user)
    return user.id

Так же посмотрим на тест:

def fake_send_email(*args, **kwargs):
    # Например, письмо можно вывести в терминал для удобства отладки

def test_register_user():
    id = register_user(name='Mike', send=fake_send_email)
    user = User.objects.get(id)
    assert user.name == 'Mike'

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

Теперь обсудим плюсы. Ни библиотека, ни код ничего не знают про тесты. Этот способ наиболее гибкий, он позволяет задавать конкретное поведение для конкретной ситуации. В некоторых экосистемах инверсия зависимостей определяет процесс сборки приложения. Особенно в мире PHP, Java и C#.


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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