HangOps.ru

Monkey patch (обезьяний патч) в pytest

Введение

Я был очень взволнован, когда понял, как использовать monkey patch в pytest, и поэтому решил написать эту статью в которой расскажу, что же такое и приводу два примера использования.

Весь исходный код, представленный в этом сообщении в блоге, можно найти на GitLab

Что такое Monkeypatching?

Monkeypatching — это динамическое изменение части программного обеспечения (например, модуля, объекта, метода или функции) во время выполнения. Monkeypatching часто используется для исправления ошибок или создания прототипов программного обеспечения, особенно при использовании внешних API или библиотек. Pytest использует эту функцию, чтобы вы могли тестировать интерфейсы, которые вы не хотите выполнять на самом деле. Например, вы можете создать модифицированную версию модуля запросов, которая не выполняет реальных транзакций HTTP во время тестирования, а просто возвращает установленные вами фиксированные данные.

Одно из преимуществ обезьяньего патча заключается в том, что вы можете значительно сократить время, необходимое для выполнения ваших тестов, поскольку вы можете избежать медленных сетевых вызовов и/или сложных вызовов внешнего API.

Monkeypatching это тоже самое, что Mocking?

Да-да. Mocking очень похожа на Monkey patch в контексте тестирования. Тем не менее, я всегда думаю о Mocking только с точки зрения тестирования, тогда как у Monkey patch есть более широкая область применения, чем просто тестирование.

Monkey patching с pytest (Пример №1)

Первый пример иллюстрирует, как использовать monkeypatching с pytest, включает изменение поведения метода getcwd() (Получить текущий рабочий каталог) из модуля os, который является частью стандартной библиотеки Python.

Вот исходный код для тестирования:

def example1(): 
    """ 
    Получить текущий каталог 

    Возвращает: 
        Текущий каталог 
    """
     current_path = os.getcwd() 
    return current_path

При тестировании этой функции было бы желательно иметь возможность указать, что возвращает os.getcwd(), вместо реального вызова этой функции из стандартной библиотеки Python. Указав, что возвращает функция os.getcwd(), вы можете писать предсказуемые тесты и проверять различные аспекты своего кода, возвращая нестандартные результаты.

Вот интеграционный тест, в котором используется monkeypatching для указания возвращаемого значения для os.getcwd() в pytest:

def test_get_current_directory(monkeypatch): 
    """ 
    ПРИ ДАННОЙ версии os.getcwd() monkeypatched , 
    КОГДА вызывается example1(), 
    ТОГДА проверьте текущий возвращаемый каталог 
    """
     def mock_getcwd(): 
        return '/data/user/directory123' 

    monkeypatch.setattr(os, 'getcwd', mock_getcwd) 
    assert example1() == '/data/user/directory123'

Эта тестовая функция использует приспособление «monkeypatch», которое является частью pytest, что означает, что «monkeypatch» передается в функцию в качестве аргумента.

Тестовая функция начинается с создания фиктивной версии функции getcwd() (функция 'mock_getcwd()' ), которая возвращает указанное значение. Затем эта фиктивная функция устанавливается для вызова при вызове os.getcwd() с использованием monkeypatch.setattr(). Что действительно приятно в том, как pytest выполняет обезьяний патч, так это то, что это изменение в «os.getcwd()» применимо только в функции «test_get_current_directory()».

Наконец, тестовая функция выполняет фактическую проверку (т. е. вызов assert), чтобы убедиться, что значение, возвращаемое из «example1()», соответствует указанному значению.

Monkey patching с pytest (Пример №2)

Второй пример иллюстрирует, как использовать monkeypatching с pytest при работе с внешним модулем, который в данном случае является модулем requests. Модуль «requests» — это замечательный модуль Python, который позволяет легко работать с HTTP-запросами.

Вот исходный код для тестирования:

