Разделы портала

Онлайн-тренинги

.
Создание тестов для REST API на Python с использованием запросов. Часть 4: имитация ответов
15.10.2020 00:00

Автор: Баз Дейкстра (Bas Djikstra)
Оригинал статьи
Перевод: Ольга Алифанова

В этой короткой серии статей я исследую библиотеку Python requests и ее использование для тестов REST API. Это четвертая статья в серии, и в ней мы разберемся с имитацией ответов для юнит-тестирования.

Последние несколько месяцев я был занят улучшением своих навыков разработки. Сейчас я работаю над проектом на Python, и одна из моих задач как разработчика – это создание хороших юнит-тестов. Создавая эти тесты, я столкнулся с проблемой, пытаясь протестировать метод, включающий взаимодействие с REST API с использованием библиотеки requests.

Естественно, я не хочу дергать сам API в моих юнит-тестах, поэтому я пытался найти способ имитации этой зависимости. Я рассматривал вариант создания имитаций с нуля, пока не наткнулся на библиотеку responses (PyPI, GitHub). Согласно их домашней странице, это "Полезная библиотека Python для имитации ответов на запросы". То, что доктор прописал.

Итак, что же можно сделать с библиотекой responses, и как выгодно воспользоваться ей, создавая юнит-тесты? Давайте посмотрим на ряд примеров, имитирующих ответы для Zippopotam.us API.

Создание имитации ответа

Допустим, наш юнит-тест проверяет, что наш код реагирует на HTTP 404, возвращенный REST API, как полагается. Это подразумевает, что нам нужен способ "перекрыть" реальный ответ API при помощи ответа со статусом HTTP 404 и (возможно) с телом, в котором сидит сообщение об ошибке.

Чтобы использовать библиотеку responses для имитации ответа, сначала надо добавить декоратор @responses.activate в тест-метод. Затем в тело тест-метода добавляется имитатор ответа:

@responses.activate
def test_simulate_data_cannot_be_found():
responses.add(
responses.GET,
'http://api.zippopotam.us/us/90210',
json={"error": "No data exists for US zip code 90210"},
status=404
)

Когда вы используете библиотеку requests для выполнения HTTP GET-запроса к http://api.zippopotam.us/us/90210, то вместо ответа от живого API (со статусом 200) вы получите фальш-ответ, который можно проверить вот так:

response = requests.get('http://api.zippopotam.us/us/90210')
assert response.status_code == 404
response_body = response.json()
assert response_body['error'] == 'No data exists for US zip code 90210'

Таким образом можно добавлять любое количество имитаторов ответа.

Неожиданные ответы

Если в ходе тестирования вы случайно наткнетесь на конечную точку, к которой не привязан ни один имитатор ответа, вы получите ConnectionError:

@responses.activate
def test_unmatched_endpoint_raises_connectionerror():
with pytest.raises(ConnectionError):
requests.get('http://api.zippopotam.us/us/12345')

Симуляция исключений

Если вы хотите проверить, как ваш код обращается с исключениями при вызове API при помощи requests, это тоже можно сделать с responses:

@responses.activate
def test_responses_can_raise_error_on_demand():
responses.add(
responses.GET,
'http://api.zippopotam.us/us/99999',
body=RuntimeError('A runtime error occurred')
)
Вы можете убедиться, что все идет как надо, проверяя эту ситуацию в тесте:
with pytest.raises(RuntimeError) as re:
requests.get('http://api.zippopotam.us/us/99999')
assert str(re.value) == 'A runtime error occurred'

Создание динамических ответов

Если вы хотите сгенерировать более сложные и/или динамические ответы, это можно сделать, создав автоматический обратный вызов и используя его в имитации. Этот вызов должен возвращать кортеж, содержащий код статуса ответа (целое число), заголовки (словарь) и ответ (в формате строки).

В этом примере я хочу интерпретировать URL запроса, взять оттуда параметры пути и использовать их в сообщении, которое я возвращаю в теле ответа:

@responses.activate
def test_using_a_callback_for_dynamic_responses():
def request_callback(request):
request_url = request.url
resp_body = {'value': generate_response_from(request_url)}
return 200, {}, json.dumps(resp_body)
responses.add_callback(
responses.GET, 'http://api.zippopotam.us/us/55555',
callback=request_callback,
content_type='application/json',
)
def generate_response_from(url):
parsed_url = urlparse(url).path
split_url = parsed_url.split('/')
return f'You requested data for {split_url[1].upper()} zip code {split_url[2]}'
И снова сделаем тест, убеждающийся, что все работает:
response = requests.get('http://api.zippopotam.us/us/55555')
assert response.json() == {'value': 'You requested data for US zip code 55555'}

К тому же responses помнит все вызовы, сделанные автоматом, и все возвращенные ответы, и это очень полезно, если вы хотите убедиться, что ваш код сделал корректное количество вызовов:

assert len(responses.calls) == 1
assert responses.calls[0].request.url == 'http://api.zippopotam.us/us/55555'
assert responses.calls[0].response.text == '{"value": "You requested data for US zip code 55555"}'

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

Примеры кода из статьи можно найти на моей страничке GitHub. Если вы скачали проект, то (если вы правильно установили Python) запустите:

pip install -r requirements.txt

из корневого каталога проекта python-requests для установки нужных библиотек, и вы сможете прогнать тесты самостоятельно.

Обсудить в форуме