Skip to content

Instantly share code, notes, and snippets.

@EtsuNDmA
Last active November 24, 2021 10:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save EtsuNDmA/93d709ab5802ffd49a0f14a517feb27f to your computer and use it in GitHub Desktop.
Save EtsuNDmA/93d709ab5802ffd49a0f14a517feb27f to your computer and use it in GitHub Desktop.
Небольшая шпаргалка по библиотечке respx
"""
Небольшая шпаргалка по библиотечке respx. Все это есть в документации https://lundberg.github.io/respx/guide/,
но местами написано очень коротко и не очевидно.
Зависимости: python > 3.10, httpx, respx, pytest-asyncio
"""
from typing import Any
import httpx
import pytest
import respx
from httpx import Response
# ========================================
# Есть несколько способов замокать запросы
# ========================================
@pytest.mark.asyncio
async def test_mock_using_fixture(respx_mock):
"""Самый простой спооб. Пригоден только для примеров, так как не дает гибко настраивать мок запроса"""
route = (respx_mock
.get("https://foo.bar/")
.mock(return_value=Response(200, text="Baz")))
async with httpx.AsyncClient() as client:
response = await client.get("https://foo.bar/")
assert route.called
assert response.status_code == 200
assert response.text == "Baz"
@pytest.mark.asyncio
@pytest.mark.respx(assert_all_mocked=True)
async def test_mock_using_pytest_mark(respx_mock):
"""То же самое, что в предыдущем варианте, но с кастомными настройками, переданными через pytest.mark"""
route = (respx_mock
.get("https://foo.bar/")
.mock(return_value=Response(200, text="Baz")))
async with httpx.AsyncClient() as client:
response = await client.get("https://foo.bar/")
assert route.called
assert response.status_code == 200
assert response.text == "Baz"
@pytest.mark.asyncio
@respx.mock(assert_all_mocked=True)
async def test_mock_using_decorator(respx_mock):
"""Можно использовать мок как декоратор"""
route = (respx_mock
.get("https://foo.bar/")
.mock(return_value=Response(200, text="Baz")))
async with httpx.AsyncClient() as client:
response = await client.get("https://foo.bar/")
assert route.called
assert response.status_code == 200
assert response.text == "Baz"
@pytest.mark.asyncio
async def test_mock_using_context_manager():
"""
Можно использовать мок как контекстный менеджер, особенно если надо сделать
несколько разных моков в одном тесте
"""
# фича python 3.10 - можно оборачивать в скобки менеджеры контекста 😍
async with (
respx.mock(base_url='https://foo.bar/') as respx_mock_foo_bar,
respx.mock(base_url='https://baz.qux/') as respx_mock_baz_aux,
):
route_foo = respx_mock_foo_bar.get('/api/spam/')
route_baz = respx_mock_baz_aux.get('/api/eggs/')
async with httpx.AsyncClient() as client:
response = await client.get("https://foo.bar/api/spam/")
assert route_foo.called
assert not route_baz.called
assert response.status_code == 200
response = await client.get("https://baz.qux/api/eggs/")
assert route_baz.called
assert response.status_code == 200
# ===================
# Как добавлять роуты
# ===================
@pytest.mark.asyncio
@respx.mock # тут нет скобок, то есть используется глобальный объект, подробности ниже
async def test_mock_using_respx_helpers():
"""Route можно добавлять через хелперы .get .post и тд из модуля respx"""
route = respx.get("https://foo.bar/")
async with httpx.AsyncClient() as client:
response = await client.get("https://foo.bar/")
assert route.called
assert response.status_code == 200
@pytest.mark.asyncio
@respx.mock(assert_all_mocked=False, assert_all_called=True)
async def test_mock_using_route_methods(respx_mock):
"""Route можно добавлять через методы объекта MockRouter()"""
route_foo = respx_mock.get("https://foo.bar/")
route_baz = respx.get("https://baz.qux/")
# только в этом случае создается новый объект класса MockRouter, а не используется глобальный respx.mock
assert respx.mock is not respx_mock
# ну и роуты конечно же отличаются
print(f'{respx_mock.routes=}\n{respx.mock.routes=}')
# Напечатает
# > respx_mock.routes=[<Route <Scheme eq 'https'> AND <Host eq 'foo.bar'> AND <Path eq '/'> AND <Method eq 'GET'>>]
# > respx.mock.routes=[<Route <Scheme eq 'https'> AND <Host eq 'baz.qux'> AND <Path eq '/'> AND <Method eq 'GET'>>]
assert respx_mock.routes != respx.mock.routes
async with httpx.AsyncClient() as client:
response = await client.get("https://foo.bar/")
assert route_foo.called
assert response.status_code == 200
# Тут возникнет эксепшен
# E AssertionError: assert False
# E + where False = <Route <Scheme eq 'https'> AND <Host eq 'baz.qux'> AND <Path eq '/'> AND <Method eq 'GET'>>.called
# так как тест будет искать роуты в respx_mock.routes, а не в respx.mock.routes
await client.get("https://baz.qux/")
assert route_baz.called
# Все это написано в одной строчке документации https://lundberg.github.io/respx/guide/#router-settings
# > By configuring, an isolated router is created, and settings are locally bound to the routes added.
# а также в докстринге к respx.router.MockRouter.__call__
#
# Вывод:
# - Не надо смешивать локальный respx_mock.get(...) и глобальный respx.get(...).
# - Предпочтительно использовать isolated router, чтобы избежать проблем с глобальными объектами
@pytest.mark.asyncio
@respx.mock
async def test_mock_using_route_methods_without_parantesis(respx_mock):
"""Однако, если мы используем декоратор без скобок, то respx.mock is respx_mock 🤯"""
route_foo = respx_mock.get("https://foo.bar/")
assert respx.mock is respx_mock
route_baz = respx.get("https://baz.qux/")
async with httpx.AsyncClient() as client:
response = await client.get("https://foo.bar/")
assert route_foo.called
assert response.status_code == 200
# теперь здесь не будет эксепшена
await client.get("https://baz.qux/")
assert route_baz.called
@pytest.mark.asyncio
@respx.mock(assert_all_called=False)
async def test_mock_response_using_shortcuts(respx_mock):
"""Эти роуты одинаковые. Смотри https://lundberg.github.io/respx/guide/#shortcuts"""
route_1 = respx_mock.get("https://foo.bar/").mock(return_value=Response(200, json={"spam": "eggs"}))
route_2 = respx_mock.get("https://foo.bar/")
route_2.return_value = Response(200, json={"spam": "eggs"})
route_3 = respx_mock.get("https://foo.bar/").respond(200, json={"spam": "eggs"})
route_4 = respx_mock.get("https://foo.bar/") % {"status_code": 200, "json": {"spam": "eggs"}}
assert route_1 == route_2 == route_3 == route_4
###############################
# Пример переиспользования мока
###############################
@pytest.fixture()
async def example_client() -> Any:
class ExampleApiClient:
async def get_foo(self) -> None:
async with httpx.AsyncClient() as client:
return await client.get("https://example.com/api/foo/")
async def get_bar(self) -> None:
async with httpx.AsyncClient() as client:
return await client.get("https://example.com/api/bar/")
yield ExampleApiClient()
@pytest.fixture()
async def example_mock() -> Any:
async with respx.mock(assert_all_mocked=True,
assert_all_called=True,
base_url="https://example.com/") as respx_mock:
yield respx_mock
@pytest.mark.asyncio
async def test_foo(example_mock, example_client):
route_foo = (example_mock
.get("api/foo/")
.respond(200))
route_bar = (example_mock
.get("api/bar/")
.respond(200, json={"baz": "qux"}))
response_foo = await example_client.get_foo()
assert route_foo.call_count == 1
assert response_foo.status_code == 200
response_bar = await example_client.get_bar()
assert route_bar.call_count == 1
assert response_bar.status_code == 200
assert response_bar.json() == {"baz": "qux"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment