Last active
June 6, 2024 08:48
-
-
Save EtsuNDmA/93d709ab5802ffd49a0f14a517feb27f to your computer and use it in GitHub Desktop.
Небольшая шпаргалка по библиотечке respx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Небольшая шпаргалка по библиотечке 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