def example2():
    """
    Call GET for http://httpbin.org/get

    Returns:
        Status Code of the HTTP Response
        URL in the Text of the HTTP Response
    """
    r = requests.get(BASE_URL + 'get')

    if r.status_code == 200:
        response_data = r.json()
        return r.status_code, response_data["url"]    else:
        return r.status_code, ''

Эта функция выполняет GET запрос на url «http://httpbin.org/get», а затем проверяет этот ответ. http://httpbin.org — отличный ресурс для тестирования вызовов API, и он принадлежит тому же автору (Kenneth Reitz), который написал модуль requests.

Чтобы проверить эту функцию, было бы желательно иметь возможность проверить ответ GET как успешный, так и неудачный. Вы можете сделать это с помощью monkeypatching в pytest!

Вот тестовая функция, которая проверяет успешный GET запрос:

def test_get_response_success(monkeypatch):
    """
    GIVEN a monkeypatched version of requests.get()
    WHEN the HTTP response is set to successful
    THEN check the HTTP response
    """
    class MockResponse(object):
        def __init__(self):
            self.status_code = 200
            self.url = 'http://httpbin.org/get'
            self.headers = {'blaa': '1234'}

        def json(self):
            return {'account': '5678',
                    'url': 'http://www.testurl.com'}

    def mock_get(url):
        return MockResponse()

    monkeypatch.setattr(requests, 'get', mock_get)
    assert example2() == (200, 'http://www.testurl.com')

Как и в первом примере, эта тестовая функция использует «monkeypatch», которая является частью pytest, что означает, что «monkeypatch» передается в функцию в качестве аргумента.

Тестовая функция начинается с создания нового класса MockResponse, который указывает фиксированные значения, которые должны быть возвращены из ответа HTTP. Затем экземпляр этого класса возвращается функцией mock_get().

Эта фиктивная функция mock_get() затем устанавливается для вызова, когда requests.get() вызывается с использованием monkeypatch.setattr().

Наконец, фактическая проверка (т. е. вызов assert) выполняется для проверки того, что возвращаемые значения из «example2()» являются ожидаемыми значениями.

Неудачный ответ HTTP GET можно проверить аналогичным образом:

def test_get_response_failure(monkeypatch):
    """
    GIVEN a monkeypatched version of requests.get()
    WHEN the HTTP response is set to failed
    THEN check the HTTP response
    """
    class MockResponse(object):
        def __init__(self):
            self.status_code = 404
            self.url = 'http://httpbin.org/get'
            self.headers = {'blaa': '1234'}

        def json(self):
            return {'error': 'bad'}

    def mock_get(url):
        return MockResponse()

    monkeypatch.setattr(requests, 'get', mock_get)
    assert example2() == (404, '')

Эта тестовая функция похожа на тест успешного GET запроса, за исключением того, что теперь она возвращает код состояния 404 (внутренняя ошибка сервера), чтобы проверить, что ошибочный путь в «example2()» работает должным образом.

Запуск тестов

Чтобы запустить тесты с помощью pytest, я рекомендую запускать их в каталоге вашего проекта верхнего уровня и использовать подробный флаг:

$ pytest -v

Это должно привести к прохождению всех тестов:

========================================= test session starts=========================
platform darwin — Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 … 
tests/integration/test_example1.py::test_get_current_directory PASSED           [ 33%]
tests/integration/test_example2.py::test_get_response_success PASSED            [ 66%]
tests/integration/test_example2.py::test_get_response_failure PASSED            [100%]
=========================================3 passed in 0.27 seconds =====================

Вывод

Я был очень рад, что в pytest заработало monkeypatching, так как это может значительно улучшить качество моего тестирования. Идея возможности указать фиксированное значение для внешнего вызова действительно полезна, поскольку помогает протестировать как можно больше путей в вашем коде. Кроме того, значительное сокращение времени выполнения ваших тестов является дополнительным преимуществом.

Автор: Patrick Kennedy

Python