Skip to content

Commit 0670246

Browse files
committed
docs and tests and 1.0.0 version
1 parent 597bc6a commit 0670246

File tree

6 files changed

+251
-19
lines changed

6 files changed

+251
-19
lines changed

README.md

Lines changed: 186 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,191 @@
44

55
No AI was used in the creation of this library.
66

7-
async-pytest-httpserver is a fully asynchronous mock HTTP server for use in pytest tests, built on top of aiohttp.
7+
async-pytest-httpserver is a fully asynchronous mock HTTP server for pytest,
8+
built on top of aiohttp.
89

9-
It is designed for testing code that performs HTTP requests (aiohttp, httpx, requests, etc.) without relying on real external services.
10+
It is designed for testing code that makes HTTP requests
11+
(via aiohttp, httpx, requests, etc.) without depending on real external
12+
services.
1013

11-
## Features
12-
- Fully asynchronous - implemented using aiohttp
13-
- Dynamic runtime mocking - add or modify mock routes while the server is running
14-
- Seamless integration with pytest-aiohttp and pytest-asyncio
15-
- Real TCP server - works with any HTTP client (aiohttp, httpx, requests, etc.)
16-
- Supports async handlers - easily define coroutine-based responses
14+
Features
15+
- Fully asynchronous — implemented using aiohttp
16+
- Dynamic runtime mocking — add or modify mock routes while the server is running
17+
- Seamless pytest integration — works smoothly with pytest-aiohttp and pytest-asyncio
18+
- Real TCP server — compatible with any HTTP client (aiohttp, httpx, requests, etc.)
19+
- Supports async handlers — easily define coroutine-based responses
20+
- Flexible mock responses — either return a Response object or a handler that produces one
21+
22+
## How to use
23+
24+
### 1. fixture for start mock server
25+
26+
```python
27+
from async_pytest_httpserver import (
28+
MockData,
29+
AddMockDataFunc,
30+
)
31+
32+
@pytest_asyncio.fixture
33+
async def some_service_mock(
34+
external_service_mock: Callable[
35+
[], Awaitable[tuple[str, AddMockDataFunc]]
36+
],
37+
) -> AsyncGenerator[AddMockDataFunc, None]:
38+
url, add_mock_data = await external_service_mock()
39+
old_url = settings.EXTERNAL_SERVICE_URL
40+
settings.EXTERNAL_SERVICE_URL = url
41+
try:
42+
yield add_mock_data
43+
finally:
44+
settings.EXTERNAL_SERVICE_URL = old_url
45+
```
46+
47+
### 2. mock specific api
48+
49+
You don’t need to follow this pattern exactly —
50+
this is just an example where the fixture is responsible for mocking
51+
a specific route.
52+
53+
```python
54+
@pytest.fixture
55+
def some_service_mock_api(
56+
some_service_mock: AddMockDataFunc,
57+
) -> Callable[
58+
[web.Response | ResponseHandler],
59+
List[dict[str, Any]],
60+
]:
61+
"""An example of a fixture where a specific API is mocked"""
62+
63+
def _create_mock(
64+
response: web.Response
65+
| Callable[[web.Request], web.Response | Awaitable[web.Response]],
66+
) -> List[dict[str, Any]]:
67+
return some_service_mock(MockData("POST", "/some_api", response))
68+
69+
return _create_mock
70+
```
71+
72+
### 3. test it
73+
74+
```python
75+
import pytest
76+
from http import HTTPStatus
77+
78+
from aiohttp.web import json_response, Request, Response
79+
80+
# example of static mock
81+
82+
@pytest.mark.asyncio
83+
async def test_static_mock(client, some_service_mock_api):
84+
# Arrange
85+
calls_info = some_service_mock_api(
86+
json_response(
87+
{"result": "some_result"},
88+
status=HTTPStatus.OK,
89+
)
90+
)
91+
92+
# Act
93+
response = await client.post(
94+
f"{settings.EXTERNAL_SERVICE_URL}/some_api",
95+
json={"text": "text"},
96+
)
97+
98+
# Assert
99+
assert response.ok
100+
data = await response.json()
101+
assert data["result"] == "some_result"
102+
103+
assert len(calls_info) == 1
104+
call_info = calls_info[0]
105+
assert call_info["json"] == {"text": "text"}
106+
107+
108+
# example of dynamic async mock
109+
110+
async def async_mock_handler(request: Request) -> Response:
111+
return json_response(
112+
{"result": "some_result"},
113+
status=HTTPStatus.OK,
114+
)
115+
116+
@pytest.mark.asyncio
117+
async def test_async_handler(client, some_service_mock_api):
118+
# Arrange
119+
calls_info = some_service_mock_api(async_mock_handler)
120+
121+
# Act
122+
response = await client.post(
123+
f"{settings.EXTERNAL_SERVICE_URL}/some_api",
124+
json={"text": "text"},
125+
)
126+
127+
# Assert
128+
assert response.ok
129+
data = await response.json()
130+
assert data["result"] == "some_result"
131+
132+
assert len(calls_info) == 1
133+
call_info = calls_info[0]
134+
assert call_info["json"] == {"text": "text"}
135+
136+
137+
# example of dynamic sync mock
138+
139+
140+
def sync_mock_handler(request: Request) -> Response:
141+
return json_response(
142+
{"result": "some_result"},
143+
status=HTTPStatus.OK,
144+
)
145+
146+
@pytest.mark.asyncio
147+
async def test_sync_handler(client, some_service_mock_api):
148+
# Arrange
149+
calls_info = some_service_mock_api(sync_mock_handler)
150+
151+
# Act
152+
response = await client.post(
153+
f"{settings.EXTERNAL_SERVICE_URL}/some_api",
154+
json={"text": "text"},
155+
)
156+
157+
# Assert
158+
assert response.ok
159+
data = await response.json()
160+
assert data["result"] == "some_result"
161+
162+
assert len(calls_info) == 1
163+
call_info = calls_info[0]
164+
assert call_info["json"] == {"text": "text"}
165+
```
166+
167+
## mock data types
168+
169+
### 1. just aiohttp.web.Response
170+
171+
for example:
172+
173+
```python
174+
from aiohttp.web import json_response
175+
176+
json_response(
177+
{"result": "some_result"},
178+
status=HTTPStatus.OK,
179+
)
180+
```
181+
182+
### 2. callable
183+
184+
If you need custom behavior instead of a static response,
185+
you can provide a callable (func or async func) that returns a
186+
aiohttp.web.Response.
187+
188+
It must match the following signature:
189+
190+
```python
191+
ResponseHandler = Callable[
192+
[web.Request], web.Response | Awaitable[web.Response]
193+
]
194+
```
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from .web_service_mock import MockData, WebServiceMock
1+
from .web_service_mock import MockData, WebServiceMock, ResponseHandler
22
from .fixtures import external_service_mock, AddMockDataFunc
33

