Что происходит, или Введение

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

1. Функции, которые делают запросы к удаленным серверам

Рассмотрим функцию, которая принимает в качестве аргумента URL-адрес и проверяет, какие стандартные HTTP-методы она поддерживает (GET, POST, PUT, DELETE).

# checkurl.py
from requests import get, post, put, delete
def check_url(url, params=None):
    """
    Check which HTTP methods (GET, POST, PUT and DELETE) are supported
    by a URL. If the status code is 200, check that the response isn't empty
    :url: URL to be checked
    :return: tuple with supported methods
    """
    result = []
    methods = {
        "GET": get,
        "POST": post,
        "PUT": put,
        "DELETE": delete,
    }
    for method, func in methods.items():
        resp = func(url, params=params)
        # Skip if the status code is 200 and the response is empty
        if resp.status_code == 200 and not resp.content:
            continue
        if resp.ok:
            result.append(method)
    return tuple(result)

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

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

2. Функции, зависящие от времени, геолокации и т.д.

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

Поскольку вы можете находиться практически в любом месте на Земле и запускать скрипт в любое время, результат будет несколько произвольным. Например, посмотрите, насколько разная температура в разных странах мира! Честно говоря, я просто хотел использовать красочное изображение для предварительного просмотра своей статьи, так что прокрутите дальше).

Что такое издеваться?

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

Как издеваться

В этой статье я буду использовать pytest для подробного тестирования с mock. Если вам требуется использовать библиотеку unittest, посмотрите это видео и прочитайте раздел Подделка функции, чтобы знать о подводных камнях, которые одинаковы для обеих библиотек.

Вам необходимо установить библиотеку requests-mock для имитации поведения сервера. Пакет pytest-mock помогает контролировать результат функции, используемой в тестируемом.

Имитация поведения сервера

Напишем тест для функции check_url из первого примера. Здесь нам нужно построить ответы для каждого запроса, который делает эта функция. Мы можем сделать это с помощью библиотеки request-mock следующим образом:

# tests.py
from checkurl import check_url
def test_check_url(requests_mock):
    """
    Check that if a request returns 200 or 301 code, the check_url functions
    consider it as a supported one, and otherwise for 400 and 500 codes
    """
    test_url = "<http://foobar>"
    requests_mock.get(test_url, status_code=200, content=b"test")
    requests_mock.post(test_url, status_code=301)
    requests_mock.put(test_url, status_code=400)
    requests_mock.delete(test_url, status_code=500)
    methods = check_url(test_url)
    assert methods == ("GET", "POST")

Я хочу, чтобы вы обратили внимание на следующие моменты:

  • Вам не нужно импортировать requests_mock напрямую с оператором import. Просто передайте его как аргумент функции
  • Методы HTTP указываются как методы библиотеки requests_mock, точно так же, как вы работаете с библиотекой requests.
  • Вы можете передать содержимое ответа в качестве аргумента content. Значение должно быть байтовой строкой
  • Если вы хотите вернуть объект JSON, вы должны указать его в качестве аргумента json. Например, requests_mock.get(test_url, status_code=200, json={"key": "value"})

Чтобы проверить, работает ли этот тест, запустите fun pytest tests.py. Я предполагаю, что вы даете файлам указанные имена, и файлы расположены в одном каталоге.

В результате тест может быть выполнен без подключения к сети и не зависит от внешних условий.

Подделка функции

Еще один способ протестировать функцию check_url — смоделировать методы get, post, put и delete из библиотеки запросов. Здесь вам нужно передать параметр «mocker» в тестовую функцию:

from checkurl import check_url
from requests import Response
def test_check_url(mocker):
    """
    Check that if a request returns 200 or 301 code, the check_url functions
    consider it as a supported one, and otherwise for 400 and 500 codes
    """
    test_url = "<http://foobar>"
    get_response = Response()
    get_response.status_code = 200
    get_response._content = b'test'
    mocker.patch("checkurl.get", return_value=get_response)
    post_response = Response()
    post_response.status_code = 301
    mocker.patch("checkurl.post", return_value=post_response)
    put_response = Response()
    put_response.status_code = 400
    mocker.patch("checkurl.put", return_value=put_response)
    delete_response = Response()
    delete_response.status_code = 500
    mocker.patch("checkurl.delete", return_value=delete_response)
    methods = check_url(test_url)
    assert methods == ("GET", "POST")

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

  • Вы можете подумать, что нам нужно исправить функцию requests.get вместо функции checkurl.get, так как она определена в библиотеке requests. Однако, если вы замените здесь checkurl на requests, фиктивная библиотека подделает исходную функцию, а не ту, которая импортирована в файле checkurl.py. Крайне важно следить за функцией, которую вы имитируете. Таким образом, если функция импортируется непосредственно в модуль, где ее использует тестируемая функция, вы должны смоделировать импортированную функцию и указать ее имя с именем модуля, а не с именем библиотеки.
  • Аргумент return_value обычно используется для создания чего-то действительного. Если вам нужно вызвать исключение, используйте параметр side_effect. Кроме того, его можно использовать, если фиктивная функция вызывается несколько раз, и вы хотите вернуть различные результаты.

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

Рекомендации

[1] Официальная документация Unittest.mock

[2] Pytest-Mock Documentation

[3] Документация Requests-mock