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

Манкипатчинг Python: Продвинутое тестирование

В предыдущем уроке мы тестировали гипотетическую функцию get_private_fork_names(username), применяя инверсию зависимостей.

Вспомним содержимое этой функции в ее исходном виде:

from github import Github

def get_private_fork_names(username):
    client = Github('access_token')
    # Клиент выполняет запрос на GitHub и возвращает
    # список приватных репозиториев указанной организации
    repos = client.get_user(username).get_repos(type='private')
    # Оставляем только имена приватных форков
    return [repo.name for repo in repos if repo.fork == True]

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

class EmailSender:
    def send(self, message):
        print(f"Отправка email: {message}")

def notify_user(user_id):
    sender = EmailSender()
    sender.send(f"Привет, {user_id}")

# функция использует другую функцию, что ипользует уже почтовый клиент
def process_order(order_id):
    notify_user(order_id)

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

def notify_user(user_id, sender=EmailSender()):
    sender.send(f"Привет, {user_id}")

def process_order(order_id, sender):  # Приходится передавать sender
    notify_user(order_id, sender)

Но есть способ, который позволяет добраться до нужных вызовов и изменить их даже без инверсии зависимостей. Именно его мы изучим в этом уроке.

Манкипатчинг

Python позволяет подменять модули, классы и методы прямо во время работы программы из любого ее места:

class User:
    def __init__(self, name):
        self.name = name

    def get_name(self):
        return self.name

user = User("John")
user.get_name() # John

patch_name = lambda: "Alice"
user.get_name = patch_name

user.get_name() # Alice

Этой возможностью мы можем воспользоваться и в тестах, подменив проблемный, недетерминированный метод:

from github import Github

def get_private_fork_names(username):
    client = Github("access_token")
    repos = client.get_user(username).get_repos(type="private")
    return [repo.name for repo in repos if repo.fork]


# В тестах:
def test_get_private_fork_names(monkeypatch):
    # определим функцию-подмену сразу в тесте
    def fake_get_repos(self):
        return [
            type('Repo', (), {'name': 'repo1', 'fork': True})(),
            type('Repo', (), {'name': 'repo2', 'fork': False})(),
            type('Repo', (), {'name': 'repo3', 'fork': True})()
        ]

    # Подменяем методы с помощью monkeypatch
    monkeypatch.setattr(Github, 'get_user', lambda self, username: self)
    monkeypatch.setattr(Github, 'get_repos', fake_get_repos)

    # Проверяем результат
    result = get_private_fork_names('hexlet')
    assert result == ['repo1', 'repo3']

Так происходит изменение модулей и классов прямо во время работы программы — в рантайме. Этот процесс называется манкипатчингом (monkey patching). Он считается опасной практикой при написании обычного кода в Python, ведь после манкипатчинга объект изменяет свое поведение не только в этом модуле, но и вообще по всей программе. Но он очень популярен и удобен в библиотеках или во время тестирования.

Для упрощения работы Pytest предоставляет специальную фикстуру monkeypatch. Она позволяет указать конктретный метод класса или модуля, что нужно заменить. После выполнения теста, все изменения будут отменены, и так манкипатчинг в тестах не повлияет на всю программу.

Патчинг запросов

Еще один частый пример исползования манкипатчинга - подмена самого HTTP-запроса. Как пример, библиотека pook. С ее помощью подменяются запросы без прямого взаимодействия с HTTP-клиентом, как в случае инверсии зависимостей:

import pook
import requests  # Популярный HTTP-клиент

@pook.on
def test_my_api():
    # Патчим запрос на конкретную страницу и указываем, что вернуть
    pook.get(
        'http://twitter.com/api/1/foobar',
        reply=404,
        response_json={'error': 'not found'}
    )

    # Сам запрос через библиотеку requests
    resp = requests.get('http://twitter.com/api/1/foobar')
    assert resp.status_code == 404
    assert resp.json() == {"error": "not found"}

Подобные библиотеки для патчинга запросов работают несколько хитрее - они заменяют методы стандартной, низкоуровневой библиотеки для работы с запросами. Так в Python это библиотека urllib3. Все остальные популярные высокоуровневые библиотки, как requests внутри используют urllib3.

Рассмотрим пример использования:

@pook.on
def test_get_private_fork_names():
    pook.get(
        'https://api.github.com/orgs/hexlet/repos',
        reply=200,
        response_json=[{ 'fork': True, 'name': 'one' }, { 'fork': False, 'name': 'two' }]
    )

    names = get_private_fork_names('hexlet')
    assert names == ["one"]  # ожидаемый список имен

Вызов pook.get(url) задает полный адрес страницы, запрос к которой надо перехватить. Pook анализирует все выполняемые запросы и подменяет только тот, который соответствует этим параметрам.

Параметры reply и response_json описывают ответ, который нужно вернуть по данному запросу. В самом простом случае достаточно указать код возврата. В нашей ситуации нужен не только код, но и данные. Именно на этих данных мы и проверяем работу функции get_private_fork_names().

Кассеты

Еще одно из применений манкипатчинга - кассеты. Так называются библиотеки, что сочетают в себе манкипатчинг запросов и использование тестовых данных:

import pytest

# https://github.com/kiwicom/pytest-recording
@pytest.mark.vcr
def test_get_private_fork_names():
    assert get_private_fork_names("hexlet") == ["one", "three"]

При первом запуске тестов выполнится настоящий запрос. Библиотека запишет его результаты в тестовые данные, например по пути tests/cassettes/ в удобном формате, чаще yml или JSON.

interactions:
# здесь детали запроса
- request:
    body: null
    headers:
      Accept:
      - '*/*'
      Connection:
      - keep-alive
      User-Agent:
      - PyGithub/Python
    method: GET
    uri: https://api.github.com/users/hexlet
# и ответ
  response:
    body:
      string: '{"message": { 'fork': True, 'name': 'one' }, { 'fork': False, 'name': 'two' }, "status":"200"}'

В последущие разы, библиотека подменит запрос и в качестве ответа вернет записанную кассету. Кассеты удобны тем, что генерация данных лежит полностью на библиотеке. Также что записанные данные полностью соотвествуют ответу нужного нам сайта или сервиса. Нам же нужно всего одни раз выполнить настоящий запрос. В дополнение, так как данные сохранены в текстовом формате, мы можем редактировать их, инсценируя ситуации разных ответов от сервиса.

Заключение

В чем плюсы и минусы такого способа работы?

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

Минус же заключается в том, что оптимальным подходом в тестировании считается тестирование «черным ящиком». Подход, при котором мы не знаем об внутреннем устройстве программы, а тестируем лишь исходя из входных и ожидаемых выходных данных. С манкипатчингом, тестирование превращается в тестирование «прозрачным ящиком». Это значит, что тест знает про устройство тестируемого кода и зависит от внутренностей. Это делает тесты хрупкими. Функция может измениться без потери работоспособности, но тесты придется переписывать, потому что они завязаны на конкретные методы и атрибуты.


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

  1. Кассеты (VCR)
  2. Responses (Для Requests)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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