Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ python:
- "3.7"
- "3.8"
install:
- pip install .[testing]
- pip install -e .[testing]
script:
- pytest --cov=pytest_httpx --cov-fail-under=100
- pytest --cov=pytest_httpx --cov-fail-under=100 --runpytest=subprocess
deploy:
provider: pypi
username: __token__
Expand Down
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@ Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixt

You can register responses for both sync and async [`HTTPX`](https://www.python-httpx.org) requests.

`httpx_mock` fixture is available within `pytest_httpx`.

```python
import pytest
import httpx
from pytest_httpx import httpx_mock


def test_something(httpx_mock):
Expand Down Expand Up @@ -73,7 +71,7 @@ Matching is performed on the full URL, query parameters included.

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_url(httpx_mock: HTTPXMock):
Expand All @@ -94,7 +92,7 @@ Matching is performed on equality.

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_post(httpx_mock: HTTPXMock):
Expand Down Expand Up @@ -141,7 +139,7 @@ Matching is performed on equality for each provided header.

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_headers_matching(httpx_mock: HTTPXMock):
Expand All @@ -159,7 +157,7 @@ Matching is performed on equality.

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_content_matching(httpx_mock: HTTPXMock):
Expand All @@ -175,7 +173,7 @@ Use `json` parameter to add a JSON response using python values.

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_json(httpx_mock: HTTPXMock):
Expand All @@ -192,7 +190,7 @@ Use `data` parameter to reply with a custom body by providing bytes or UTF-8 enc

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_str_body(httpx_mock: HTTPXMock):
Expand All @@ -218,7 +216,7 @@ You can specify `boundary` parameter to specify the multipart boundary to use.

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_multipart_body(httpx_mock: HTTPXMock):
Expand All @@ -245,7 +243,7 @@ Use `status_code` parameter to specify the HTTP status code of the response.

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_status_code(httpx_mock: HTTPXMock):
Expand All @@ -262,7 +260,7 @@ Use `headers` parameter to specify the extra headers of the response.

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_headers(httpx_mock: HTTPXMock):
Expand All @@ -279,7 +277,7 @@ Use `http_version` parameter to specify the HTTP protocol version of the respons

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_http_version(httpx_mock: HTTPXMock):
Expand All @@ -306,7 +304,7 @@ Callback should return a httpcore response (as a tuple), you can use `pytest_htt

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock, to_response
from pytest_httpx import HTTPXMock, to_response


def test_dynamic_response(httpx_mock: HTTPXMock):
Expand All @@ -332,7 +330,7 @@ This can be useful if you want to assert that your code handles HTTPX exceptions
```python
import httpx
import pytest
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_exception_raising(httpx_mock: HTTPXMock):
Expand Down Expand Up @@ -385,7 +383,7 @@ Matching is performed on equality.

```python
import httpx
from pytest_httpx import httpx_mock, HTTPXMock
from pytest_httpx import HTTPXMock


def test_many_requests(httpx_mock: HTTPXMock):
Expand Down
22 changes: 21 additions & 1 deletion pytest_httpx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,22 @@
import httpx
import pytest

from pytest_httpx._httpx_mock import HTTPXMock, to_response, _PytestSyncTransport, _PytestAsyncTransport
from pytest_httpx.version import __version__
from pytest_httpx._httpx_mock import httpx_mock, HTTPXMock, to_response


@pytest.fixture
def httpx_mock(monkeypatch) -> HTTPXMock:
mock = HTTPXMock()
# Mock synchronous requests
monkeypatch.setattr(
httpx.Client, "transport_for_url", lambda self, url: _PytestSyncTransport(mock),
)
# Mock asynchronous requests
monkeypatch.setattr(
httpx.AsyncClient,
"transport_for_url",
lambda self, url: _PytestAsyncTransport(mock),
)
yield mock
mock.assert_and_reset()
17 changes: 0 additions & 17 deletions pytest_httpx/_httpx_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,23 +290,6 @@ async def request(
return self.mock._handle_request(*args, **kwargs)


@pytest.fixture
def httpx_mock(monkeypatch) -> HTTPXMock:
mock = HTTPXMock()
# Mock synchronous requests
monkeypatch.setattr(
httpx.Client, "transport_for_url", lambda self, url: _PytestSyncTransport(mock),
)
# Mock asynchronous requests
monkeypatch.setattr(
httpx.AsyncClient,
"transport_for_url",
lambda self, url: _PytestAsyncTransport(mock),
)
yield mock
mock.assert_and_reset()


def to_response(
status_code: int = 200,
http_version: str = "HTTP/1.1",
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
],
keywords=["pytest", "testing", "mock", "httpx"],
packages=find_packages(exclude=["tests*"]),
entry_points={'pytest11': ['pytest_httpx = pytest_httpx']},
install_requires=["httpx==0.13.*", "pytest>=5.4.*,<6.*"],
extras_require={
"testing": [
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# see https://docs.pytest.org/en/documentation-restructure/how-to/writing_plugins.html#testing-plugins
pytest_plugins = ["pytester"]
98 changes: 54 additions & 44 deletions tests/test_httpx_async.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import re
from typing import Optional

import pytest
import httpx
import pytest

from pytest_httpx import httpx_mock, HTTPXMock, to_response
from pytest_httpx import HTTPXMock, to_response


@pytest.mark.asyncio
Expand All @@ -13,8 +12,8 @@ async def test_without_response(httpx_mock: HTTPXMock):
async with httpx.AsyncClient() as client:
await client.get("http://test_url")
assert (
str(exception_info.value)
== "No mock can be found for GET request on http://test_url."
str(exception_info.value)
== "No mock can be found for GET request on http://test_url."
)


Expand Down Expand Up @@ -378,14 +377,14 @@ async def test_multipart_body(httpx_mock: HTTPXMock):

response = await client.get("http://test_url")
assert (
response.text
== '--2256d3a36d2a61a1eba35a22bee5c74a\r\nContent-Disposition: form-data; name="file1"; filename="upload"\r\nContent-Type: application/octet-stream\r\n\r\ncontent of file 1\r\n--2256d3a36d2a61a1eba35a22bee5c74a--\r\n'
response.text
== '--2256d3a36d2a61a1eba35a22bee5c74a\r\nContent-Disposition: form-data; name="file1"; filename="upload"\r\nContent-Type: application/octet-stream\r\n\r\ncontent of file 1\r\n--2256d3a36d2a61a1eba35a22bee5c74a--\r\n'
)

response = await client.get("http://test_url")
assert (
response.text
== """--2256d3a36d2a61a1eba35a22bee5c74a\r
response.text
== """--2256d3a36d2a61a1eba35a22bee5c74a\r
Content-Disposition: form-data; name="key1"\r
\r
value1\r
Expand Down Expand Up @@ -425,32 +424,32 @@ async def test_requests_retrieval(httpx_mock: HTTPXMock):
await client.delete("http://test_url", headers={"X-Test": "test header 4"})

assert (
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="PATCH").read()
== b"sent content 5"
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="PATCH").read()
== b"sent content 5"
)
assert (
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="HEAD").read()
== b""
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="HEAD").read()
== b""
)
assert (
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="PUT").read()
== b"sent content 3"
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="PUT").read()
== b"sent content 3"
)
assert (
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="GET").headers[
"x-test"
]
== "test header 1"
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="GET").headers[
"x-test"
]
== "test header 1"
)
assert (
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="POST").read()
== b"sent content 2"
httpx_mock.get_request(url=httpx.URL("http://test_url"), method="POST").read()
== b"sent content 2"
)
assert (
httpx_mock.get_request(
url=httpx.URL("http://test_url"), method="DELETE"
).headers["x-test"]
== "test header 4"
httpx_mock.get_request(
url=httpx.URL("http://test_url"), method="DELETE"
).headers["x-test"]
== "test header 4"
)