44
__all__ = [
55
"MockData",
66
"WebServiceMock",
7+
"ResponseHandler",
78
"external_service_mock",
89
"AddMockDataFunc",
910
]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "async-pytest-httpserver"
3-
version = "0.1.1"
3+
version = "1.0.0"
44
description = "Async mock HTTP server for pytest, built on top of aiohttp."
55
readme = "README.md"
66
requires-python = ">=3.13"

tests/conftest.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from async_pytest_httpserver import (
88
MockData,
99
AddMockDataFunc,
10+
ResponseHandler,
1011
)
1112
from . import settings
1213

@@ -33,17 +34,13 @@ async def some_service_mock(
3334
def some_service_mock_api(
3435
some_service_mock: AddMockDataFunc,
3536
) -> Callable[
36-
[
37-
web.Response
38-
| Callable[[web.Request], web.Response | Awaitable[web.Response]]
39-
],
37+
[web.Response | ResponseHandler],
4038
List[dict[str, Any]],
4139
]:
4240
"""An example of a fixture where a specific API is mocked"""
4341

4442
def _create_mock(
45-
response: web.Response
46-
| Callable[[web.Request], web.Response | Awaitable[web.Response]],
43+
response: web.Response | ResponseHandler,
4744
) -> List[dict[str, Any]]:
4845
return some_service_mock(MockData("POST", "/some_api", response))
4946

tests/test_example.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import pytest
22
from http import HTTPStatus
33

4-
from aiohttp.web import json_response
4+
from aiohttp.web import json_response, Request, Response
55
from . import settings
66

77

88
@pytest.mark.asyncio
9-
async def test_example(client, some_service_mock_api):
9+
async def test_static_mock(client, some_service_mock_api):
1010
# Arrange
1111
calls_info = some_service_mock_api(
1212
json_response(
@@ -29,3 +29,59 @@ async def test_example(client, some_service_mock_api):
2929
assert len(calls_info) == 1
3030
call_info = calls_info[0]
3131
assert call_info["json"] == {"text": "text"}
32+
33+
34+
async def async_mock_handler(request: Request) -> Response:
35+
return json_response(
36+
{"result": "some_result"},
37+
status=HTTPStatus.OK,
38+
)
39+
40+
41+
@pytest.mark.asyncio
42+
async def test_async_handler(client, some_service_mock_api):
43+
# Arrange
44+
calls_info = some_service_mock_api(async_mock_handler)
45+
46+
# Act
47+
response = await client.post(
48+
f"{settings.EXTERNAL_SERVICE_URL}/some_api",
49+
json={"text": "text"},
50+
)
51+
52+
# Assert
53+
assert response.ok
54+
data = await response.json()
55+
assert data["result"] == "some_result"
56+
57+
assert len(calls_info) == 1
58+
call_info = calls_info[0]
59+
assert call_info["json"] == {"text": "text"}
60+
61+
62+
def sync_mock_handler(request: Request) -> Response:
63+
return json_response(
64+
{"result": "some_result"},
65+
status=HTTPStatus.OK,
66+
)
67+
68+
69+
@pytest.mark.asyncio
70+
async def test_sync_handler(client, some_service_mock_api):
71+
# Arrange
72+
calls_info = some_service_mock_api(sync_mock_handler)
73+
74+
# Act
75+
response = await client.post(
76+
f"{settings.EXTERNAL_SERVICE_URL}/some_api",
77+
json={"text": "text"},
78+
)
79+
80+
# Assert
81+
assert response.ok
82+
data = await response.json()
83+
assert data["result"] == "some_result"
84+
85+
assert len(calls_info) == 1
86+
call_info = calls_info[0]
87+
assert call_info["json"] == {"text": "text"}

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)