Expand Down Expand Up @@ -585,7 +584,7 @@ def raise_timeout(*args, **kwargs):
@pytest.mark.asyncio
async def test_callback_returning_response(httpx_mock: HTTPXMock):
def custom_response(request: httpx.Request, *args, **kwargs):
return to_response(json={"url": str(request.url)},)
return to_response(json={"url": str(request.url)}, )

httpx_mock.add_callback(custom_response, url="http://test_url")

Expand All @@ -597,7 +596,7 @@ def custom_response(request: httpx.Request, *args, **kwargs):
@pytest.mark.asyncio
async def test_callback_executed_twice(httpx_mock: HTTPXMock):
def custom_response(*args, **kwargs):
return to_response(json=["content"],)
return to_response(json=["content"], )

httpx_mock.add_callback(custom_response)

Expand All @@ -624,19 +623,30 @@ def custom_response(*args, **kwargs):
assert response.json() == ["content"]


@pytest.mark.xfail(
raises=AssertionError,
reason="Single request cannot be returned if there is more than one matching.",
)
@pytest.mark.asyncio
async def test_request_retrieval_with_more_than_one(httpx_mock: HTTPXMock):
httpx_mock.add_response()

async with httpx.AsyncClient() as client:
await client.get("http://test_url", headers={"X-TEST": "test header 1"})
await client.get("http://test_url", headers={"X-TEST": "test header 2"})

httpx_mock.get_request(url=httpx.URL("http://test_url"))
def test_request_retrieval_with_more_than_one(testdir):
"""
Single request cannot be returned if there is more than one matching.
"""
testdir.makepyfile("""
import pytest
import httpx


@pytest.mark.asyncio
async def test_request_retrieval_with_more_than_one(httpx_mock):
httpx_mock.add_response()

async with httpx.AsyncClient() as client:
await client.get("http://test_url", headers={"X-TEST": "test header 1"})
await client.get("http://test_url", headers={"X-TEST": "test header 2"})

httpx_mock.get_request(url=httpx.URL("http://test_url"))
""")
result = testdir.runpytest()
result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines([
'*AssertionError: More than one request (2) matched, use get_requests instead.'
])


@pytest.mark.asyncio
Expand Down Expand Up @@ -664,8 +674,8 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock):
with pytest.raises(httpx.HTTPError) as exception_info:
await client.get("http://test_url")
assert (
str(exception_info.value)
== "No mock can be found for GET request on http://test_url."
str(exception_info.value)
== "No mock can be found for GET request on http://test_url."
)

# Clean up responses to avoid assertion failure
Expand All @@ -689,8 +699,8 @@ async def test_content_not_matching(httpx_mock: HTTPXMock):
with pytest.raises(httpx.HTTPError) as exception_info:
await client.post("http://test_url", data=b"This is the body2")
assert (
str(exception_info.value)
== "No mock can be found for POST request on http://test_url."
str(exception_info.value)
== "No mock can be found for POST request on http://test_url."
)

# Clean up responses to avoid assertion failure
Expand Down
Loading