pax_global_header 0000666 0000000 0000000 00000000064 14722140600 0014506 g ustar 00root root 0000000 0000000 52 comment=44778067b07e97bfe29bcdd1ba2dc9511d20bb67
pytest_httpx-0.35.0/ 0000775 0000000 0000000 00000000000 14722140600 0014352 5 ustar 00root root 0000000 0000000 pytest_httpx-0.35.0/.github/ 0000775 0000000 0000000 00000000000 14722140600 0015712 5 ustar 00root root 0000000 0000000 pytest_httpx-0.35.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14722140600 0017747 5 ustar 00root root 0000000 0000000 pytest_httpx-0.35.0/.github/workflows/release.yml 0000664 0000000 0000000 00000001041 14722140600 0022106 0 ustar 00root root 0000000 0000000 name: Release
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Create packages
run: |
python -m pip install build
python -m build .
- name: Publish packages
run: |
python -m pip install twine
python -m twine upload dist/* --skip-existing --username __token__ --password ${{ secrets.pypi_password }}
pytest_httpx-0.35.0/.github/workflows/test.yml 0000664 0000000 0000000 00000001357 14722140600 0021457 0 ustar 00root root 0000000 0000000 name: Test
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e .[testing]
- name: Test
run: |
pytest --cov=pytest_httpx --cov-fail-under=100 --cov-report=term-missing --runpytest=subprocess
- name: Test packages creation
run: |
python -m pip install build
python -m build .
pytest_httpx-0.35.0/.gitignore 0000664 0000000 0000000 00000002537 14722140600 0016351 0 ustar 00root root 0000000 0000000 # Created by .ignore support plugin (hsz.mobi)
### VirtualEnv template
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env
# virtualenv
.venv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
.idea/
.pypirc
pip.ini
pytest_httpx-0.35.0/.pre-commit-config.yaml 0000664 0000000 0000000 00000000133 14722140600 0020630 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black pytest_httpx-0.35.0/CHANGELOG.md 0000664 0000000 0000000 00000057154 14722140600 0016177 0 ustar 00root root 0000000 0000000 # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.35.0] - 2024-11-28
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.28.\*
## [0.34.0] - 2024-11-18
### Added
- `is_optional` parameter is now available on responses and callbacks registration. Allowing to add optional responses while keeping other responses as mandatory. Refer to documentation for more details.
- `is_reusable` parameter is now available on responses and callbacks registration. Allowing to add multi-match responses while keeping other responses as single-match. Refer to documentation for more details.
### Fixed
- `httpx_mock.get_request` will now also propose to refine filters if more than one request is found instead of only proposing to switch to `httpx_mock.get_requests`.
## [0.33.0] - 2024-10-28
### Added
- Explicit support for python `3.13`.
- `should_mock` option (callable returning a boolean) is now available, defaulting to always returning `True`. Refer to documentation for more details.
- Matching on the full multipart body can now be performed using `match_files` and `match_data` parameters. Refer to documentation for more details.
- Matching on extensions (including timeout) can now be performed using `match_extensions` parameter. Refer to documentation for more details.
### Removed
- `non_mocked_hosts` option is not available anymore. Use `should_mock` instead as in the following sample:
```python
import pytest
@pytest.mark.httpx_mock(non_mocked_hosts=["my_local_test_host"])
def test_previous_behavior(httpx_mock):
...
@pytest.mark.httpx_mock(should_mock=lambda request: request.url.host not in ["my_local_test_host"])
def test_new_behavior(httpx_mock):
...
```
Please note that your hosts might need to be prefixed with `www.` depending on your usage.
## [0.32.0] - 2024-09-27
### Added
- The following option is now available:
- `can_send_already_matched_responses` (boolean), defaulting to `False`.
- Assertion failure message in case of unmatched responses is now linking documentation on how to deactivate the check.
- Assertion failure message in case of unmatched requests is now linking documentation on how to deactivate the check.
- `httpx.TimeoutException` message issued in case of unmatched request is now linking documentation on how to reuse responses (in case some responses are already matched).
### Fixed
- Documentation now clearly state the risks associated with changing the default options.
- Assertion failure message in case of unmatched requests at teardown is now describing requests in a more user-friendly way.
- Assertion failure message in case of unmatched requests at teardown is now prefixing requests with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
- Assertion failure message in case of unmatched responses at teardown is now prefixing responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
- `httpx.TimeoutException` message issued in case of unmatched request is now prefixing available responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
- `httpx.TimeoutException` message issued in case of unmatched request is now listing unmatched responses (in registration order) before already matched one (still in registration order).
- The incentive behind this change is to help identify a potential mismatch faster as the first unmatched response is the most likely to be the one expected to match.
- Response description in failure messages (`httpx.TimeoutException` message issued in case of unmatched request or assertion failure message in case of unmatched responses at teardown) is now displaying if the response was already matched or not and less misleading in it's phrasing about what it can match (a single request by default).
### Changed
- Last registered matching response will not be reused by default anymore in case all matching responses have already been sent.
- This behavior can be changed thanks to the new `pytest.mark.httpx_mock(can_send_already_matched_responses=True)` option.
- The incentive behind this change is to spot regression if a request was issued more than the expected number of times.
- `HTTPXMock` class was only exposed for type hinting purpose. This is now explained in the class docstring.
- As a result this is the last time a change to `__init__` signature will be documented and considered a breaking change.
- Future changes will not be documented and will be considered as internal refactoring not worth a version bump.
- `__init__` now expects one parameter, the newly introduced (since [0.31.0]) options.
- `HTTPXMockOptions` class was never intended to be exposed and is now marked as private.
## [0.31.2] - 2024-09-23
### Fixed
- `httpx_mock` marker can now be defined at different levels for a single test.
## [0.31.1] - 2024-09-22
### Fixed
- It is now possible to match on content provided as async iterable by the client.
## [0.31.0] - 2024-09-20
### Changed
- Tests will now fail at teardown by default if some requests were issued but were not matched.
- This behavior can be changed thanks to the new `pytest.mark.httpx_mock(assert_all_requests_were_expected=False)` option.
- The incentive behind this change is to spot unexpected requests in case code is swallowing `httpx.TimeoutException`.
- The `httpx_mock` fixture is now configured using a marker (many thanks to [`Frazer McLean`](https://github.com/RazerM)).
```python
# Apply marker to whole module
pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
# Or to specific tests
@pytest.mark.httpx_mock(non_mocked_hosts=[...])
def test_foo(httpx_mock):
...
```
- The following options are available:
- `assert_all_responses_were_requested` (boolean), defaulting to `True`.
- `assert_all_requests_were_expected` (boolean), defaulting to `True`.
- `non_mocked_hosts` (iterable), defaulting to an empty list, meaning all hosts are mocked.
- `httpx_mock.reset` do not expect any parameter anymore and will only reset the mock state (no assertions will be performed).
### Removed
- `pytest` `7` is not supported anymore (`pytest` `8` has been out for 9 months already).
- `assert_all_responses_were_requested` fixture is not available anymore, use `pytest.mark.httpx_mock(assert_all_responses_were_requested=False)` instead.
- `non_mocked_hosts` fixture is not available anymore, use `pytest.mark.httpx_mock(non_mocked_hosts=[])` instead.
## [0.30.0] - 2024-02-21
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.27.\*
### Fixed
- Switch from `setup.py` to `pyproject.toml` (many thanks to [`Felix Scherz`](https://github.com/felixscherz)).
## [0.29.0] - 2024-01-29
### Added
- Add support for [`pytest`](https://docs.pytest.org)==8.\* ([`pytest`](https://docs.pytest.org)==7.\* is still supported for now) (many thanks to [`Yossi Rozantsev`](https://github.com/Apakottur)).
## [0.28.0] - 2023-12-21
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.26.\*
## [0.27.0] - 2023-11-13
### Added
- Explicit support for python `3.12`.
### Fixed
- Custom HTTP transport are now handled (parent call to `handle_async_request` or `handle_request`).
### Changed
- Only HTTP transport are now mocked, this should not have any impact, however if it does, please feel free to open an issue describing your use case.
## [0.26.0] - 2023-09-18
### Added
- Added `proxy_url` parameter which allows matching on proxy URL.
## [0.25.0] - 2023-09-11
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.25.\*
### Removed
- `pytest` `6` is no longer supported.
## [0.24.0] - 2023-09-04
### Added
- Added `match_json` parameter which allows matching on JSON decoded body (matching against python representation instead of bytes).
### Changed
- Even if it was never documented as a feature, the `match_headers` parameter was not considering header names case when matching.
- As this might have been considered a feature by some users, the fact that `match_headers` will now respect casing is documented as a breaking change.
### Fixed
- Matching on headers does not ignore name case anymore, the name must now be cased as sent (as some servers might expect a specific case).
- Error message in case a request does not match will now include request headers with mismatching name case as well.
- Error message in case a request does not match will now include request headers when not provided as lower-cased to `match_headers`.
- Add `:Any` type hint to `**matchers` function arguments to satisfy strict type checking mode in [`pyright`](https://microsoft.github.io/pyright/#/).
## [0.23.1] - 2023-08-02
### Fixed
- Version `0.23.0` introduced a regression removing the support for mutating json content provided in `httpx_mock.add_response`.
- This is fixed, you can now expect the JSON return being as it was when provided to `httpx_mock.add_response`:
```python
mutating_json = {"content": "request 1"}
# This will return {"content": "request 1"}
httpx_mock.add_response(json=mutating_json)
mutating_json["content"] = "request 2"
# This will return {"content": "request 2"}
httpx_mock.add_response(json=mutating_json)
```
## [0.23.0] - 2023-08-02
### Removed
- Python `3.7` and `3.8` are no longer supported.
### Fixed
- `httpx_mock.add_response` is now returning a new `httpx.Response` instance upon each matching request. Preventing unnecessary recursion in streams.
## [0.22.0] - 2023-04-12
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.24.\*
## [0.21.3] - 2023-01-20
### Fixed
- Update version specifiers for `pytest` dependency to support `packaging` `23`.
- Add explicit support for `python` `3.11`.
## [0.21.2] - 2022-11-03
### Fixed
- URL containing non ASCII characters in query can now be matched.
- Requests are now cleared when calling `httpx_mock.reset`.
## [0.21.1] - 2022-10-20
### Fixed
- `httpx_mock.add_callback` now handles async callbacks.
## [0.21.0] - 2022-05-24
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.23.\*
### Removed
- Python `3.6` is no longer supported.
## [0.20.0] - 2022-02-05
### Added
- Add support for [`pytest`](https://docs.pytest.org)==7.\* ([`pytest`](https://docs.pytest.org)==6.\* is still supported for now) (many thanks to [`Craig Blaszczyk`](https://github.com/jakul)).
## [0.19.0] - 2022-01-26
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.22.\*
### Deprecated
- Python 3.6 is no longer supported.
## [0.18.0] - 2022-01-17
### Fixed
- Callback are now executed as expected when there is a matching already sent response.
### Changed
- Registration order is now looking at responses and callbacks. Prior to this version, registration order was looking at responses before callbacks.
### Removed
- `httpx_mock.add_response` `data`, `files` and `boundary` parameters have been removed. It was deprecated since `0.17.0`. Refer to this version changelog entry for more details on how to update your code.
## [0.17.3] - 2021-12-27
### Fixed
- A callback can now raise an exception again (regression in mypy check since [0.16.0]).
### Added
- An exception can now be raised without creating a callback by using `httpx_mock.add_exception` method.
## [0.17.2] - 2021-12-23
### Fixed
- Do not consider a callback response as read, even if it is not a stream, before returning to `httpx`. Allowing any specific httpx handling to be triggered such as `httpx.Response.elapsed` computing.
## [0.17.1] - 2021-12-20
### Fixed
- Do not consider a response as read, even if it is not a stream, before returning to `httpx`. Allowing any specific httpx handling to be triggered such as `httpx.Response.elapsed` computing.
## [0.17.0] - 2021-12-20
### Changed
- `httpx_mock.add_response` `data` parameter is only used for multipart content. It was deprecated since `0.14.0`. Refer to this version changelog entry for more details on how to update your code.
### Removed
- `pytest_httpx.to_response` function has been removed. It was deprecated since `0.14.0`. Refer to this version changelog entry for more details on how to update your code.
### Deprecated
- `httpx_mock.add_response` `data`, `files` and `boundary` parameters that were only used for multipart content. Instead, provide the `stream` parameter with an instance of the `httpx._multipart.MultipartStream`.
### Fixed
- Responses are no more read or closed when returned to the client. Allowing to add a response once and reading it as a new response on every request.
## [0.16.0] - 2021-12-20
### Changed
- Callbacks are now expected to have a single parameter, the request. The previously second parameter `extensions`, can still be accessed via `request.extensions`.
### Fixed
- Allow for users to run `mypy --strict`.
## [0.15.0] - 2021-11-16
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.21.\*
## [0.14.0] - 2021-10-22
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.20.\* (many thanks to [`Terence Honles`](https://github.com/terencehonles))
- Callbacks are now expected to return a `httpx.Response` instance instead of the previous `httpcore.Response` tuple. As a consequence, `pytest_httpx.to_response` now returns a `httpx.Response` instance.
### Added
- `httpx_mock.add_response` now allows to explicitly provide bytes using `content` parameter.
- `httpx_mock.add_response` now allows to explicitly provide string using `text` parameter.
- `httpx_mock.add_response` now allows to explicitly provide HTML string content using `html` parameter.
- `httpx_mock.add_response` now allows to explicitly provide streamed content using `stream` parameter and the new `pytest_httpx.IteratorStream` class.
### Deprecated
- `pytest_httpx.to_response` is now deprecated in favor of `httpx.Response`. This function will be removed in a future release.
- `httpx_mock.add_response` `data` parameter should now only be used for multipart content. Instead, use the appropriate parameter amongst `content`, `text`, `html` or `stream`.
## [0.13.0] - 2021-08-19
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.19.\*
- `files` parameter of `httpx_mock.add_response` now expect dictionary values to be binary (as per [`httpx` new requirement](https://github.com/encode/httpx/blob/master/CHANGELOG.md#0190-19th-june-2021)).
## [0.12.1] - 2021-08-11
### Fixed
- Type information is now provided following [PEP 561](https://www.python.org/dev/peps/pep-0561/) (many thanks to [`Caleb Ho`](https://github.com/calebho)).
## [0.12.0] - 2021-04-27
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.18.\*
- `ext` callback parameter was renamed into `extensions`.
## [0.11.0] - 2021-03-01
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.17.\*
## [0.10.1] - 2020-11-25
### Fixed
- Order of different parameters does not matter anymore for URL matching. It does however still matter for a same parameter.
## [0.10.0] - 2020-10-06
### Added
- Document how to assert that no requests were issued.
- Document how to send cookies.
- Explicit support for python 3.9
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.16.\*
- Update documentation to reflect the latest way of sending bytes using `httpx`. Via `content` parameter instead of `data`.
- Code now follow `black==20.8b1` formatting instead of the git master version.
- Sending a JSON response using `json` parameter will now set the `application/json` content-type header by default.
### Fixed
- Allow to provide any supported `httpx` headers type in headers parameter for `httpx_mock.add_response` and `pytest_httpx.to_response`. Previously only dict was supported.
## [0.9.0] - 2020-09-22
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.15.\*
- Callbacks are now called with `ext` dictionary instead of `timeout`. To follow `httpcore` design changes. You can still retrieve timeout by using ```ext['timeout']```
## [0.8.0] - 2020-08-26
### Added
- `non_mocked_hosts` fixture allowing to avoid mocking requests sent on some specific hosts.
### Changed
- Display the matchers that were not matched instead of the responses that were not sent.
## [0.7.0] - 2020-08-13
### Changed
- The `httpx.HTTPError` message issued in case no mock could be found is now a `httpx.TimeoutException` containing all information required to fix the test case (if needed).
## [0.6.0] - 2020-08-07
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.14.\*
## [0.5.0] - 2020-07-31
### Changed
- requires [`pytest`](https://docs.pytest.org/en/latest/) 6.
- `assert_and_reset` mock method has been renamed to `reset` and now takes a boolean parameter to specify if assertion should be performed.
### Added
- It is now possible to disable the assertion that all registered responses were requested thanks to the `assert_all_responses_were_requested` fixture. Refer to documentation for more details.
### Removed
- It is not possible to provide an URL encoded response anymore by providing a dictionary in `data` parameter.
## [0.4.0] - 2020-06-05
### Changed
- `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture does not need to be explicitly imported anymore (many thanks to [`Thomas LÉVEIL`](https://github.com/thomasleveil)).
## [0.3.0] - 2020-05-24
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.13.\*
- requires [`pytest`](https://docs.pytest.org/en/latest/) 5.4.0 (at least)
- callbacks must now return a tuple as per `httpcore` specifications. Refer to documentation for more details.
- callbacks timeout parameter is now a dict as per `httpcore` specifications.
## [0.2.1] - 2020-03-20
### Fixed
- Handle the fact that some classes and functions we use are now part of internals within [`httpx`](https://www.python-httpx.org).
## [0.2.0] - 2020-03-09
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.12.\*
## [0.1.0] - 2020-02-13
### Added
- Consider as stable.
## [0.0.5] - 2020-02-10
### Added
- match_headers parameter now allows matching on headers.
- match_content parameter now allows matching on full body.
### Changed
- `httpx.HTTPError` is now raised instead of `Exception` in case a request cannot be matched.
## [0.0.4] - 2020-02-07
### Changed
- url is not a mandatory parameter for response registration anymore.
- url is not a mandatory parameter for callback registration anymore.
- url is not a mandatory parameter for request retrieval anymore.
- method does not have a default value for response registration anymore.
- method does not have a default value for callback registration anymore.
- method does not have a default value for request retrieval anymore.
- url and methods are not positional arguments anymore.
## [0.0.3] - 2020-02-06
### Added
- Allow providing JSON response as python values.
- Mock async `httpx` requests as well.
- Allow providing files and boundary for multipart response.
- Allow to provide data as a dictionary for multipart response.
- Allow providing callbacks that are executed upon reception of a request.
- Handle the fact that parameters may be introduced in `httpx` *Dispatcher.send method.
- Allow to retrieve all matching requests with HTTPXMock.get_requests
### Changed
- method can now be provided even if not entirely upper-cased.
- content parameter renamed into data.
- HTTPXMock.get_request now fails if more than one request match. Use HTTPXMock.get_request instead.
- HTTPXMock.requests is now private, use HTTPXMock.get_requests instead.
- HTTPXMock.responses is now private, it should not be accessed anyway.
- url can now be a re.Pattern instance.
## [0.0.2] - 2020-02-06
### Added
- Allow to retrieve requests.
## [0.0.1] - 2020-02-05
### Added
- First release, should be considered as unstable for now as design might change.
[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.35.0...HEAD
[0.35.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.34.0...v0.35.0
[0.34.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.33.0...v0.34.0
[0.33.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.32.0...v0.33.0
[0.32.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.2...v0.32.0
[0.31.2]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.1...v0.31.2
[0.31.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.0...v0.31.1
[0.31.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.30.0...v0.31.0
[0.30.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.29.0...v0.30.0
[0.29.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.28.0...v0.29.0
[0.28.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.27.0...v0.28.0
[0.27.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.26.0...v0.27.0
[0.26.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.25.0...v0.26.0
[0.25.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.24.0...v0.25.0
[0.24.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.23.1...v0.24.0
[0.23.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.23.0...v0.23.1
[0.23.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.22.0...v0.23.0
[0.22.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.3...v0.22.0
[0.21.3]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.2...v0.21.3
[0.21.2]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.1...v0.21.2
[0.21.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.0...v0.21.1
[0.21.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.20.0...v0.21.0
[0.20.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.19.0...v0.20.0
[0.19.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.18.0...v0.19.0
[0.18.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.17.3...v0.18.0
[0.17.3]: https://github.com/Colin-b/pytest_httpx/compare/v0.17.2...v0.17.3
[0.17.2]: https://github.com/Colin-b/pytest_httpx/compare/v0.17.1...v0.17.2
[0.17.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.17.0...v0.17.1
[0.17.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.16.0...v0.17.0
[0.16.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.15.0...v0.16.0
[0.15.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.12.1...v0.13.0
[0.12.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.12.0...v0.12.1
[0.12.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.11.0...v0.12.0
[0.11.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.10.1...v0.11.0
[0.10.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.10.0...v0.10.1
[0.10.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.9.0...v0.10.0
[0.9.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.2.1...v0.3.0
[0.2.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.5...v0.1.0
[0.0.5]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.4...v0.0.5
[0.0.4]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.3...v0.0.4
[0.0.3]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.2...v0.0.3
[0.0.2]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.1...v0.0.2
[0.0.1]: https://github.com/Colin-b/pytest_httpx/releases/tag/v0.0.1
pytest_httpx-0.35.0/CONTRIBUTING.md 0000664 0000000 0000000 00000004170 14722140600 0016605 0 ustar 00root root 0000000 0000000 # How to contribute
Everyone is free to contribute on this project.
There are two ways to contribute:
- Submit an issue.
- Submit a pull request.
## Submitting an issue
Before creating an issue please make sure that it was not already reported.
### When?
- You encountered an issue.
- You have a change proposal.
- You have a feature request.
### How?
1) Go to the *Issues* tab and click on the *New issue* button.
2) Title should be a small sentence describing the request.
3) The comment should contain as much information as possible
* Actual behavior (including the version you used)
* Expected behavior
* Steps to reproduce
## Submitting a pull request
### When?
- You fixed an issue.
- You changed something.
- You added a new feature.
### How?
#### Code
1) Create a new branch based on `develop` branch.
2) Fetch all dev dependencies.
* Install required python modules using `pip`: **python -m pip install .[testing]**
3) Ensure tests are ok by running them using [`pytest`](https://doc.pytest.org/en/latest/index.html).
4) Add your changes.
5) Follow [Black](https://black.readthedocs.io/en/stable/) code formatting.
* Install [pre-commit](https://pre-commit.com) python module using `pip`: **python -m pip install pre-commit**
* To add the [pre-commit](https://pre-commit.com) hook, after the installation run: **pre-commit install**
6) Add at least one [`pytest`](https://doc.pytest.org/en/latest/index.html) test case.
* Unless it is an internal refactoring request or a documentation update.
7) Add related [changelog entry](https://keepachangelog.com/en/1.1.0/) in the `Unreleased` section.
* Unless it is a documentation update.
#### Enter pull request
1) Go to the *Pull requests* tab and click on the *New pull request* button.
2) *base* should always be set to `develop` and it should be compared to your branch.
3) Title should be a small sentence describing the request.
4) The comment should contain as much information as possible
* Actual behavior (before the new code)
* Expected behavior (with the new code)
* Steps to reproduce (with and without the new code to see the difference)
pytest_httpx-0.35.0/LICENSE 0000664 0000000 0000000 00000002057 14722140600 0015363 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2024 Colin Bounouar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
pytest_httpx-0.35.0/MANIFEST.in 0000664 0000000 0000000 00000000062 14722140600 0016106 0 ustar 00root root 0000000 0000000 include CHANGELOG.md
recursive-include tests *.py
pytest_httpx-0.35.0/README.md 0000664 0000000 0000000 00000076772 14722140600 0015654 0 ustar 00root root 0000000 0000000
Send responses to HTTPX using pytest
> [!NOTE]
> Version 1.0.0 will be released once httpx is considered as stable (release of 1.0.0).
>
> However, current state can be considered as stable.
Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture will make sure every [`httpx`](https://www.python-httpx.org) request will be replied to with user provided responses ([unless some hosts are explicitly skipped](#do-not-mock-some-requests)).
- [Add responses](#add-responses)
- [JSON body](#add-json-response)
- [Custom body](#reply-with-custom-body)
- [Multipart body (files, ...)](#add-multipart-response)
- [HTTP status code](#add-non-200-response)
- [HTTP headers](#reply-with-custom-headers)
- [HTTP/2.0](#add-http/2.0-response)
- [Add dynamic responses](#dynamic-responses)
- [Raising exceptions](#raising-exceptions)
- [Check requests](#check-sent-requests)
- [Configuration](#configuring-httpx_mock)
- [Register more responses than requested](#allow-to-register-more-responses-than-what-will-be-requested)
- [Register less responses than requested](#allow-to-not-register-responses-for-every-request)
- [Allow to register a response for more than one request](#allow-to-register-a-response-for-more-than-one-request)
- [Do not mock some requests](#do-not-mock-some-requests)
- [Migrating](#migrating-to-pytest-httpx)
- [responses](#from-responses)
- [aioresponses](#from-aioresponses)
## Add responses
You can register responses for both sync and async [`HTTPX`](https://www.python-httpx.org) requests.
```python
import pytest
import httpx
def test_something(httpx_mock):
httpx_mock.add_response()
with httpx.Client() as client:
response = client.get("https://test_url")
@pytest.mark.asyncio
async def test_something_async(httpx_mock):
httpx_mock.add_response()
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
```
If all registered responses are not sent back during test execution, the test case will fail at teardown [(unless you turned `assert_all_responses_were_requested` option off)](#allow-to-register-more-responses-than-what-will-be-requested).
Default response is a `HTTP/1.1` `200 (OK)` without any body.
### How response is selected
In case more than one response match request, the first one not yet sent (according to the registration order) will be sent.
In case all matching responses have been sent once, the request will [not be considered as matched](#in-case-no-response-can-be-found) [(unless you turned `can_send_already_matched_responses` option on)](#allow-to-register-a-response-for-more-than-one-request).
You can add criteria so that response will be sent only in case of a more specific matching.
#### Matching on URL
`url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance.
Matching is performed on the full URL, query parameters included.
Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once.
```python
import httpx
import re
from pytest_httpx import HTTPXMock
def test_url(httpx_mock: HTTPXMock):
httpx_mock.add_response(url="https://test_url?a=1&b=2")
with httpx.Client() as client:
response1 = client.delete("https://test_url?a=1&b=2")
response2 = client.get("https://test_url?b=2&a=1")
def test_url_as_pattern(httpx_mock: HTTPXMock):
httpx_mock.add_response(url=re.compile(".*test.*"))
with httpx.Client() as client:
response = client.get("https://test_url")
def test_url_as_httpx_url(httpx_mock: HTTPXMock):
httpx_mock.add_response(url=httpx.URL("https://test_url", params={"a": "1", "b": "2"}))
with httpx.Client() as client:
response = client.get("https://test_url?a=1&b=2")
```
#### Matching on HTTP method
Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) to reply to.
`method` parameter must be a string. It will be upper-cased, so it can be provided lower cased.
Matching is performed on equality.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_post(httpx_mock: HTTPXMock):
httpx_mock.add_response(method="POST")
with httpx.Client() as client:
response = client.post("https://test_url")
def test_put(httpx_mock: HTTPXMock):
httpx_mock.add_response(method="PUT")
with httpx.Client() as client:
response = client.put("https://test_url")
def test_delete(httpx_mock: HTTPXMock):
httpx_mock.add_response(method="DELETE")
with httpx.Client() as client:
response = client.delete("https://test_url")
def test_patch(httpx_mock: HTTPXMock):
httpx_mock.add_response(method="PATCH")
with httpx.Client() as client:
response = client.patch("https://test_url")
def test_head(httpx_mock: HTTPXMock):
httpx_mock.add_response(method="HEAD")
with httpx.Client() as client:
response = client.head("https://test_url")
```
#### Matching on proxy URL
`proxy_url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance.
Matching is performed on the full proxy URL, query parameters included.
Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_proxy_url(httpx_mock: HTTPXMock):
httpx_mock.add_response(proxy_url="http://test_proxy_url?b=1&a=2")
with httpx.Client(proxy="http://test_proxy_url?a=2&b=1") as client:
response = client.get("https://test_url")
```
#### Matching on HTTP headers
Use `match_headers` parameter to specify the HTTP headers (as a dict) to reply to.
Matching is performed on equality for each provided header.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_headers_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(match_headers={'User-Agent': 'python-httpx/0.25.0'})
with httpx.Client() as client:
response = client.get("https://test_url")
```
#### Matching on HTTP body
Use `match_content` parameter to specify the full HTTP body (as bytes) to reply to.
Matching is performed on equality.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_content_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(match_content=b"This is the body")
with httpx.Client() as client:
response = client.post("https://test_url", content=b"This is the body")
```
##### Matching on HTTP JSON body
Use `match_json` parameter to specify the JSON decoded HTTP body to reply to.
Matching is performed on equality. You can however use `unittest.mock.ANY` to do partial matching.
```python
import httpx
from pytest_httpx import HTTPXMock
from unittest.mock import ANY
def test_json_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(match_json={"a": "json", "b": 2})
with httpx.Client() as client:
response = client.post("https://test_url", json={"a": "json", "b": 2})
def test_partial_json_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(match_json={"a": "json", "b": ANY})
with httpx.Client() as client:
response = client.post("https://test_url", json={"a": "json", "b": 2})
```
Note that `match_content` or `match_files` cannot be provided if `match_json` is also provided.
##### Matching on HTTP multipart body
Use `match_files` and `match_data` parameters to specify the full multipart body to reply to.
Matching is performed on equality.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_multipart_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"})
with httpx.Client() as client:
response = client.post("https://test_url", files={"name": ("file_name", b"File content")}, data={"field": "value"})
```
Note that `match_content` or `match_json` cannot be provided if `match_files` is also provided.
#### Matching on extensions
Use `match_extensions` parameter to specify the extensions (as a dict) to reply to.
Matching is performed on equality for each provided extension.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_extensions_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(match_extensions={'test': 'value'})
with httpx.Client() as client:
response = client.get("https://test_url", extensions={"test": "value"})
```
##### Matching on HTTP timeout(s)
Use `match_extensions` parameter to specify the timeouts (as a dict) to reply to.
Matching is performed on the full timeout dict equality.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_timeout_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(match_extensions={'timeout': {'connect': 10, 'read': 10, 'write': 10, 'pool': 10}})
with httpx.Client() as client:
response = client.get("https://test_url", timeout=10)
```
### Add JSON response
Use `json` parameter to add a JSON response using python values.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_json(httpx_mock: HTTPXMock):
httpx_mock.add_response(json=[{"key1": "value1", "key2": "value2"}])
with httpx.Client() as client:
assert client.get("https://test_url").json() == [{"key1": "value1", "key2": "value2"}]
```
Note that the `content-type` header will be set to `application/json` by default in the response.
### Reply with custom body
Use `text` parameter to reply with a custom body by providing UTF-8 encoded string.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_str_body(httpx_mock: HTTPXMock):
httpx_mock.add_response(text="This is my UTF-8 content")
with httpx.Client() as client:
assert client.get("https://test_url").text == "This is my UTF-8 content"
```
Use `content` parameter to reply with a custom body by providing bytes.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_bytes_body(httpx_mock: HTTPXMock):
httpx_mock.add_response(content=b"This is my bytes content")
with httpx.Client() as client:
assert client.get("https://test_url").content == b"This is my bytes content"
```
Use `html` parameter to reply with a custom body by providing UTF-8 encoded string.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_html_body(httpx_mock: HTTPXMock):
httpx_mock.add_response(html="This is HTML content")
with httpx.Client() as client:
assert client.get("https://test_url").text == "
This is HTML content"
```
### Reply by streaming chunks
Use `stream` parameter (as `httpx.SyncByteStream` or `httpx.AsyncByteStream`) to stream chunks that you specify.
Note that `pytest_httpx.IteratorStream` can be used to provide an iterable.
```python
import httpx
import pytest
from pytest_httpx import HTTPXMock, IteratorStream
def test_sync_streaming(httpx_mock: HTTPXMock):
httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"]))
with httpx.Client() as client:
with client.stream(method="GET", url="https://test_url") as response:
assert list(response.iter_raw()) == [b"part 1", b"part 2"]
@pytest.mark.asyncio
async def test_async_streaming(httpx_mock: HTTPXMock):
httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"]))
async with httpx.AsyncClient() as client:
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == [b"part 1", b"part 2"]
```
### Add multipart response
Use the httpx `MultipartStream` via the `stream` parameter to send a multipart response.
Reach out to `httpx` developers if you need this publicly exposed as [this is not a standard use case](https://github.com/encode/httpx/issues/872#issuecomment-633584819).
```python
import httpx
from httpx._multipart import MultipartStream
from pytest_httpx import HTTPXMock
def test_multipart_body(httpx_mock: HTTPXMock):
httpx_mock.add_response(stream=MultipartStream(data={"key1": "value1"}, files={"file1": b"content of file 1"}, boundary=b"2256d3a36d2a61a1eba35a22bee5c74a"))
with httpx.Client() as client:
assert client.get("https://test_url").text == '''--2256d3a36d2a61a1eba35a22bee5c74a\r
Content-Disposition: form-data; name="key1"\r
\r
value1\r
--2256d3a36d2a61a1eba35a22bee5c74a\r
Content-Disposition: form-data; name="file1"; filename="upload"\r
Content-Type: application/octet-stream\r
\r
content of file 1\r
--2256d3a36d2a61a1eba35a22bee5c74a--\r
'''
```
### Add non 200 response
Use `status_code` parameter to specify the HTTP status code (as an int) of the response.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_status_code(httpx_mock: HTTPXMock):
httpx_mock.add_response(status_code=404)
with httpx.Client() as client:
assert client.get("https://test_url").status_code == 404
```
### Reply with custom headers
Use `headers` parameter to specify the extra headers of the response.
Any valid httpx headers type is supported, you can submit headers as a dict (str or bytes), a list of 2-tuples (str or bytes) or a [`httpx.Header`](https://www.python-httpx.org/api/#headers) instance.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_headers_as_str_dict(httpx_mock: HTTPXMock):
httpx_mock.add_response(headers={"X-Header1": "Test value"})
with httpx.Client() as client:
assert client.get("https://test_url").headers["x-header1"] == "Test value"
def test_headers_as_str_tuple_list(httpx_mock: HTTPXMock):
httpx_mock.add_response(headers=[("X-Header1", "Test value")])
with httpx.Client() as client:
assert client.get("https://test_url").headers["x-header1"] == "Test value"
def test_headers_as_httpx_headers(httpx_mock: HTTPXMock):
httpx_mock.add_response(headers=httpx.Headers({b"X-Header1": b"Test value"}))
with httpx.Client() as client:
assert client.get("https://test_url").headers["x-header1"] == "Test value"
```
#### Reply with cookies
Cookies are sent in the `set-cookie` HTTP header.
You can then send cookies in the response by setting the `set-cookie` header with [the value following key=value format]((https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)).
```python
import httpx
from pytest_httpx import HTTPXMock
def test_cookie(httpx_mock: HTTPXMock):
httpx_mock.add_response(headers={"set-cookie": "key=value"})
with httpx.Client() as client:
response = client.get("https://test_url")
assert dict(response.cookies) == {"key": "value"}
def test_cookies(httpx_mock: HTTPXMock):
httpx_mock.add_response(headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")])
with httpx.Client() as client:
response = client.get("https://test_url")
assert dict(response.cookies) == {"key": "value", "key2": "value2"}
```
### Add HTTP/2.0 response
Use `http_version` parameter to specify the HTTP protocol version (as a string) of the response.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_http_version(httpx_mock: HTTPXMock):
httpx_mock.add_response(http_version="HTTP/2.0")
with httpx.Client() as client:
assert client.get("https://test_url").http_version == "HTTP/2.0"
```
## Add callbacks
You can perform custom manipulation upon request reception by registering callbacks.
Callback should expect one parameter, the received [`httpx.Request`](https://www.python-httpx.org/api/#request).
If all callbacks are not executed during test execution, the test case will fail at teardown [(unless you turned `assert_all_responses_were_requested` option off)](#allow-to-register-more-responses-than-what-will-be-requested).
Note that callbacks are considered as responses, and thus are [selected the same way](#how-response-is-selected).
Meaning that you can transpose `httpx_mock.add_response` calls in the related examples into `httpx_mock.add_callback`.
### Dynamic responses
Callback should return a [`httpx.Response`](https://www.python-httpx.org/api/#response) instance.
```python
import httpx
from pytest_httpx import HTTPXMock
def test_dynamic_response(httpx_mock: HTTPXMock):
def custom_response(request: httpx.Request):
return httpx.Response(
status_code=200, json={"url": str(request.url)},
)
httpx_mock.add_callback(custom_response)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.json() == {"url": "https://test_url"}
```
Alternatively, callbacks can also be asynchronous.
As in the following sample simulating network latency on some responses only.
```python
import asyncio
import httpx
import pytest
from pytest_httpx import HTTPXMock
@pytest.mark.asyncio
async def test_dynamic_async_response(httpx_mock: HTTPXMock):
async def simulate_network_latency(request: httpx.Request):
await asyncio.sleep(1)
return httpx.Response(
status_code=200, json={"url": str(request.url)},
)
httpx_mock.add_callback(simulate_network_latency)
httpx_mock.add_response()
async with httpx.AsyncClient() as client:
responses = await asyncio.gather(
# Response will be received after one second
client.get("https://test_url"),
# Response will instantly be received (1 second before the first request)
client.get("https://test_url")
)
```
### Raising exceptions
You can simulate HTTPX exception throwing by raising an exception in your callback or use `httpx_mock.add_exception` with the exception instance.
This can be useful if you want to assert that your code handles HTTPX exceptions properly.
```python
import httpx
import pytest
from pytest_httpx import HTTPXMock
def test_exception_raising(httpx_mock: HTTPXMock):
httpx_mock.add_exception(httpx.ReadTimeout("Unable to read within timeout"))
with httpx.Client() as client:
with pytest.raises(httpx.ReadTimeout):
client.get("https://test_url")
```
#### In case no response can be found
The default behavior is to instantly raise a [`httpx.TimeoutException`](https://www.python-httpx.org/advanced/timeouts/) in case no matching response can be found.
The exception message will display the request and every registered responses to help you identify any possible mismatch.
```python
import httpx
import pytest
from pytest_httpx import HTTPXMock
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_timeout(httpx_mock: HTTPXMock):
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException):
client.get("https://test_url")
```
## Check sent requests
The best way to ensure the content of your requests is still to use the `match_headers` and / or `match_content` parameters when adding a response.
In the same spirit, ensuring that no request was issued does not necessarily require any code [(unless you turned `assert_all_requests_were_expected` option off)](#allow-to-not-register-responses-for-every-request).
In any case, you always have the ability to retrieve the requests that were issued.
As in the following samples:
```python
import httpx
from pytest_httpx import HTTPXMock
def test_many_requests(httpx_mock: HTTPXMock):
httpx_mock.add_response()
with httpx.Client() as client:
response1 = client.get("https://test_url")
response2 = client.get("https://test_url")
requests = httpx_mock.get_requests()
def test_single_request(httpx_mock: HTTPXMock):
httpx_mock.add_response()
with httpx.Client() as client:
response = client.get("https://test_url")
request = httpx_mock.get_request()
def test_no_request(httpx_mock: HTTPXMock):
assert not httpx_mock.get_request()
```
### How requests are selected
You can add criteria so that requests will be returned only in case of a more specific matching.
Note that requests are [selected the same way as responses](#how-response-is-selected).
Meaning that you can transpose `httpx_mock.add_response` calls in the related examples into `httpx_mock.get_requests` or `httpx_mock.get_request`.
## Configuring httpx_mock
The `httpx_mock` marker is available and can be used to change the default behavior of the `httpx_mock` fixture.
Refer to [available options](#available-options) for an exhaustive list of options that can be set [per test](#per-test), [per module](#per-module) or even [on the whole test suite](#for-the-whole-test-suite).
### Per test
```python
import pytest
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
def test_something(httpx_mock):
...
```
### Per module
```python
import pytest
pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
```
### For the whole test suite
This should be set in the root `conftest.py` file.
```python
import pytest
def pytest_collection_modifyitems(session, config, items):
for item in items:
item.add_marker(pytest.mark.httpx_mock(assert_all_responses_were_requested=False))
```
> [!IMPORTANT]
> Note that [there currently is a bug in pytest](https://github.com/pytest-dev/pytest/issues/10406) where `pytest_collection_modifyitems` will actually add the marker AFTER its `module` and `class` registration.
>
> Meaning the order is currently:
> module -> class -> test suite -> test
>
> instead of:
> test suite -> module -> class -> test
### Available options
#### Allow to register more responses than what will be requested
By default, `pytest-httpx` will ensure that every response was requested during test execution.
If you want to add an optional response, you can use the `is_optional` parameter when [registering a response](#add-responses) or [a callback](#add-callbacks).
```python
def test_fewer_requests_than_expected(httpx_mock):
# Even if this response never received a corresponding request, the test will not fail at teardown
httpx_mock.add_response(is_optional=True)
```
If you don't have control over the response registration process (shared fixtures),
and you want to allow fewer requests than what you registered responses for,
you can use the `httpx_mock` marker `assert_all_responses_were_requested` option.
> [!CAUTION]
> Use this option at your own risk of not spotting regression (requests not sent) in your code base!
```python
import pytest
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
def test_fewer_requests_than_expected(httpx_mock):
# Even if this response never received a corresponding request, the test will not fail at teardown
httpx_mock.add_response()
```
Note that the `is_optional` parameter will take precedence over the `assert_all_responses_were_requested` option.
Meaning you can still register a response that will be checked for execution at teardown even if `assert_all_responses_were_requested` was set to `False`.
```python
import pytest
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
def test_force_expected_request(httpx_mock):
# Even if the assert_all_responses_were_requested option is set, the test will fail at teardown if this is not matched
httpx_mock.add_response(is_optional=False)
```
#### Allow to not register responses for every request
By default, `pytest-httpx` will ensure that every request that was issued was expected.
You can use the `httpx_mock` marker `assert_all_requests_were_expected` option to allow more requests than what you registered responses for.
> [!CAUTION]
> Use this option at your own risk of not spotting regression (unexpected requests) in your code base!
```python
import pytest
import httpx
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_more_requests_than_expected(httpx_mock):
with httpx.Client() as client:
# Even if this request was not expected, the test will not fail at teardown
with pytest.raises(httpx.TimeoutException):
client.get("https://test_url")
```
#### Allow to register a response for more than one request
By default, `pytest-httpx` will ensure that every request that was issued was expected.
If you want to add a response once, while allowing it to match more than once, you can use the `is_reusable` parameter when [registering a response](#add-responses) or [a callback](#add-callbacks).
```python
import httpx
def test_more_requests_than_responses(httpx_mock):
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.get("https://test_url")
# Even if only one response was registered, the test will not fail at teardown as this request will also be matched
client.get("https://test_url")
```
If you don't have control over the response registration process (shared fixtures),
and you want to allow multiple requests to match the same registered response,
you can use the `httpx_mock` marker `can_send_already_matched_responses` option.
With this option, in case all matching responses have been sent at least once, the last one (according to the registration order) will be sent.
> [!CAUTION]
> Use this option at your own risk of not spotting regression (requests issued more than the expected number of times) in your code base!
```python
import pytest
import httpx
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_more_requests_than_responses(httpx_mock):
httpx_mock.add_response()
with httpx.Client() as client:
client.get("https://test_url")
# Even if only one response was registered, the test will not fail at teardown as this request will also be matched
client.get("https://test_url")
```
#### Do not mock some requests
By default, `pytest-httpx` will mock every request.
But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through.
To do so, you can use the `httpx_mock` marker `should_mock` option and provide a callable expecting the [`httpx.Request`](https://www.python-httpx.org/api/#request) as parameter and returning a boolean.
Returning `True` will ensure that the request is handled by `pytest-httpx` (mocked), `False` will let the request pass through (not mocked).
```python
import pytest
import httpx
@pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "www.my_local_test_host")
def test_partial_mock(httpx_mock):
httpx_mock.add_response()
with httpx.Client() as client:
# This request will NOT be mocked
response1 = client.get("https://www.my_local_test_host/sub?param=value")
# This request will be mocked
response2 = client.get("https://test_url")
```
## Migrating to pytest-httpx
Here is how to migrate from well-known testing libraries to `pytest-httpx`.
### From responses
| Feature | responses | pytest-httpx |
|:------------------|:---------------------------|:----------------------------|
| Add a response | `responses.add()` | `httpx_mock.add_response()` |
| Add a callback | `responses.add_callback()` | `httpx_mock.add_callback()` |
| Retrieve requests | `responses.calls` | `httpx_mock.get_requests()` |
#### Add a response or a callback
Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`.
Below is a list of parameters that will require a change in your code.
| Parameter | responses | pytest-httpx |
|:---------------------|:------------------------------------|:---------------------------------------------------------------------|
| method | `method=responses.GET` | `method="GET"` |
| body (as bytes) | `body=b"sample"` | `content=b"sample"` |
| body (as str) | `body="sample"` | `text="sample"` |
| status code | `status=201` | `status_code=201` |
| headers | `adding_headers={"name": "value"}` | `headers={"name": "value"}` |
| content-type header | `content_type="application/custom"` | `headers={"content-type": "application/custom"}` |
| Match the full query | `match_querystring=True` | The full query is always matched when providing the `url` parameter. |
Sample adding a response with `responses`:
```python
from responses import RequestsMock
def test_response(responses: RequestsMock):
responses.add(
method=responses.GET,
url="https://test_url",
body=b"This is the response content",
status=400,
)
```
Sample adding the same response with `pytest-httpx`:
```python
from pytest_httpx import HTTPXMock
def test_response(httpx_mock: HTTPXMock):
httpx_mock.add_response(
method="GET",
url="https://test_url",
content=b"This is the response content",
status_code=400,
)
```
### From aioresponses
| Feature | aioresponses | pytest-httpx |
|:---------------|:------------------------|:-------------------------------------------|
| Add a response | `aioresponses.method()` | `httpx_mock.add_response(method="METHOD")` |
| Add a callback | `aioresponses.method()` | `httpx_mock.add_callback(method="METHOD")` |
#### Add a response or a callback
Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`.
Below is a list of parameters that will require a change in your code.
| Parameter | responses | pytest-httpx |
|:----------------|:---------------------|:--------------------|
| body (as bytes) | `body=b"sample"` | `content=b"sample"` |
| body (as str) | `body="sample"` | `text="sample"` |
| body (as JSON) | `payload=["sample"]` | `json=["sample"]` |
| status code | `status=201` | `status_code=201` |
Sample adding a response with `aioresponses`:
```python
import pytest
from aioresponses import aioresponses
@pytest.fixture
def mock_aioresponse():
with aioresponses() as m:
yield m
def test_response(mock_aioresponse):
mock_aioresponse.get(
url="https://test_url",
body=b"This is the response content",
status=400,
)
```
Sample adding the same response with `pytest-httpx`:
```python
def test_response(httpx_mock):
httpx_mock.add_response(
method="GET",
url="https://test_url",
content=b"This is the response content",
status_code=400,
)
```
pytest_httpx-0.35.0/_config.yml 0000664 0000000 0000000 00000000032 14722140600 0016474 0 ustar 00root root 0000000 0000000 theme: jekyll-theme-cayman pytest_httpx-0.35.0/pyproject.toml 0000664 0000000 0000000 00000003610 14722140600 0017266 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["setuptools", "setuptools_scm"]
build-backend = "setuptools.build_meta"
[project]
name = "pytest-httpx"
description = "Send responses to httpx."
readme = "README.md"
requires-python = ">=3.9"
license = {file = "LICENSE"}
authors = [
{ name = "Colin Bounouar", email = "colin.bounouar.dev@gmail.com" },
]
maintainers = [
{ name = "Colin Bounouar", email = "colin.bounouar.dev@gmail.com" },
]
keywords = [
"httpx",
"pytest",
"testing",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: Pytest",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Software Development :: Build Tools",
"Typing :: Typed",
]
dependencies = [
"httpx==0.28.*",
"pytest==8.*",
]
dynamic = ["version"]
[project.urls]
documentation = "https://colin-b.github.io/pytest_httpx/"
repository = "https://github.com/Colin-b/pytest_httpx"
changelog = "https://github.com/Colin-b/pytest_httpx/blob/master/CHANGELOG.md"
issues = "https://github.com/Colin-b/pytest_httpx/issues"
[project.optional-dependencies]
testing = [
# Used to check coverage
"pytest-cov==6.*",
# Used to run async tests
"pytest-asyncio==0.24.*",
]
[project.entry-points.pytest11]
pytest_httpx = "pytest_httpx"
[tool.setuptools.dynamic]
version = {attr = "pytest_httpx.version.__version__"}
[tool.pytest.ini_options]
# Silence deprecation warnings about option "asyncio_default_fixture_loop_scope"
asyncio_default_fixture_loop_scope = "function"
pytest_httpx-0.35.0/pytest_httpx/ 0000775 0000000 0000000 00000000000 14722140600 0017131 5 ustar 00root root 0000000 0000000 pytest_httpx-0.35.0/pytest_httpx/__init__.py 0000664 0000000 0000000 00000004345 14722140600 0021250 0 ustar 00root root 0000000 0000000 from collections.abc import Generator
from operator import methodcaller
import httpx
import pytest
from pytest import Config, FixtureRequest, MonkeyPatch
from pytest_httpx._httpx_mock import HTTPXMock
from pytest_httpx._httpx_internals import IteratorStream
from pytest_httpx._options import _HTTPXMockOptions
from pytest_httpx.version import __version__
__all__ = (
"HTTPXMock",
"IteratorStream",
"__version__",
)
@pytest.fixture
def httpx_mock(
monkeypatch: MonkeyPatch,
request: FixtureRequest,
) -> Generator[HTTPXMock, None, None]:
options = {}
for marker in request.node.iter_markers("httpx_mock"):
options = marker.kwargs | options
__tracebackhide__ = methodcaller("errisinstance", TypeError)
options = _HTTPXMockOptions(**options)
mock = HTTPXMock(options)
# Mock synchronous requests
real_handle_request = httpx.HTTPTransport.handle_request
def mocked_handle_request(
transport: httpx.HTTPTransport, request: httpx.Request
) -> httpx.Response:
if options.should_mock(request):
return mock._handle_request(transport, request)
return real_handle_request(transport, request)
monkeypatch.setattr(
httpx.HTTPTransport,
"handle_request",
mocked_handle_request,
)
# Mock asynchronous requests
real_handle_async_request = httpx.AsyncHTTPTransport.handle_async_request
async def mocked_handle_async_request(
transport: httpx.AsyncHTTPTransport, request: httpx.Request
) -> httpx.Response:
if options.should_mock(request):
return await mock._handle_async_request(transport, request)
return await real_handle_async_request(transport, request)
monkeypatch.setattr(
httpx.AsyncHTTPTransport,
"handle_async_request",
mocked_handle_async_request,
)
yield mock
try:
mock._assert_options()
finally:
mock.reset()
def pytest_configure(config: Config) -> None:
config.addinivalue_line(
"markers",
"httpx_mock(*, assert_all_responses_were_requested=True, assert_all_requests_were_expected=True, can_send_already_matched_responses=False, should_mock=lambda request: True): Configure httpx_mock fixture.",
)
pytest_httpx-0.35.0/pytest_httpx/_httpx_internals.py 0000664 0000000 0000000 00000003442 14722140600 0023073 0 ustar 00root root 0000000 0000000 import base64
from typing import Union, Optional
from collections.abc import Sequence, Iterable, AsyncIterator, Iterator
import httpcore
import httpx
# TODO Get rid of this internal import
from httpx._content import IteratorByteStream, AsyncIteratorByteStream
# Those types are internally defined within httpx._types
HeaderTypes = Union[
httpx.Headers,
dict[str, str],
dict[bytes, bytes],
Sequence[tuple[str, str]],
Sequence[tuple[bytes, bytes]],
]
class IteratorStream(AsyncIteratorByteStream, IteratorByteStream):
def __init__(self, stream: Iterable[bytes]):
class Stream:
def __iter__(self) -> Iterator[bytes]:
yield from stream
async def __aiter__(self) -> AsyncIterator[bytes]:
for chunk in stream:
yield chunk
AsyncIteratorByteStream.__init__(self, stream=Stream())
IteratorByteStream.__init__(self, stream=Stream())
def _to_httpx_url(url: httpcore.URL, headers: list[tuple[bytes, bytes]]) -> httpx.URL:
for name, value in headers:
if b"Proxy-Authorization" == name:
return httpx.URL(
scheme=url.scheme.decode(),
host=url.host.decode(),
port=url.port,
raw_path=url.target,
userinfo=base64.b64decode(value[6:]),
)
return httpx.URL(
scheme=url.scheme.decode(),
host=url.host.decode(),
port=url.port,
raw_path=url.target,
)
def _proxy_url(
real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport]
) -> Optional[httpx.URL]:
if isinstance(
real_pool := real_transport._pool, (httpcore.HTTPProxy, httpcore.AsyncHTTPProxy)
):
return _to_httpx_url(real_pool._proxy_url, real_pool._proxy_headers)
pytest_httpx-0.35.0/pytest_httpx/_httpx_mock.py 0000664 0000000 0000000 00000042225 14722140600 0022027 0 ustar 00root root 0000000 0000000 import copy
import inspect
from typing import Union, Optional, Callable, Any, NoReturn
from collections.abc import Awaitable
import httpx
from pytest_httpx import _httpx_internals
from pytest_httpx._options import _HTTPXMockOptions
from pytest_httpx._pretty_print import RequestDescription
from pytest_httpx._request_matcher import _RequestMatcher
class HTTPXMock:
"""
This class is only exposed for `httpx_mock` fixture type hinting purpose.
"""
def __init__(self, options: _HTTPXMockOptions) -> None:
"""Private and subject to breaking changes without notice."""
self._options = options
self._requests: list[
tuple[Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport], httpx.Request]
] = []
self._callbacks: list[
tuple[
_RequestMatcher,
Callable[
[httpx.Request],
Union[
Optional[httpx.Response], Awaitable[Optional[httpx.Response]]
],
],
]
] = []
self._requests_not_matched: list[httpx.Request] = []
def add_response(
self,
status_code: int = 200,
http_version: str = "HTTP/1.1",
headers: Optional[_httpx_internals.HeaderTypes] = None,
content: Optional[bytes] = None,
text: Optional[str] = None,
html: Optional[str] = None,
stream: Any = None,
json: Any = None,
**matchers: Any,
) -> None:
"""
Mock the response that will be sent if a request match.
:param status_code: HTTP status code of the response to send. Default to 200 (OK).
:param http_version: HTTP protocol version of the response to send. Default to HTTP/1.1
:param headers: HTTP headers of the response to send. Default to no headers.
:param content: HTTP body of the response (as bytes).
:param text: HTTP body of the response (as string).
:param html: HTTP body of the response (as HTML string content).
:param stream: HTTP body of the response (as httpx.SyncByteStream or httpx.AsyncByteStream) as stream content.
:param json: HTTP body of the response (if JSON should be used as content type) if data is not provided.
:param url: Full URL identifying the request(s) to match.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the request(s) to match.
:param proxy_url: Full proxy URL identifying the request(s) to match.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
:param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable.
:param match_data: Multipart data (excluding files) identifying the request(s) to match. Must be a dictionary.
:param match_files: Multipart files identifying the request(s) to match. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param is_optional: True will mark this response as optional, False will expect a request matching it. Must be a boolean. Default to the opposite of assert_all_responses_were_requested option value (itself defaulting to True, meaning this parameter default to False).
:param is_reusable: True will allow re-using this response even if it already matched, False prevent re-using it. Must be a boolean. Default to the can_send_already_matched_responses option value (itself defaulting to False).
"""
json = copy.deepcopy(json) if json is not None else None
def response_callback(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=status_code,
extensions={"http_version": http_version.encode("ascii")},
headers=headers,
json=json,
content=content,
text=text,
html=html,
stream=stream,
)
self.add_callback(response_callback, **matchers)
def add_callback(
self,
callback: Callable[
[httpx.Request],
Union[Optional[httpx.Response], Awaitable[Optional[httpx.Response]]],
],
**matchers: Any,
) -> None:
"""
Mock the action that will take place if a request match.
:param callback: The callable that will be called upon reception of the matched request.
It must expect one parameter, the received httpx.Request and should return a httpx.Response.
:param url: Full URL identifying the request(s) to match.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the request(s) to match.
:param proxy_url: Full proxy URL identifying the request(s) to match.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
:param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable.
:param match_data: Multipart data (excluding files) identifying the request(s) to match. Must be a dictionary.
:param match_files: Multipart files identifying the request(s) to match. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param match_extensions: Extensions identifying the request(s) to match. Must be a dictionary.
:param is_optional: True will mark this callback as optional, False will expect a request matching it. Must be a boolean. Default to the opposite of assert_all_responses_were_requested option value (itself defaulting to True, meaning this parameter default to False).
:param is_reusable: True will allow re-using this callback even if it already matched, False prevent re-using it. Must be a boolean. Default to the can_send_already_matched_responses option value (itself defaulting to False).
"""
self._callbacks.append((_RequestMatcher(self._options, **matchers), callback))
def add_exception(self, exception: Exception, **matchers: Any) -> None:
"""
Raise an exception if a request match.
:param exception: The exception that will be raised upon reception of the matched request.
:param url: Full URL identifying the request(s) to match.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the request(s) to match.
:param proxy_url: Full proxy URL identifying the request(s) to match.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
:param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable.
:param match_data: Multipart data (excluding files) identifying the request(s) to match. Must be a dictionary.
:param match_files: Multipart files identifying the request(s) to match. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param match_extensions: Extensions identifying the request(s) to match. Must be a dictionary.
:param is_optional: True will mark this exception response as optional, False will expect a request matching it. Must be a boolean. Default to the opposite of assert_all_responses_were_requested option value (itself defaulting to True, meaning this parameter default to False).
:param is_reusable: True will allow re-using this exception response even if it already matched, False prevent re-using it. Must be a boolean. Default to the can_send_already_matched_responses option value (itself defaulting to False).
"""
def exception_callback(request: httpx.Request) -> None:
if isinstance(exception, httpx.RequestError):
exception.request = request
raise exception
self.add_callback(exception_callback, **matchers)
def _handle_request(
self,
real_transport: httpx.HTTPTransport,
request: httpx.Request,
) -> httpx.Response:
# Store the content in request for future matching
request.read()
self._requests.append((real_transport, request))
callback = self._get_callback(real_transport, request)
if callback:
response = callback(request)
if response:
return _unread(response)
self._request_not_matched(real_transport, request)
async def _handle_async_request(
self,
real_transport: httpx.AsyncHTTPTransport,
request: httpx.Request,
) -> httpx.Response:
# Store the content in request for future matching
await request.aread()
self._requests.append((real_transport, request))
callback = self._get_callback(real_transport, request)
if callback:
response = callback(request)
if response:
if inspect.isawaitable(response):
response = await response
return _unread(response)
self._request_not_matched(real_transport, request)
def _request_not_matched(
self,
real_transport: Union[httpx.AsyncHTTPTransport, httpx.HTTPTransport],
request: httpx.Request,
) -> NoReturn:
self._requests_not_matched.append(request)
raise httpx.TimeoutException(
self._explain_that_no_response_was_found(real_transport, request),
request=request,
)
def _explain_that_no_response_was_found(
self,
real_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport],
request: httpx.Request,
) -> str:
matchers = [matcher for matcher, _ in self._callbacks]
message = f"No response can be found for {RequestDescription(real_transport, request, matchers)}"
already_matched = []
unmatched = []
for matcher in matchers:
if matcher.nb_calls:
already_matched.append(matcher)
else:
unmatched.append(matcher)
matchers_description = "\n".join(
[f"- {matcher}" for matcher in unmatched + already_matched]
)
if matchers_description:
message += f" amongst:\n{matchers_description}"
# If we could not find a response, but we have already matched responses
# it might be that user is expecting one of those responses to be reused
if any(not matcher.is_reusable for matcher in already_matched):
message += "\n\nIf you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request"
return message
def _get_callback(
self,
real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport],
request: httpx.Request,
) -> Optional[
Callable[
[httpx.Request],
Union[Optional[httpx.Response], Awaitable[Optional[httpx.Response]]],
]
]:
callbacks = [
(matcher, callback)
for matcher, callback in self._callbacks
if matcher.match(real_transport, request)
]
# No callback match this request
if not callbacks:
return None
# Callbacks match this request
for matcher, callback in callbacks:
# Return the first not yet called
if not matcher.nb_calls:
matcher.nb_calls += 1
return callback
# Or the last registered (if it can be reused)
if matcher.is_reusable:
matcher.nb_calls += 1
return callback
# All callbacks have already been matched and last registered cannot be reused
return None
def get_requests(self, **matchers: Any) -> list[httpx.Request]:
"""
Return all requests sent that match (empty list if no requests were matched).
:param url: Full URL identifying the requests to retrieve.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the requests to retrieve. Must be an upper-cased string value.
:param proxy_url: Full proxy URL identifying the requests to retrieve.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param match_headers: HTTP headers identifying the requests to retrieve. Must be a dictionary.
:param match_content: Full HTTP body identifying the requests to retrieve. Must be bytes.
:param match_json: JSON decoded HTTP body identifying the requests to retrieve. Must be JSON encodable.
:param match_data: Multipart data (excluding files) identifying the requests to retrieve. Must be a dictionary.
:param match_files: Multipart files identifying the requests to retrieve. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param match_extensions: Extensions identifying the requests to retrieve. Must be a dictionary.
"""
matcher = _RequestMatcher(self._options, **matchers)
return [
request
for real_transport, request in self._requests
if matcher.match(real_transport, request)
]
def get_request(self, **matchers: Any) -> Optional[httpx.Request]:
"""
Return the single request that match (or None).
:param url: Full URL identifying the request to retrieve.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the request to retrieve. Must be an upper-cased string value.
:param proxy_url: Full proxy URL identifying the request to retrieve.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param match_headers: HTTP headers identifying the request to retrieve. Must be a dictionary.
:param match_content: Full HTTP body identifying the request to retrieve. Must be bytes.
:param match_json: JSON decoded HTTP body identifying the request to retrieve. Must be JSON encodable.
:param match_data: Multipart data (excluding files) identifying the request to retrieve. Must be a dictionary.
:param match_files: Multipart files identifying the request to retrieve. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param match_extensions: Extensions identifying the request to retrieve. Must be a dictionary.
:raises AssertionError: in case more than one request match.
"""
requests = self.get_requests(**matchers)
assert (
len(requests) <= 1
), f"More than one request ({len(requests)}) matched, use get_requests instead or refine your filters."
return requests[0] if requests else None
def reset(self) -> None:
self._requests.clear()
self._callbacks.clear()
self._requests_not_matched.clear()
def _assert_options(self) -> None:
callbacks_not_executed = [
matcher for matcher, _ in self._callbacks if matcher.should_have_matched()
]
matchers_description = "\n".join(
[f"- {matcher}" for matcher in callbacks_not_executed]
)
assert not callbacks_not_executed, (
"The following responses are mocked but not requested:\n"
f"{matchers_description}\n"
"\n"
"If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested"
)
if self._options.assert_all_requests_were_expected:
requests_description = "\n".join(
[
f"- {request.method} request on {request.url}"
for request in self._requests_not_matched
]
)
assert not self._requests_not_matched, (
f"The following requests were not expected:\n"
f"{requests_description}\n"
"\n"
"If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request"
)
def _unread(response: httpx.Response) -> httpx.Response:
# Allow to read the response on client side
response.is_stream_consumed = False
response.is_closed = False
if hasattr(response, "_content"):
del response._content
return response
pytest_httpx-0.35.0/pytest_httpx/_options.py 0000664 0000000 0000000 00000001236 14722140600 0021337 0 ustar 00root root 0000000 0000000 from typing import Callable
import httpx
class _HTTPXMockOptions:
def __init__(
self,
*,
assert_all_responses_were_requested: bool = True,
assert_all_requests_were_expected: bool = True,
can_send_already_matched_responses: bool = False,
should_mock: Callable[[httpx.Request], bool] = lambda request: True,
) -> None:
self.assert_all_responses_were_requested = assert_all_responses_were_requested
self.assert_all_requests_were_expected = assert_all_requests_were_expected
self.can_send_already_matched_responses = can_send_already_matched_responses
self.should_mock = should_mock
pytest_httpx-0.35.0/pytest_httpx/_pretty_print.py 0000664 0000000 0000000 00000005423 14722140600 0022411 0 ustar 00root root 0000000 0000000 from typing import Union
import httpx
from pytest_httpx._httpx_internals import _proxy_url
from pytest_httpx._request_matcher import _RequestMatcher
class RequestDescription:
def __init__(
self,
real_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport],
request: httpx.Request,
matchers: list[_RequestMatcher],
):
self.real_transport = real_transport
self.request = request
headers_encoding = request.headers.encoding
self.expected_headers = {
# httpx uses lower cased header names as internal key
header.lower().encode(headers_encoding)
for matcher in matchers
if matcher.headers
for header in matcher.headers
}
self.expect_body = any([matcher.expect_body() for matcher in matchers])
self.expect_proxy = any([matcher.proxy_url is not None for matcher in matchers])
self.expected_extensions = {
extension
for matcher in matchers
if matcher.extensions
for extension in matcher.extensions
}
def __str__(self) -> str:
request_description = f"{self.request.method} request on {self.request.url}"
if extra_description := self.extra_request_description():
request_description += f" with {extra_description}"
return request_description
def extra_request_description(self) -> str:
extra_description = []
if self.expected_headers:
headers_encoding = self.request.headers.encoding
present_headers = {}
# Can be cleaned based on the outcome of https://github.com/encode/httpx/discussions/2841
for name, lower_name, value in self.request.headers._list:
if lower_name in self.expected_headers:
name = name.decode(headers_encoding)
if name in present_headers:
present_headers[name] += f", {value.decode(headers_encoding)}"
else:
present_headers[name] = value.decode(headers_encoding)
extra_description.append(f"{present_headers} headers")
if self.expect_body:
extra_description.append(f"{self.request.read()} body")
if self.expect_proxy:
proxy_url = _proxy_url(self.real_transport)
extra_description.append(f"{proxy_url if proxy_url else 'no'} proxy URL")
if self.expected_extensions:
present_extensions = {
name: value
for name, value in self.request.extensions.items()
if name in self.expected_extensions
}
extra_description.append(f"{present_extensions} extensions")
return " and ".join(extra_description)
pytest_httpx-0.35.0/pytest_httpx/_request_matcher.py 0000664 0000000 0000000 00000017667 14722140600 0023056 0 ustar 00root root 0000000 0000000 import json
import re
from typing import Optional, Union, Any
from re import Pattern
import httpx
from pytest_httpx._httpx_internals import _proxy_url
from pytest_httpx._options import _HTTPXMockOptions
def _url_match(
url_to_match: Union[Pattern[str], httpx.URL], received: httpx.URL
) -> bool:
if isinstance(url_to_match, re.Pattern):
return url_to_match.match(str(received)) is not None
# Compare query parameters apart as order of parameters should not matter
received_params = dict(received.params)
params = dict(url_to_match.params)
# Remove the query parameters from the original URL to compare everything besides query parameters
received_url = received.copy_with(query=None)
url = url_to_match.copy_with(query=None)
return (received_params == params) and (url == received_url)
class _RequestMatcher:
def __init__(
self,
options: _HTTPXMockOptions,
url: Optional[Union[str, Pattern[str], httpx.URL]] = None,
method: Optional[str] = None,
proxy_url: Optional[Union[str, Pattern[str], httpx.URL]] = None,
match_headers: Optional[dict[str, Any]] = None,
match_content: Optional[bytes] = None,
match_json: Optional[Any] = None,
match_data: Optional[dict[str, Any]] = None,
match_files: Optional[Any] = None,
match_extensions: Optional[dict[str, Any]] = None,
is_optional: Optional[bool] = None,
is_reusable: Optional[bool] = None,
):
self._options = options
self.nb_calls = 0
self.url = httpx.URL(url) if url and isinstance(url, str) else url
self.method = method.upper() if method else method
self.headers = match_headers
self.content = match_content
self.json = match_json
self.data = match_data
self.files = match_files
self.proxy_url = (
httpx.URL(proxy_url)
if proxy_url and isinstance(proxy_url, str)
else proxy_url
)
self.extensions = match_extensions
self.is_optional = not options.assert_all_responses_were_requested if is_optional is None else is_optional
self.is_reusable = options.can_send_already_matched_responses if is_reusable is None else is_reusable
if self._is_matching_body_more_than_one_way():
raise ValueError(
"Only one way of matching against the body can be provided. "
"If you want to match against the JSON decoded representation, use match_json. "
"If you want to match against the multipart representation, use match_files (and match_data). "
"Otherwise, use match_content."
)
if self.data and not self.files:
raise ValueError(
"match_data is meant to be used for multipart matching (in conjunction with match_files)."
"Use match_content to match url encoded data."
)
def expect_body(self) -> bool:
matching_ways = [
self.content is not None,
self.json is not None,
self.files is not None,
]
return sum(matching_ways) == 1
def _is_matching_body_more_than_one_way(self) -> bool:
matching_ways = [
self.content is not None,
self.json is not None,
self.files is not None,
]
return sum(matching_ways) > 1
def match(
self,
real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport],
request: httpx.Request,
) -> bool:
return (
self._url_match(request)
and self._method_match(request)
and self._headers_match(request)
and self._content_match(request)
and self._proxy_match(real_transport)
and self._extensions_match(request)
)
def _url_match(self, request: httpx.Request) -> bool:
if not self.url:
return True
return _url_match(self.url, request.url)
def _method_match(self, request: httpx.Request) -> bool:
if not self.method:
return True
return request.method == self.method
def _headers_match(self, request: httpx.Request) -> bool:
if not self.headers:
return True
encoding = request.headers.encoding
request_headers = {}
# Can be cleaned based on the outcome of https://github.com/encode/httpx/discussions/2841
for raw_name, raw_value in request.headers.raw:
if raw_name in request_headers:
request_headers[raw_name] += b", " + raw_value
else:
request_headers[raw_name] = raw_value
return all(
request_headers.get(header_name.encode(encoding))
== header_value.encode(encoding)
for header_name, header_value in self.headers.items()
)
def _content_match(self, request: httpx.Request) -> bool:
if self.content is not None:
return request.content == self.content
if self.json is not None:
try:
# httpx._content.encode_json hard codes utf-8 encoding.
return json.loads(request.content.decode("utf-8")) == self.json
except json.decoder.JSONDecodeError:
return False
if self.files:
if not (
boundary_matched := re.match(b"^--([0-9a-f]*)\r\n", request.content)
):
return False
# Ensure we re-use the same boundary for comparison
boundary = boundary_matched.group(1)
# Prevent internal httpx changes from impacting users not matching on files
from httpx._multipart import MultipartStream
multipart_content = b"".join(
MultipartStream(self.data or {}, self.files, boundary)
)
return request.content == multipart_content
return True
def _proxy_match(
self, real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport]
) -> bool:
if not self.proxy_url:
return True
if real_proxy_url := _proxy_url(real_transport):
return _url_match(self.proxy_url, real_proxy_url)
return False
def _extensions_match(self, request: httpx.Request) -> bool:
if not self.extensions:
return True
return all(
request.extensions.get(extension_name) == extension_value
for extension_name, extension_value in self.extensions.items()
)
def should_have_matched(self) -> bool:
"""Return True if the matcher did not serve its purpose."""
return not self.is_optional and not self.nb_calls
def __str__(self) -> str:
if self.is_reusable:
matcher_description = f"Match {self.method or 'every'} request"
else:
matcher_description = "Already matched" if self.nb_calls else "Match"
matcher_description += f" {self.method or 'any'} request"
if self.url:
matcher_description += f" on {self.url}"
if extra_description := self._extra_description():
matcher_description += f" with {extra_description}"
return matcher_description
def _extra_description(self) -> str:
extra_description = []
if self.headers:
extra_description.append(f"{self.headers} headers")
if self.content is not None:
extra_description.append(f"{self.content} body")
if self.json is not None:
extra_description.append(f"{self.json} json body")
if self.data is not None:
extra_description.append(f"{self.data} multipart data")
if self.files is not None:
extra_description.append(f"{self.files} files")
if self.proxy_url:
extra_description.append(f"{self.proxy_url} proxy URL")
if self.extensions:
extra_description.append(f"{self.extensions} extensions")
return " and ".join(extra_description)
pytest_httpx-0.35.0/pytest_httpx/py.typed 0000664 0000000 0000000 00000000000 14722140600 0020616 0 ustar 00root root 0000000 0000000 pytest_httpx-0.35.0/pytest_httpx/version.py 0000664 0000000 0000000 00000000564 14722140600 0021175 0 ustar 00root root 0000000 0000000 # Version number as Major.Minor.Patch
# The version modification must respect the following rules:
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
__version__ = "0.35.0"
pytest_httpx-0.35.0/tests/ 0000775 0000000 0000000 00000000000 14722140600 0015514 5 ustar 00root root 0000000 0000000 pytest_httpx-0.35.0/tests/__init__.py 0000664 0000000 0000000 00000000000 14722140600 0017613 0 ustar 00root root 0000000 0000000 pytest_httpx-0.35.0/tests/conftest.py 0000664 0000000 0000000 00000000205 14722140600 0017710 0 ustar 00root root 0000000 0000000 # see https://docs.pytest.org/en/documentation-restructure/how-to/writing_plugins.html#testing-plugins
pytest_plugins = ["pytester"]
pytest_httpx-0.35.0/tests/test_httpx_async.py 0000664 0000000 0000000 00000270470 14722140600 0021503 0 ustar 00root root 0000000 0000000 import asyncio
import math
import os
import re
import time
from collections.abc import AsyncIterable
import httpx
import pytest
from pytest import Testdir
from unittest.mock import ANY
import pytest_httpx
from pytest_httpx import HTTPXMock
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_without_response(httpx_mock: HTTPXMock) -> None:
with pytest.raises(Exception) as exception_info:
async with httpx.AsyncClient() as client:
await client.get("https://test_url")
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url"""
)
@pytest.mark.asyncio
async def test_default_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b""
assert response.status_code == 200
assert response.headers == httpx.Headers({})
assert response.http_version == "HTTP/1.1"
@pytest.mark.asyncio
async def test_url_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b""
@pytest.mark.asyncio
async def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b""
response = await client.post("https://test_url")
assert response.content == b""
@pytest.mark.asyncio
async def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?a=1&b=2", is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url?a=1&b=2")
assert response.content == b""
# Parameters order should not matter
response = await client.get("https://test_url?b=2&a=1")
assert response.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_url_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_optional=True)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get("https://test_url2")
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url2 amongst:
- Match any request on https://test_url"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?a=1&a=2", is_optional=True)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
# Same parameter order matters as it corresponds to a list on server side
await client.get("https://test_url?a=2&a=1")
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url?a=2&a=1 amongst:
- Match any request on https://test_url?a=1&a=2"""
)
@pytest.mark.asyncio
async def test_method_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(method="get", is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b""
response = await client.get("https://test_url2")
assert response.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_method_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(method="get", is_optional=True)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url")
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url amongst:
- Match GET request"""
)
@pytest.mark.asyncio
async def test_reusing_one_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", content=b"test content", is_reusable=True
)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b"test content"
response = await client.get("https://test_url")
assert response.content == b"test content"
@pytest.mark.asyncio
async def test_response_with_string_body(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", text="test content")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b"test content"
@pytest.mark.asyncio
async def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", html="
test content")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b"test content"
@pytest.mark.asyncio
async def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
stream=pytest_httpx.IteratorStream([b"part 1", b"part 2"]),
is_reusable=True,
)
async with httpx.AsyncClient() as client:
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == [
b"part 1",
b"part 2",
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
async for _ in response.aiter_raw():
pass # pragma: no cover
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == [
b"part 1",
b"part 2",
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
async for _ in response.aiter_raw():
pass # pragma: no cover
@pytest.mark.asyncio
async def test_content_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
content=b"part 1 and 2",
is_reusable=True,
)
async with httpx.AsyncClient() as client:
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == [
b"part 1 and 2",
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
async for _ in response.aiter_raw():
pass # pragma: no cover
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == [
b"part 1 and 2",
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
async for _ in response.aiter_raw():
pass # pragma: no cover
@pytest.mark.asyncio
async def test_text_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
text="part 1 and 2",
is_reusable=True,
)
async with httpx.AsyncClient() as client:
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == [
b"part 1 and 2",
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
async for _ in response.aiter_raw():
pass # pragma: no cover
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == [
b"part 1 and 2",
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
async for _ in response.aiter_raw():
pass # pragma: no cover
@pytest.mark.asyncio
async def test_default_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == []
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
async for _ in response.aiter_raw():
pass # pragma: no cover
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == []
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
async for _ in response.aiter_raw():
pass # pragma: no cover
@pytest.mark.asyncio
async def test_with_many_responses(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", content=b"test content 1")
httpx_mock.add_response(url="https://test_url", content=b"test content 2")
httpx_mock.add_response(url="https://test_url", content=b"test content 2")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b"test content 1"
response = await client.get("https://test_url")
assert response.content == b"test content 2"
response = await client.get("https://test_url")
assert response.content == b"test content 2"
@pytest.mark.asyncio
async def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", content=b"test content 1")
httpx_mock.add_response(
url="https://test_url", content=b"test content 2", is_reusable=True
)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b"test content 1"
response = await client.get("https://test_url")
assert response.content == b"test content 2"
response = await client.get("https://test_url")
assert response.content == b"test content 2"
@pytest.mark.asyncio
async def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", method="GET", content=b"test content 1"
)
httpx_mock.add_response(
url="https://test_url", method="POST", content=b"test content 2"
)
httpx_mock.add_response(
url="https://test_url", method="PUT", content=b"test content 3"
)
httpx_mock.add_response(
url="https://test_url", method="DELETE", content=b"test content 4"
)
httpx_mock.add_response(
url="https://test_url", method="PATCH", content=b"test content 5"
)
httpx_mock.add_response(
url="https://test_url", method="HEAD", content=b"test content 6"
)
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url")
assert response.content == b"test content 2"
response = await client.get("https://test_url")
assert response.content == b"test content 1"
response = await client.put("https://test_url")
assert response.content == b"test content 3"
response = await client.head("https://test_url")
assert response.content == b"test content 6"
response = await client.patch("https://test_url")
assert response.content == b"test content 5"
response = await client.delete("https://test_url")
assert response.content == b"test content 4"
@pytest.mark.asyncio
async def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", method="GET", content=b"test content 1", status_code=200
)
httpx_mock.add_response(
url="https://test_url",
method="POST",
content=b"test content 2",
status_code=201,
)
httpx_mock.add_response(
url="https://test_url", method="PUT", content=b"test content 3", status_code=202
)
httpx_mock.add_response(
url="https://test_url",
method="DELETE",
content=b"test content 4",
status_code=303,
)
httpx_mock.add_response(
url="https://test_url",
method="PATCH",
content=b"test content 5",
status_code=404,
)
httpx_mock.add_response(
url="https://test_url",
method="HEAD",
content=b"test content 6",
status_code=500,
)
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url")
assert response.content == b"test content 2"
assert response.status_code == 201
response = await client.get("https://test_url")
assert response.content == b"test content 1"
assert response.status_code == 200
response = await client.put("https://test_url")
assert response.content == b"test content 3"
assert response.status_code == 202
response = await client.head("https://test_url")
assert response.content == b"test content 6"
assert response.status_code == 500
response = await client.patch("https://test_url")
assert response.content == b"test content 5"
assert response.status_code == 404
response = await client.delete("https://test_url")
assert response.content == b"test content 4"
assert response.status_code == 303
@pytest.mark.asyncio
async def test_with_many_responses_urls_str(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url?param1=test", method="GET", content=b"test content 1"
)
httpx_mock.add_response(
url="https://test_url?param2=test", method="POST", content=b"test content 2"
)
httpx_mock.add_response(
url="https://test_url?param3=test", method="PUT", content=b"test content 3"
)
httpx_mock.add_response(
url="https://test_url?param4=test", method="DELETE", content=b"test content 4"
)
httpx_mock.add_response(
url="https://test_url?param5=test", method="PATCH", content=b"test content 5"
)
httpx_mock.add_response(
url="https://test_url?param6=test", method="HEAD", content=b"test content 6"
)
async with httpx.AsyncClient() as client:
response = await client.post(
httpx.URL("https://test_url", params={"param2": "test"})
)
assert response.content == b"test content 2"
response = await client.get(
httpx.URL("https://test_url", params={"param1": "test"})
)
assert response.content == b"test content 1"
response = await client.put(
httpx.URL("https://test_url", params={"param3": "test"})
)
assert response.content == b"test content 3"
response = await client.head(
httpx.URL("https://test_url", params={"param6": "test"})
)
assert response.content == b"test content 6"
response = await client.patch(
httpx.URL("https://test_url", params={"param5": "test"})
)
assert response.content == b"test content 5"
response = await client.delete(
httpx.URL("https://test_url", params={"param4": "test"})
)
assert response.content == b"test content 4"
@pytest.mark.asyncio
async def test_response_with_pattern_in_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url=re.compile(".*test.*"))
httpx_mock.add_response(url="https://unmatched", content=b"test content")
async with httpx.AsyncClient() as client:
response = await client.get("https://unmatched")
assert response.content == b"test content"
response = await client.get("https://test_url")
assert response.content == b""
@pytest.mark.asyncio
async def test_request_with_pattern_in_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url")
httpx_mock.add_response(url="https://unmatched")
async with httpx.AsyncClient() as client:
await client.get("https://unmatched")
await client.get("https://test_url", headers={"X-Test": "1"})
assert httpx_mock.get_request(url=re.compile(".*test.*")).headers["x-test"] == "1"
@pytest.mark.asyncio
async def test_requests_with_pattern_in_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url")
httpx_mock.add_response(url="https://tests_url")
httpx_mock.add_response(url="https://unmatched")
async with httpx.AsyncClient() as client:
await client.get("https://tests_url", headers={"X-Test": "1"})
await client.get("https://unmatched", headers={"X-Test": "2"})
await client.get("https://test_url")
requests = httpx_mock.get_requests(url=re.compile(".*test.*"))
assert len(requests) == 2
assert requests[0].headers["x-test"] == "1"
assert "x-test" not in requests[1].headers
@pytest.mark.asyncio
async def test_callback_with_pattern_in_url(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json={"url": str(request.url)})
def custom_response2(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=200,
extensions={"http_version": b"HTTP/2.0"},
json={"url": str(request.url)},
)
httpx_mock.add_callback(custom_response, url=re.compile(".*test.*"))
httpx_mock.add_callback(custom_response2, url="https://unmatched")
async with httpx.AsyncClient() as client:
response = await client.get("https://unmatched")
assert response.http_version == "HTTP/2.0"
response = await client.get("https://test_url")
assert response.http_version == "HTTP/1.1"
@pytest.mark.asyncio
async def test_async_callback_with_await_statement(httpx_mock: HTTPXMock) -> None:
async def simulate_network_latency(request: httpx.Request):
await asyncio.sleep(1)
return httpx.Response(
status_code=200,
json={"url": str(request.url), "time": time.time()},
)
def instant_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=200, json={"url": str(request.url), "time": time.time()}
)
httpx_mock.add_callback(simulate_network_latency)
httpx_mock.add_callback(instant_response)
httpx_mock.add_response(json={"url": "not a callback"})
async with httpx.AsyncClient() as client:
responses = await asyncio.gather(
client.get("https://slow"),
client.get("https://fast_with_callback"),
client.get("https://fast_with_response"),
)
slow_response = responses[0].json()
assert slow_response["url"] == "https://slow"
fast_callback_response = responses[1].json()
assert fast_callback_response["url"] == "https://fast_with_callback"
fast_response = responses[2].json()
assert fast_response["url"] == "not a callback"
# Ensure slow request was properly awaited (did not block subsequent async queries)
assert math.isclose(slow_response["time"], fast_callback_response["time"] + 1)
@pytest.mark.asyncio
async def test_async_callback_with_pattern_in_url(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json={"url": str(request.url)})
async def custom_response2(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=200,
extensions={"http_version": b"HTTP/2.0"},
json={"url": str(request.url)},
)
httpx_mock.add_callback(custom_response, url=re.compile(".*test.*"))
httpx_mock.add_callback(custom_response2, url="https://unmatched")
async with httpx.AsyncClient() as client:
response = await client.get("https://unmatched")
assert response.http_version == "HTTP/2.0"
response = await client.get("https://test_url")
assert response.http_version == "HTTP/1.1"
@pytest.mark.asyncio
async def test_with_many_responses_urls_instances(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param1": "test"}),
method="GET",
content=b"test content 1",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param2": "test"}),
method="POST",
content=b"test content 2",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param3": "test"}),
method="PUT",
content=b"test content 3",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param4": "test"}),
method="DELETE",
content=b"test content 4",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param5": "test"}),
method="PATCH",
content=b"test content 5",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param6": "test"}),
method="HEAD",
content=b"test content 6",
)
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url?param2=test")
assert response.content == b"test content 2"
response = await client.get("https://test_url?param1=test")
assert response.content == b"test content 1"
response = await client.put("https://test_url?param3=test")
assert response.content == b"test content 3"
response = await client.head("https://test_url?param6=test")
assert response.content == b"test content 6"
response = await client.patch("https://test_url?param5=test")
assert response.content == b"test content 5"
response = await client.delete("https://test_url?param4=test")
assert response.content == b"test content 4"
@pytest.mark.asyncio
async def test_with_http_version_2(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", http_version="HTTP/2", content=b"test content 1"
)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b"test content 1"
assert response.http_version == "HTTP/2"
@pytest.mark.asyncio
async def test_with_headers(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
content=b"test content 1",
headers={"X-Test": "Test value"},
)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b"test content 1"
assert response.headers == httpx.Headers(
{"x-test": "Test value", "content-length": "14"}
)
@pytest.mark.asyncio
async def test_requests_retrieval(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", method="GET", content=b"test content 1"
)
httpx_mock.add_response(
url="https://test_url", method="POST", content=b"test content 2"
)
httpx_mock.add_response(
url="https://test_url", method="PUT", content=b"test content 3"
)
httpx_mock.add_response(
url="https://test_url", method="DELETE", content=b"test content 4"
)
httpx_mock.add_response(
url="https://test_url", method="PATCH", content=b"test content 5"
)
httpx_mock.add_response(
url="https://test_url", method="HEAD", content=b"test content 6"
)
async with httpx.AsyncClient() as client:
await client.post("https://test_url", content=b"sent content 2")
await client.get("https://test_url", headers={"X-TEST": "test header 1"})
await client.put("https://test_url", content=b"sent content 3")
await client.head("https://test_url")
await client.patch("https://test_url", content=b"sent content 5")
await client.delete("https://test_url", headers={"X-Test": "test header 4"})
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="PATCH").read()
== b"sent content 5"
)
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="HEAD").read()
== b""
)
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="PUT").read()
== b"sent content 3"
)
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="GET").headers[
"x-test"
]
== "test header 1"
)
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="POST").read()
== b"sent content 2"
)
assert (
httpx_mock.get_request(
url=httpx.URL("https://test_url"), method="DELETE"
).headers["x-test"]
== "test header 4"
)
@pytest.mark.asyncio
async def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_reusable=True)
async with httpx.AsyncClient() as client:
await client.get("https://test_url", headers={"X-TEST": "test header 1"})
await client.get("https://test_url", headers={"X-TEST": "test header 2"})
requests = httpx_mock.get_requests(url=httpx.URL("https://test_url"))
assert len(requests) == 2
assert requests[0].headers["x-test"] == "test header 1"
assert requests[1].headers["x-test"] == "test header 2"
@pytest.mark.asyncio
async def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.get("https://test_url", headers={"X-TEST": "test header 1"})
await client.get("https://test_url2", headers={"X-TEST": "test header 2"})
request = httpx_mock.get_request(url=httpx.URL("https://test_url"))
assert request.headers["x-test"] == "test header 1"
@pytest.mark.asyncio
async def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.get("https://test_url", headers={"X-TEST": "test header 1"})
await client.get("https://test_url2", headers={"X-TEST": "test header 2"})
requests = httpx_mock.get_requests(method="GET")
assert len(requests) == 2
assert requests[0].headers["x-test"] == "test header 1"
assert requests[1].headers["x-test"] == "test header 2"
@pytest.mark.asyncio
async def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.get("https://test_url", headers={"X-TEST": "test header 1"})
await client.post("https://test_url", headers={"X-TEST": "test header 2"})
request = httpx_mock.get_request(method="GET")
assert request.headers["x-test"] == "test header 1"
@pytest.mark.asyncio
async def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.get("https://test_url", headers={"X-TEST": "test header 1"})
await client.get("https://test_url", headers={"X-TEST": "test header 2"})
await client.post("https://test_url", headers={"X-TEST": "test header 3"})
await client.get("https://test_url2", headers={"X-TEST": "test header 4"})
requests = httpx_mock.get_requests(url=httpx.URL("https://test_url"), method="GET")
assert len(requests) == 2
assert requests[0].headers["x-test"] == "test header 1"
assert requests[1].headers["x-test"] == "test header 2"
@pytest.mark.asyncio
async def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.post("https://test_url", headers={"X-TEST": "test header 1"})
await client.get("https://test_url2", headers={"X-TEST": "test header 2"})
requests = httpx_mock.get_requests()
assert len(requests) == 2
assert requests[0].headers["x-test"] == "test header 1"
assert requests[1].headers["x-test"] == "test header 2"
@pytest.mark.asyncio
async def test_default_request_retrieval(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
async with httpx.AsyncClient() as client:
await client.post("https://test_url", headers={"X-TEST": "test header 1"})
request = httpx_mock.get_request()
assert request.headers["x-test"] == "test header 1"
@pytest.mark.asyncio
async def test_requests_json_body(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", method="GET", json=["list content 1", "list content 2"]
)
httpx_mock.add_response(
url="https://test_url",
method="POST",
json={"key 1": "value 1", "key 2": "value 2"},
)
httpx_mock.add_response(url="https://test_url", method="PUT", json="string value")
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url")
assert response.json() == {"key 1": "value 1", "key 2": "value 2"}
assert response.headers["content-type"] == "application/json"
response = await client.get("https://test_url")
assert response.json() == ["list content 1", "list content 2"]
assert response.headers["content-type"] == "application/json"
response = await client.put("https://test_url")
assert response.json() == "string value"
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_callback_raising_exception(httpx_mock: HTTPXMock) -> None:
def raise_timeout(request: httpx.Request) -> httpx.Response:
raise httpx.ReadTimeout(
f"Unable to read within {request.extensions['timeout']['read']}",
request=request,
)
httpx_mock.add_callback(raise_timeout, url="https://test_url")
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.ReadTimeout) as exception_info:
await client.get("https://test_url")
assert str(exception_info.value) == "Unable to read within 5.0"
@pytest.mark.asyncio
async def test_async_callback_raising_exception(httpx_mock: HTTPXMock) -> None:
async def raise_timeout(request: httpx.Request) -> httpx.Response:
raise httpx.ReadTimeout(
f"Unable to read within {request.extensions['timeout']['read']}",
request=request,
)
httpx_mock.add_callback(raise_timeout, url="https://test_url")
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.ReadTimeout) as exception_info:
await client.get("https://test_url")
assert str(exception_info.value) == "Unable to read within 5.0"
@pytest.mark.asyncio
async def test_request_exception_raising(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_exception(
httpx.ReadTimeout("Unable to read within 5.0"), url="https://test_url"
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.ReadTimeout) as exception_info:
await client.get("https://test_url")
assert str(exception_info.value) == "Unable to read within 5.0"
assert exception_info.value.request is not None
@pytest.mark.asyncio
async def test_non_request_exception_raising(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_exception(
httpx.HTTPError("Unable to read within 5.0"), url="https://test_url"
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.HTTPError) as exception_info:
await client.get("https://test_url")
assert str(exception_info.value) == "Unable to read within 5.0"
@pytest.mark.asyncio
async def test_callback_returning_response(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json={"url": str(request.url)})
httpx_mock.add_callback(custom_response, url="https://test_url")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == {"url": "https://test_url"}
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_async_callback_returning_response(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json={"url": str(request.url)})
httpx_mock.add_callback(custom_response, url="https://test_url")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == {"url": "https://test_url"}
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_callback_executed_twice(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
httpx_mock.add_callback(custom_response, is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
response = await client.post("https://test_url")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_async_callback_executed_twice(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
httpx_mock.add_callback(custom_response, is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
response = await client.post("https://test_url")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_callback_registered_after_response(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content2"])
httpx_mock.add_response(json=["content1"])
httpx_mock.add_callback(custom_response, is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == ["content1"]
assert response.headers["content-type"] == "application/json"
response = await client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
# Assert that the last registered callback is sent again even if there is a response
response = await client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_async_callback_registered_after_response(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content2"])
httpx_mock.add_response(json=["content1"])
httpx_mock.add_callback(custom_response, is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == ["content1"]
assert response.headers["content-type"] == "application/json"
response = await client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
# Assert that the last registered callback is sent again even if there is a response
response = await client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_response_registered_after_callback(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content1"])
httpx_mock.add_callback(custom_response)
httpx_mock.add_response(json=["content2"], is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == ["content1"]
assert response.headers["content-type"] == "application/json"
response = await client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
# Assert that the last registered response is sent again even if there is a callback
response = await client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_response_registered_after_async_callback(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content1"])
httpx_mock.add_callback(custom_response)
httpx_mock.add_response(json=["content2"], is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == ["content1"]
assert response.headers["content-type"] == "application/json"
response = await client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
# Assert that the last registered response is sent again even if there is a callback
response = await client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_callback_matching_method(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
httpx_mock.add_callback(custom_response, method="GET", is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
response = await client.get("https://test_url2")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
@pytest.mark.asyncio
async def test_async_callback_matching_method(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
httpx_mock.add_callback(custom_response, method="GET", is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
response = await client.get("https://test_url2")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
def test_request_retrieval_with_more_than_one(testdir: Testdir) -> None:
"""
Single request cannot be returned if there is more than one matching.
"""
testdir.makepyfile(
"""
import httpx
import pytest
@pytest.mark.asyncio
async def test_request_retrieval_with_more_than_one(httpx_mock):
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.get("https://test_url", headers={"X-TEST": "test header 1"})
await client.get("https://test_url", headers={"X-TEST": "test header 2"})
httpx_mock.get_request(url=httpx.URL("https://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 or refine your filters."
]
)
@pytest.mark.asyncio
async def test_headers_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}
)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b""
@pytest.mark.asyncio
async def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"})
async with httpx.AsyncClient() as client:
response = await client.get(
"https://test_url",
headers=[("my-custom-header", "value1"), ("my-custom-header", "value2")],
)
assert response.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_multi_value_headers_not_matching_single_value_issued(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
match_headers={"my-custom-header": "value1"}, is_optional=True
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get(
"https://test_url",
headers=[
("my-custom-header", "value1"),
("my-custom-header", "value2"),
],
)
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst:
- Match any request with {'my-custom-header': 'value1'} headers"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_multi_value_headers_not_matching_multi_value_issued(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
match_headers={"my-custom-header": "value1, value2"}, is_optional=True
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get(
"https://test_url",
headers=[
("my-custom-header", "value1"),
("my-custom-header", "value3"),
],
)
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst:
- Match any request with {'my-custom-header': 'value1, value2'} headers"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={"user-agent": f"python-httpx/{httpx.__version__}"},
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get("https://test_url")
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
- Match any request with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
"Host2": "test_url",
},
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get("https://test_url")
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_url_not_matching_upper_case_headers_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
method="GET",
url="https://test_url?q=b",
match_headers={"MyHeader": "Something"},
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get("https://test_url", headers={"MyHeader": "Something"})
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
- Match GET request on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
@pytest.mark.asyncio
async def test_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_content=b"This is the body")
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url", content=b"This is the body")
assert response.read() == b""
@pytest.mark.asyncio
async def test_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(proxy_url="http://user:pwd@my_other_proxy/")
async with httpx.AsyncClient(proxy="http://user:pwd@my_other_proxy") as client:
response = await client.get("https://test_url")
assert response.read() == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True)
async with httpx.AsyncClient(proxy="http://my_test_proxy") as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get("http://test_url")
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with http://my_test_proxy/ proxy URL amongst:
- Match any request with http://my_test_proxy proxy URL"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get("http://test_url")
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with no proxy URL amongst:
- Match any request with http://my_test_proxy proxy URL"""
)
@pytest.mark.asyncio
async def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.post("https://test_url", content=b"This is the body")
await client.post("https://test_url2", content=b"This is the body")
await client.post("https://test_url2", content=b"This is the body2")
assert len(httpx_mock.get_requests(match_content=b"This is the body")) == 2
@pytest.mark.asyncio
async def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.post("https://test_url", json=["my_str"])
await client.post("https://test_url2", json=["my_str"])
await client.post("https://test_url2", json=["my_str2"])
assert len(httpx_mock.get_requests(match_json=["my_str"])) == 2
@pytest.mark.asyncio
async def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient(
mounts={
"http://": httpx.AsyncHTTPTransport(proxy="http://my_test_proxy"),
"https://": httpx.AsyncHTTPTransport(
proxy="http://user:pwd@my_other_proxy"
),
}
) as client:
await client.get("https://test_url")
await client.get("https://test_url2")
await client.get("http://test_url2")
assert (
len(httpx_mock.get_requests(proxy_url="http://user:pwd@my_other_proxy/")) == 2
)
@pytest.mark.asyncio
async def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient(
mounts={
"http://": httpx.AsyncHTTPTransport(proxy="http://my_test_proxy"),
"https://": httpx.AsyncHTTPTransport(
proxy="http://user:pwd@my_other_proxy"
),
}
) as client:
await client.get("https://test_url")
await client.get("https://test_url2")
await client.get("http://test_url2")
assert httpx_mock.get_request(proxy_url="http://my_test_proxy/")
@pytest.mark.asyncio
async def test_requests_retrieval_files_and_data_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.put(
"https://test_url",
files={"name": ("file_name", b"File content")},
data={"field1": "value"},
)
await client.put(
"https://test_url2",
files={"name": ("file_name", b"File content")},
data={"field": "value"},
)
await client.put(
"http://test_url2",
files={"name": ("file_name", b"File content")},
data={"field": "value"},
)
assert (
len(
httpx_mock.get_requests(
match_files={"name": ("file_name", b"File content")},
match_data={"field": "value"},
)
)
== 2
)
@pytest.mark.asyncio
async def test_request_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.put(
"https://test_url",
files={"name": ("file_name", b"File content")},
data={"field": "value"},
)
await client.get("https://test_url2")
await client.get("http://test_url2")
assert httpx_mock.get_request(
match_files={"name": ("file_name", b"File content")},
match_data={"field": "value"},
)
@pytest.mark.asyncio
async def test_requests_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.get("https://test_url")
await client.get("https://test_url2", timeout=10)
await client.get("https://test_url2", timeout=10)
assert (
len(
httpx_mock.get_requests(
match_extensions={
"timeout": {"connect": 10, "read": 10, "write": 10, "pool": 10}
}
)
)
== 2
)
@pytest.mark.asyncio
async def test_request_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
async with httpx.AsyncClient() as client:
await client.get("https://test_url", timeout=httpx.Timeout(5, read=10))
await client.get("https://test_url2", timeout=10)
await client.get("http://test_url2", timeout=10)
assert httpx_mock.get_request(
match_extensions={"timeout": {"connect": 5, "read": 10, "write": 5, "pool": 5}}
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_content=b"This is the body", is_optional=True)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body2")
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'This is the body2' body amongst:
- Match any request with b'This is the body' body"""
)
@pytest.mark.asyncio
async def test_json_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url", json={"b": 2, "a": 1})
assert response.read() == b""
@pytest.mark.asyncio
async def test_json_partial_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": ANY})
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url", json={"b": 2, "a": 1})
assert response.read() == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'{"c":3,"b":2,"a":1}' body amongst:
- Match any request with {'a': 1, 'b': 2} json body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_json={"a": 1, "b": 2},
match_headers={"foo": "bar"},
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with {} headers and b'{"c":3,"b":2,"a":1}' body amongst:
- Match any request with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"foobar")
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'foobar' body amongst:
- Match any request with {'a': 1, 'b': 2} json body"""
)
@pytest.mark.asyncio
async def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url", content=b"This is the body")
assert response.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
async def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url", content=b"This is the body")
assert response.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_headers_not_matching_and_url_and_content_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_url_and_headers_not_matching_and_content_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_url_and_headers_matching_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_headers_matching_and_url_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_url_matching_and_headers_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url2",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
async def test_method_and_url_and_headers_and_content_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
method="POST",
match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url", content=b"This is the body")
assert response.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_headers_not_matching_and_method_and_url_and_content_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_url_and_headers_not_matching_and_method_and_content_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_method_and_url_and_headers_matching_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_method_and_headers_matching_and_url_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_method_and_url_matching_and_headers_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_method_matching_and_url_and_headers_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_method_and_url_and_headers_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
method="PUT",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match PUT request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@pytest.mark.asyncio
async def test_header_as_str_tuple_list(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")]
)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert dict(response.cookies) == {"key": "value", "key2": "value2"}
@pytest.mark.asyncio
async def test_header_as_bytes_tuple_list(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
headers=[(b"set-cookie", b"key=value"), (b"set-cookie", b"key2=value2")]
)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert dict(response.cookies) == {"key": "value", "key2": "value2"}
@pytest.mark.asyncio
async def test_header_as_bytes_dict(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(headers={b"set-cookie": b"key=value"})
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert dict(response.cookies) == {"key": "value"}
@pytest.mark.asyncio
async def test_header_as_httpx_headers(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(headers=httpx.Headers({"set-cookie": "key=value"}))
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert dict(response.cookies) == {"key": "value"}
@pytest.mark.asyncio
async def test_elapsed_when_add_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.elapsed is not None
@pytest.mark.asyncio
async def test_elapsed_when_add_callback(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_callback(
callback=lambda req: httpx.Response(status_code=200, json={"foo": "bar"})
)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.elapsed is not None
@pytest.mark.asyncio
async def test_elapsed_when_add_async_callback(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json={"foo": "bar"})
httpx_mock.add_callback(custom_response)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.elapsed is not None
@pytest.mark.asyncio
async def test_non_ascii_url_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?query_type=数据")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url?query_type=数据")
assert response.content == b""
@pytest.mark.asyncio
async def test_url_encoded_matching_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?a=%E6%95%B0%E6%8D%AE")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url?a=数据")
assert response.content == b""
@pytest.mark.asyncio
async def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
async with httpx.AsyncClient() as client:
await client.get("https://test_url")
assert len(httpx_mock.get_requests()) == 1
httpx_mock.reset()
assert len(httpx_mock.get_requests()) == 0
@pytest.mark.asyncio
async def test_mutating_json(httpx_mock: HTTPXMock) -> None:
mutating_json = {"content": "request 1"}
httpx_mock.add_response(json=mutating_json)
mutating_json["content"] = "request 2"
httpx_mock.add_response(json=mutating_json)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.json() == {"content": "request 1"}
response = await client.get("https://test_url")
assert response.json() == {"content": "request 2"}
@pytest.mark.asyncio
async def test_streams_are_not_cascading_resulting_in_maximum_recursion(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(json={"abc": "def"}, is_reusable=True)
async with httpx.AsyncClient() as client:
tasks = [client.get("https://test_url") for _ in range(950)]
await asyncio.gather(*tasks)
# No need to assert anything, this test case ensure that no error was raised by the gather
@pytest.mark.asyncio
async def test_custom_transport(httpx_mock: HTTPXMock) -> None:
class CustomTransport(httpx.AsyncHTTPTransport):
def __init__(self, prefix: str, *args, **kwargs):
super().__init__(*args, **kwargs)
self.prefix = prefix
async def handle_async_request(
self,
request: httpx.Request,
) -> httpx.Response:
httpx_response = await super().handle_async_request(request)
httpx_response.headers["x-prefix"] = self.prefix
return httpx_response
httpx_mock.add_response()
async with httpx.AsyncClient(transport=CustomTransport(prefix="test")) as client:
response = await client.post("https://test_url", content=b"This is the body")
assert response.read() == b""
assert response.headers["x-prefix"] == "test"
@pytest.mark.asyncio
async def test_response_selection_content_matching_with_async_iterable(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(match_content=b"full content 1", content=b"matched 1")
httpx_mock.add_response(match_content=b"full content 2", content=b"matched 2")
async def stream_content_1() -> AsyncIterable[bytes]:
yield b"full"
yield b" "
yield b"content"
yield b" 1"
async def stream_content_2() -> AsyncIterable[bytes]:
yield b"full"
yield b" "
yield b"content"
yield b" 2"
async with httpx.AsyncClient() as client:
response_2 = await client.put("https://test_url", content=stream_content_2())
response_1 = await client.put("https://test_url", content=stream_content_1())
assert response_1.content == b"matched 1"
assert response_2.content == b"matched 2"
@pytest.mark.asyncio
async def test_request_selection_content_matching_with_async_iterable(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(match_content=b"full content 1")
httpx_mock.add_response(match_content=b"full content 2")
async def stream_content_1() -> AsyncIterable[bytes]:
yield b"full"
yield b" "
yield b"content"
yield b" 1"
async def stream_content_2() -> AsyncIterable[bytes]:
yield b"full"
yield b" "
yield b"content"
yield b" 2"
async with httpx.AsyncClient() as client:
await client.put("https://test_url_2", content=stream_content_2())
await client.put("https://test_url_1", content=stream_content_1())
assert (
httpx_mock.get_request(match_content=b"full content 1").url
== "https://test_url_1"
)
assert (
httpx_mock.get_request(match_content=b"full content 2").url
== "https://test_url_2"
)
@pytest.mark.asyncio
async def test_files_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_files={"name": ("file_name", b"File content")})
async with httpx.AsyncClient() as client:
response = await client.put(
"https://test_url", files={"name": ("file_name", b"File content")}
)
assert response.content == b""
@pytest.mark.asyncio
async def test_files_and_data_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_files={"name": ("file_name", b"File content")},
match_data={"field": "value"},
)
async with httpx.AsyncClient() as client:
response = await client.put(
"https://test_url",
files={"name": ("file_name", b"File content")},
data={"field": "value"},
)
assert response.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> None:
# Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018
monkeypatch.setattr(
os,
"urandom",
lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18",
)
httpx_mock.add_response(
match_files={"name2": ("file_name", b"File content")}, is_optional=True
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.put(
"https://test_url", files={"name1": ("file_name", b"File content")}
)
assert (
str(exception_info.value)
== """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name1"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst:
- Match any request with {'name2': ('file_name', b'File content')} files"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) -> None:
# Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018
monkeypatch.setattr(
os,
"urandom",
lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18",
)
httpx_mock.add_response(
match_files={"name": ("file_name2", b"File content")}, is_optional=True
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.put(
"https://test_url", files={"name": ("file_name1", b"File content")}
)
assert (
str(exception_info.value)
== """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name1"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst:
- Match any request with {'name': ('file_name2', b'File content')} files"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> None:
# Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018
monkeypatch.setattr(
os,
"urandom",
lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18",
)
httpx_mock.add_response(
match_files={"name": ("file_name", b"File content2")}, is_optional=True
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.put(
"https://test_url", files={"name": ("file_name", b"File content1")}
)
assert (
str(exception_info.value)
== """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content1\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst:
- Match any request with {'name': ('file_name', b'File content2')} files"""
)
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_files_matching_but_data_not_matching(
httpx_mock: HTTPXMock, monkeypatch
) -> None:
# Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018
monkeypatch.setattr(
os,
"urandom",
lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18",
)
httpx_mock.add_response(
match_files={"name": ("file_name", b"File content")},
match_data={"field": "value"},
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.put(
"https://test_url", files={"name": ("file_name", b"File content")}
)
assert (
str(exception_info.value)
== """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst:
- Match any request with {'field': 'value'} multipart data and {'name': ('file_name', b'File content')} files"""
)
@pytest.mark.asyncio
async def test_timeout_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}}
)
async with httpx.AsyncClient() as client:
response = await client.put(
"https://test_url", timeout=httpx.Timeout(5, write=10)
)
assert response.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_timeout_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}},
is_optional=True,
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get("https://test_url", extensions={"test": "value"})
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'timeout': {'connect': 5.0, 'read': 5.0, 'write': 5.0, 'pool': 5.0}} extensions amongst:
- Match any request with {'timeout': {'connect': 5, 'read': 5, 'write': 10, 'pool': 5}} extensions"""
)
@pytest.mark.asyncio
async def test_extensions_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_extensions={"test": "value"})
async with httpx.AsyncClient() as client:
response = await client.put(
"https://test_url", extensions={"test": "value", "test2": "value2"}
)
assert response.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
async def test_extensions_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_extensions={"test": "value"}, is_optional=True)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.get("https://test_url", extensions={"test": "value2"})
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'test': 'value2'} extensions amongst:
- Match any request with {'test': 'value'} extensions"""
)
@pytest.mark.asyncio
async def test_optional_response_not_matched(httpx_mock: HTTPXMock) -> None:
# This response is optional and the fact that it was never requested should not trigger anything
httpx_mock.add_response(url="https://test_url", is_optional=True)
httpx_mock.add_response(url="https://test_url2")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url2")
assert response.content == b""
@pytest.mark.asyncio
async def test_optional_response_matched(httpx_mock: HTTPXMock) -> None:
# This response is optional and the fact that it was never requested should not trigger anything
httpx_mock.add_response(url="https://test_url", is_optional=True)
httpx_mock.add_response(url="https://test_url2")
async with httpx.AsyncClient() as client:
response1 = await client.get("https://test_url")
response2 = await client.get("https://test_url2")
assert response1.content == b""
assert response2.content == b""
@pytest.mark.asyncio
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
async def test_mandatory_response_matched(httpx_mock: HTTPXMock) -> None:
# This response is optional and the fact that it was never requested should not trigger anything
httpx_mock.add_response(url="https://test_url")
# This response MUST be requested (overrides global settings via marker)
httpx_mock.add_response(url="https://test_url2", is_optional=False)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url2")
assert response.content == b""
@pytest.mark.asyncio
async def test_multi_response_matched_once(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_reusable=True)
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b""
@pytest.mark.asyncio
async def test_multi_response_matched_twice(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_reusable=True)
async with httpx.AsyncClient() as client:
response1 = await client.get("https://test_url")
response2 = await client.get("https://test_url")
assert response1.content == b""
assert response2.content == b""
pytest_httpx-0.35.0/tests/test_httpx_sync.py 0000664 0000000 0000000 00000240324 14722140600 0021335 0 ustar 00root root 0000000 0000000 import os
import re
from collections.abc import Iterable
from unittest.mock import ANY
import httpx
import pytest
from pytest import Testdir
import pytest_httpx
from pytest_httpx import HTTPXMock
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_without_response(httpx_mock: HTTPXMock) -> None:
with pytest.raises(Exception) as exception_info:
with httpx.Client() as client:
client.get("https://test_url")
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url"""
)
def test_default_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b""
assert response.status_code == 200
assert response.headers == httpx.Headers({})
assert response.http_version == "HTTP/1.1"
def test_url_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url")
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b""
def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_reusable=True)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b""
response = client.post("https://test_url")
assert response.content == b""
def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?a=1&b=2", is_reusable=True)
with httpx.Client() as client:
response = client.post("https://test_url?a=1&b=2")
assert response.content == b""
# Parameters order should not matter
response = client.get("https://test_url?b=2&a=1")
assert response.content == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_url_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_optional=True)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get("https://test_url2")
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url2 amongst:
- Match any request on https://test_url"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?a=1&a=2", is_optional=True)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
# Same parameter order matters as it corresponds to a list on server side
client.get("https://test_url?a=2&a=1")
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url?a=2&a=1 amongst:
- Match any request on https://test_url?a=1&a=2"""
)
def test_method_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(method="get", is_reusable=True)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b""
response = client.get("https://test_url2")
assert response.content == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_method_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(method="get", is_optional=True)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url")
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url amongst:
- Match GET request"""
)
def test_reusing_one_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", content=b"test content", is_reusable=True
)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b"test content"
response = client.get("https://test_url")
assert response.content == b"test content"
def test_response_with_string_body(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", text="test content")
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b"test content"
def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", html="test content")
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.text == "test content"
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="GET",
url="https://test_url?q=b",
match_headers={"MyHeader": "Something"},
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get("https://test_url", headers={"MyHeader": "Something"})
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
- Match GET request on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
stream=pytest_httpx.IteratorStream([b"part 1", b"part 2"]),
is_reusable=True,
)
with httpx.Client() as client:
with client.stream(method="GET", url="https://test_url") as response:
assert list(response.iter_raw()) == [b"part 1", b"part 2"]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
list(response.iter_raw())
# Assert a response can be streamed more than once
with client.stream(method="GET", url="https://test_url") as response:
assert list(response.iter_raw()) == [b"part 1", b"part 2"]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
list(response.iter_raw())
def test_content_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
content=b"part 1 and 2",
is_reusable=True,
)
with httpx.Client() as client:
with client.stream(method="GET", url="https://test_url") as response:
assert list(response.iter_raw()) == [b"part 1 and 2"]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
list(response.iter_raw())
# Assert a response can be streamed more than once
with client.stream(method="GET", url="https://test_url") as response:
assert list(response.iter_raw()) == [b"part 1 and 2"]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
list(response.iter_raw())
def test_text_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
text="part 1 and 2",
is_reusable=True,
)
with httpx.Client() as client:
with client.stream(method="GET", url="https://test_url") as response:
assert list(response.iter_raw()) == [b"part 1 and 2"]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
list(response.iter_raw())
# Assert a response can be streamed more than once
with client.stream(method="GET", url="https://test_url") as response:
assert list(response.iter_raw()) == [b"part 1 and 2"]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
list(response.iter_raw())
def test_default_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
with client.stream(method="GET", url="https://test_url") as response:
assert list(response.iter_raw()) == []
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
list(response.iter_raw())
# Assert a response can be streamed more than once
with client.stream(method="GET", url="https://test_url") as response:
assert list(response.iter_raw()) == []
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
list(response.iter_raw())
def test_with_many_responses(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", content=b"test content 1")
httpx_mock.add_response(url="https://test_url", content=b"test content 2")
httpx_mock.add_response(url="https://test_url", content=b"test content 2")
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b"test content 1"
response = client.get("https://test_url")
assert response.content == b"test content 2"
response = client.get("https://test_url")
assert response.content == b"test content 2"
def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", content=b"test content 1")
httpx_mock.add_response(
url="https://test_url", content=b"test content 2", is_reusable=True
)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b"test content 1"
response = client.get("https://test_url")
assert response.content == b"test content 2"
response = client.get("https://test_url")
assert response.content == b"test content 2"
def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", method="GET", content=b"test content 1"
)
httpx_mock.add_response(
url="https://test_url", method="POST", content=b"test content 2"
)
httpx_mock.add_response(
url="https://test_url", method="PUT", content=b"test content 3"
)
httpx_mock.add_response(
url="https://test_url", method="DELETE", content=b"test content 4"
)
httpx_mock.add_response(
url="https://test_url", method="PATCH", content=b"test content 5"
)
httpx_mock.add_response(
url="https://test_url", method="HEAD", content=b"test content 6"
)
with httpx.Client() as client:
response = client.post("https://test_url")
assert response.content == b"test content 2"
response = client.get("https://test_url")
assert response.content == b"test content 1"
response = client.put("https://test_url")
assert response.content == b"test content 3"
response = client.head("https://test_url")
assert response.content == b"test content 6"
response = client.patch("https://test_url")
assert response.content == b"test content 5"
response = client.delete("https://test_url")
assert response.content == b"test content 4"
def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", method="GET", content=b"test content 1", status_code=200
)
httpx_mock.add_response(
url="https://test_url",
method="POST",
content=b"test content 2",
status_code=201,
)
httpx_mock.add_response(
url="https://test_url", method="PUT", content=b"test content 3", status_code=202
)
httpx_mock.add_response(
url="https://test_url",
method="DELETE",
content=b"test content 4",
status_code=303,
)
httpx_mock.add_response(
url="https://test_url",
method="PATCH",
content=b"test content 5",
status_code=404,
)
httpx_mock.add_response(
url="https://test_url",
method="HEAD",
content=b"test content 6",
status_code=500,
)
with httpx.Client() as client:
response = client.post("https://test_url")
assert response.content == b"test content 2"
assert response.status_code == 201
response = client.get("https://test_url")
assert response.content == b"test content 1"
assert response.status_code == 200
response = client.put("https://test_url")
assert response.content == b"test content 3"
assert response.status_code == 202
response = client.head("https://test_url")
assert response.content == b"test content 6"
assert response.status_code == 500
response = client.patch("https://test_url")
assert response.content == b"test content 5"
assert response.status_code == 404
response = client.delete("https://test_url")
assert response.content == b"test content 4"
assert response.status_code == 303
def test_with_many_responses_urls_str(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url?param1=test", method="GET", content=b"test content 1"
)
httpx_mock.add_response(
url="https://test_url?param2=test", method="POST", content=b"test content 2"
)
httpx_mock.add_response(
url="https://test_url?param3=test", method="PUT", content=b"test content 3"
)
httpx_mock.add_response(
url="https://test_url?param4=test", method="DELETE", content=b"test content 4"
)
httpx_mock.add_response(
url="https://test_url?param5=test", method="PATCH", content=b"test content 5"
)
httpx_mock.add_response(
url="https://test_url?param6=test", method="HEAD", content=b"test content 6"
)
with httpx.Client() as client:
response = client.post(httpx.URL("https://test_url", params={"param2": "test"}))
assert response.content == b"test content 2"
response = client.get(httpx.URL("https://test_url", params={"param1": "test"}))
assert response.content == b"test content 1"
response = client.put(httpx.URL("https://test_url", params={"param3": "test"}))
assert response.content == b"test content 3"
response = client.head(httpx.URL("https://test_url", params={"param6": "test"}))
assert response.content == b"test content 6"
response = client.patch(
httpx.URL("https://test_url", params={"param5": "test"})
)
assert response.content == b"test content 5"
response = client.delete(
httpx.URL("https://test_url", params={"param4": "test"})
)
assert response.content == b"test content 4"
def test_response_with_pattern_in_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url=re.compile(".*test.*"))
httpx_mock.add_response(url="https://unmatched", content=b"test content")
with httpx.Client() as client:
response = client.get("https://unmatched")
assert response.content == b"test content"
response = client.get("https://test_url")
assert response.content == b""
def test_request_with_pattern_in_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url")
httpx_mock.add_response(url="https://unmatched")
with httpx.Client() as client:
client.get("https://unmatched")
client.get("https://test_url", headers={"X-Test": "1"})
assert httpx_mock.get_request(url=re.compile(".*test.*")).headers["x-test"] == "1"
def test_requests_with_pattern_in_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url")
httpx_mock.add_response(url="https://tests_url")
httpx_mock.add_response(url="https://unmatched")
with httpx.Client() as client:
client.get("https://tests_url", headers={"X-Test": "1"})
client.get("https://unmatched", headers={"X-Test": "2"})
client.get("https://test_url")
requests = httpx_mock.get_requests(url=re.compile(".*test.*"))
assert len(requests) == 2
assert requests[0].headers["x-test"] == "1"
assert "x-test" not in requests[1].headers
def test_callback_with_pattern_in_url(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json={"url": str(request.url)})
def custom_response2(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=200,
extensions={"http_version": b"HTTP/2.0"},
json={"url": str(request.url)},
)
httpx_mock.add_callback(custom_response, url=re.compile(".*test.*"))
httpx_mock.add_callback(custom_response2, url="https://unmatched")
with httpx.Client() as client:
response = client.get("https://unmatched")
assert response.http_version == "HTTP/2.0"
response = client.get("https://test_url")
assert response.http_version == "HTTP/1.1"
def test_with_many_responses_urls_instances(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param1": "test"}),
method="GET",
content=b"test content 1",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param2": "test"}),
method="POST",
content=b"test content 2",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param3": "test"}),
method="PUT",
content=b"test content 3",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param4": "test"}),
method="DELETE",
content=b"test content 4",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param5": "test"}),
method="PATCH",
content=b"test content 5",
)
httpx_mock.add_response(
url=httpx.URL("https://test_url", params={"param6": "test"}),
method="HEAD",
content=b"test content 6",
)
with httpx.Client() as client:
response = client.post("https://test_url?param2=test")
assert response.content == b"test content 2"
response = client.get("https://test_url?param1=test")
assert response.content == b"test content 1"
response = client.put("https://test_url?param3=test")
assert response.content == b"test content 3"
response = client.head("https://test_url?param6=test")
assert response.content == b"test content 6"
response = client.patch("https://test_url?param5=test")
assert response.content == b"test content 5"
response = client.delete("https://test_url?param4=test")
assert response.content == b"test content 4"
def test_with_http_version_2(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", http_version="HTTP/2", content=b"test content 1"
)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b"test content 1"
assert response.http_version == "HTTP/2"
def test_with_headers(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
content=b"test content 1",
headers={"X-Test": "Test value"},
)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b"test content 1"
assert response.headers == httpx.Headers(
{"x-test": "Test value", "content-length": "14"}
)
def test_requests_retrieval(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", method="GET", content=b"test content 1"
)
httpx_mock.add_response(
url="https://test_url", method="POST", content=b"test content 2"
)
httpx_mock.add_response(
url="https://test_url", method="PUT", content=b"test content 3"
)
httpx_mock.add_response(
url="https://test_url", method="DELETE", content=b"test content 4"
)
httpx_mock.add_response(
url="https://test_url", method="PATCH", content=b"test content 5"
)
httpx_mock.add_response(
url="https://test_url", method="HEAD", content=b"test content 6"
)
with httpx.Client() as client:
client.post("https://test_url", content=b"sent content 2")
client.get("https://test_url", headers={"X-TEST": "test header 1"})
client.put("https://test_url", content=b"sent content 3")
client.head("https://test_url")
client.patch("https://test_url", content=b"sent content 5")
client.delete("https://test_url", headers={"X-Test": "test header 4"})
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="PATCH").read()
== b"sent content 5"
)
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="HEAD").read()
== b""
)
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="PUT").read()
== b"sent content 3"
)
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="GET").headers[
"x-test"
]
== "test header 1"
)
assert (
httpx_mock.get_request(url=httpx.URL("https://test_url"), method="POST").read()
== b"sent content 2"
)
assert (
httpx_mock.get_request(
url=httpx.URL("https://test_url"), method="DELETE"
).headers["x-test"]
== "test header 4"
)
def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_reusable=True)
with httpx.Client() as client:
client.get("https://test_url", headers={"X-TEST": "test header 1"})
client.get("https://test_url", headers={"X-TEST": "test header 2"})
requests = httpx_mock.get_requests(url=httpx.URL("https://test_url"))
assert len(requests) == 2
assert requests[0].headers["x-test"] == "test header 1"
assert requests[1].headers["x-test"] == "test header 2"
def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.get("https://test_url", headers={"X-TEST": "test header 1"})
client.get("https://test_url2", headers={"X-TEST": "test header 2"})
request = httpx_mock.get_request(url=httpx.URL("https://test_url"))
assert request.headers["x-test"] == "test header 1"
def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.get("https://test_url", headers={"X-TEST": "test header 1"})
client.get("https://test_url2", headers={"X-TEST": "test header 2"})
requests = httpx_mock.get_requests(method="GET")
assert len(requests) == 2
assert requests[0].headers["x-test"] == "test header 1"
assert requests[1].headers["x-test"] == "test header 2"
def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.get("https://test_url", headers={"X-TEST": "test header 1"})
client.post("https://test_url", headers={"X-TEST": "test header 2"})
request = httpx_mock.get_request(method="GET")
assert request.headers["x-test"] == "test header 1"
def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.get("https://test_url", headers={"X-TEST": "test header 1"})
client.get("https://test_url", headers={"X-TEST": "test header 2"})
client.post("https://test_url", headers={"X-TEST": "test header 3"})
client.get("https://test_url2", headers={"X-TEST": "test header 4"})
requests = httpx_mock.get_requests(url=httpx.URL("https://test_url"), method="GET")
assert len(requests) == 2
assert requests[0].headers["x-test"] == "test header 1"
assert requests[1].headers["x-test"] == "test header 2"
def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.post("https://test_url", headers={"X-TEST": "test header 1"})
client.get("https://test_url2", headers={"X-TEST": "test header 2"})
requests = httpx_mock.get_requests()
assert len(requests) == 2
assert requests[0].headers["x-test"] == "test header 1"
assert requests[1].headers["x-test"] == "test header 2"
def test_default_request_retrieval(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
with httpx.Client() as client:
client.post("https://test_url", headers={"X-TEST": "test header 1"})
request = httpx_mock.get_request()
assert request.headers["x-test"] == "test header 1"
def test_requests_json_body(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url", method="GET", json=["list content 1", "list content 2"]
)
httpx_mock.add_response(
url="https://test_url",
method="POST",
json={"key 1": "value 1", "key 2": "value 2"},
)
httpx_mock.add_response(url="https://test_url", method="PUT", json="string value")
with httpx.Client() as client:
response = client.post("https://test_url")
assert response.json() == {"key 1": "value 1", "key 2": "value 2"}
assert response.headers["content-type"] == "application/json"
response = client.get("https://test_url")
assert response.json() == ["list content 1", "list content 2"]
assert response.headers["content-type"] == "application/json"
response = client.put("https://test_url")
assert response.json() == "string value"
assert response.headers["content-type"] == "application/json"
def test_callback_raising_exception(httpx_mock: HTTPXMock) -> None:
def raise_timeout(request: httpx.Request) -> httpx.Response:
raise httpx.ReadTimeout(
f"Unable to read within {request.extensions['timeout']['read']}",
request=request,
)
httpx_mock.add_callback(raise_timeout, url="https://test_url")
with httpx.Client() as client:
with pytest.raises(httpx.ReadTimeout) as exception_info:
client.get("https://test_url")
assert str(exception_info.value) == "Unable to read within 5.0"
def test_request_exception_raising(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_exception(
httpx.ReadTimeout("Unable to read within 5.0"), url="https://test_url"
)
with httpx.Client() as client:
with pytest.raises(httpx.ReadTimeout) as exception_info:
client.get("https://test_url")
assert str(exception_info.value) == "Unable to read within 5.0"
assert exception_info.value.request is not None
def test_non_request_exception_raising(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_exception(
httpx.HTTPError("Unable to read within 5.0"), url="https://test_url"
)
with httpx.Client() as client:
with pytest.raises(httpx.HTTPError) as exception_info:
client.get("https://test_url")
assert str(exception_info.value) == "Unable to read within 5.0"
def test_callback_returning_response(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json={"url": str(request.url)})
httpx_mock.add_callback(custom_response, url="https://test_url")
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.json() == {"url": "https://test_url"}
assert response.headers["content-type"] == "application/json"
def test_callback_executed_twice(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
httpx_mock.add_callback(custom_response, is_reusable=True)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
response = client.post("https://test_url")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
def test_callback_registered_after_response(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content2"])
httpx_mock.add_response(json=["content1"])
httpx_mock.add_callback(custom_response, is_reusable=True)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.json() == ["content1"]
assert response.headers["content-type"] == "application/json"
response = client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
# Assert that the last registered callback is sent again even if there is a response
response = client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
def test_response_registered_after_callback(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content1"])
httpx_mock.add_callback(custom_response)
httpx_mock.add_response(json=["content2"], is_reusable=True)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.json() == ["content1"]
assert response.headers["content-type"] == "application/json"
response = client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
# Assert that the last registered response is sent again even if there is a callback
response = client.post("https://test_url")
assert response.json() == ["content2"]
assert response.headers["content-type"] == "application/json"
def test_callback_matching_method(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
httpx_mock.add_callback(custom_response, method="GET", is_reusable=True)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
response = client.get("https://test_url2")
assert response.json() == ["content"]
assert response.headers["content-type"] == "application/json"
def test_request_retrieval_with_more_than_one(testdir: Testdir) -> None:
"""
Single request cannot be returned if there is more than one matching.
"""
testdir.makepyfile(
"""
import httpx
def test_request_retrieval_with_more_than_one(httpx_mock):
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.get("https://test_url", headers={"X-TEST": "test header 1"})
client.get("https://test_url", headers={"X-TEST": "test header 2"})
httpx_mock.get_request(url=httpx.URL("https://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 or refine your filters."
]
)
def test_headers_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}
)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b""
def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"})
with httpx.Client() as client:
response = client.get(
"https://test_url",
headers=[("my-custom-header", "value1"), ("my-custom-header", "value2")],
)
assert response.content == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_multi_value_headers_not_matching_single_value_issued(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
match_headers={"my-custom-header": "value1"}, is_optional=True
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get(
"https://test_url",
headers=[
("my-custom-header", "value1"),
("my-custom-header", "value2"),
],
)
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst:
- Match any request with {'my-custom-header': 'value1'} headers"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_multi_value_headers_not_matching_multi_value_issued(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
match_headers={"my-custom-header": "value1, value2"}, is_optional=True
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get(
"https://test_url",
headers=[
("my-custom-header", "value1"),
("my-custom-header", "value3"),
],
)
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst:
- Match any request with {'my-custom-header': 'value1, value2'} headers"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={"user-agent": f"python-httpx/{httpx.__version__}"},
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get("https://test_url")
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
- Match any request with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
"Host2": "test_url",
},
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get("https://test_url")
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
)
def test_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_content=b"This is the body")
with httpx.Client() as client:
response = client.post("https://test_url", content=b"This is the body")
assert response.read() == b""
def test_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(proxy_url="http://user:pwd@my_other_proxy/")
with httpx.Client(proxy="http://user:pwd@my_other_proxy") as client:
response = client.get("https://test_url")
assert response.read() == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True)
with httpx.Client(proxy="http://my_test_proxy") as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get("http://test_url")
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with http://my_test_proxy/ proxy URL amongst:
- Match any request with http://my_test_proxy proxy URL"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get("http://test_url")
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with no proxy URL amongst:
- Match any request with http://my_test_proxy proxy URL"""
)
def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.post("https://test_url", content=b"This is the body")
client.post("https://test_url2", content=b"This is the body")
client.post("https://test_url2", content=b"This is the body2")
assert len(httpx_mock.get_requests(match_content=b"This is the body")) == 2
def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.post("https://test_url", json=["my_str"])
client.post("https://test_url2", json=["my_str"])
client.post("https://test_url2", json=["my_str2"])
assert len(httpx_mock.get_requests(match_json=["my_str"])) == 2
def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client(
mounts={
"http://": httpx.HTTPTransport(proxy="http://my_test_proxy"),
"https://": httpx.HTTPTransport(proxy="http://user:pwd@my_other_proxy"),
}
) as client:
client.get("https://test_url")
client.get("https://test_url2")
client.get("http://test_url2")
assert (
len(httpx_mock.get_requests(proxy_url="http://user:pwd@my_other_proxy/")) == 2
)
def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client(
mounts={
"http://": httpx.HTTPTransport(proxy="http://my_test_proxy"),
"https://": httpx.HTTPTransport(proxy="http://user:pwd@my_other_proxy"),
}
) as client:
client.get("https://test_url")
client.get("https://test_url2")
client.get("http://test_url2")
assert httpx_mock.get_request(proxy_url="http://my_test_proxy/")
def test_requests_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.put(
"https://test_url",
files={"name": ("file_name", b"File content")},
data={"field1": "value"},
)
client.put(
"https://test_url2",
files={"name": ("file_name", b"File content")},
data={"field": "value"},
)
client.put(
"http://test_url2",
files={"name": ("file_name", b"File content")},
data={"field": "value"},
)
assert (
len(
httpx_mock.get_requests(
match_files={"name": ("file_name", b"File content")},
match_data={"field": "value"},
)
)
== 2
)
def test_request_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.put(
"https://test_url",
files={"name": ("file_name", b"File content")},
data={"field": "value"},
)
client.get("https://test_url2")
client.get("http://test_url2")
assert httpx_mock.get_request(
match_files={"name": ("file_name", b"File content")},
match_data={"field": "value"},
)
def test_requests_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.get("https://test_url")
client.get("https://test_url2", timeout=10)
client.get("https://test_url2", timeout=10)
assert (
len(
httpx_mock.get_requests(
match_extensions={
"timeout": {"connect": 10, "read": 10, "write": 10, "pool": 10}
}
)
)
== 2
)
def test_request_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(is_reusable=True)
with httpx.Client() as client:
client.get("https://test_url", timeout=httpx.Timeout(5, read=10))
client.get("https://test_url2", timeout=10)
client.get("http://test_url2", timeout=10)
assert httpx_mock.get_request(
match_extensions={"timeout": {"connect": 5, "read": 10, "write": 5, "pool": 5}}
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_content=b"This is the body", is_optional=True)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body2")
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'This is the body2' body amongst:
- Match any request with b'This is the body' body"""
)
def test_match_json_and_match_content_error(httpx_mock: HTTPXMock) -> None:
with pytest.raises(ValueError) as exception_info:
httpx_mock.add_response(match_json={"a": 1}, match_content=b"")
assert (
str(exception_info.value)
== "Only one way of matching against the body can be provided. If you want to match against the JSON decoded representation, use match_json. If you want to match against the multipart representation, use match_files (and match_data). Otherwise, use match_content."
)
def test_match_json_and_match_files_error(httpx_mock: HTTPXMock) -> None:
with pytest.raises(ValueError) as exception_info:
httpx_mock.add_response(
match_json={"a": 1}, match_files={"name": ("file_name", b"File content")}
)
assert (
str(exception_info.value)
== "Only one way of matching against the body can be provided. If you want to match against the JSON decoded representation, use match_json. If you want to match against the multipart representation, use match_files (and match_data). Otherwise, use match_content."
)
def test_match_content_and_match_files_error(httpx_mock: HTTPXMock) -> None:
with pytest.raises(ValueError) as exception_info:
httpx_mock.add_response(
match_content=b"",
match_files={"name": ("file_name", b"File content")},
)
assert (
str(exception_info.value)
== "Only one way of matching against the body can be provided. If you want to match against the JSON decoded representation, use match_json. If you want to match against the multipart representation, use match_files (and match_data). Otherwise, use match_content."
)
def test_json_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
with httpx.Client() as client:
response = client.post("https://test_url", json={"b": 2, "a": 1})
assert response.read() == b""
def test_json_partial_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": ANY})
with httpx.Client() as client:
response = client.post("https://test_url", json={"b": 2, "a": 1})
assert response.read() == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'{"c":3,"b":2,"a":1}' body amongst:
- Match any request with {'a': 1, 'b': 2} json body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_json={"a": 1, "b": 2},
match_headers={"foo": "bar"},
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with {} headers and b'{"c":3,"b":2,"a":1}' body amongst:
- Match any request with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"foobar")
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'foobar' body amongst:
- Match any request with {'a': 1, 'b': 2} json body"""
)
def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
with httpx.Client() as client:
response = client.post("https://test_url", content=b"This is the body")
assert response.content == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
with httpx.Client() as client:
response = client.post("https://test_url", content=b"This is the body")
assert response.content == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_headers_not_matching_and_url_and_content_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_url_and_headers_not_matching_and_content_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_url_and_headers_matching_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_headers_matching_and_url_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_url_matching_and_headers_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url2",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
def test_method_and_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
method="POST",
match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
with httpx.Client() as client:
response = client.post("https://test_url", content=b"This is the body")
assert response.content == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_headers_not_matching_and_method_and_url_and_content_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_url_and_headers_not_matching_and_method_and_content_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_method_and_url_and_headers_matching_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_method_and_headers_matching_and_url_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_method_and_url_matching_and_headers_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_method_matching_and_url_and_headers_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
method="POST",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_method_and_url_and_headers_and_content_not_matching(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="https://test_url2",
method="PUT",
match_headers={
"User-Agent": f"python-httpx/{httpx.__version__}",
"Host": "test_url2",
},
match_content=b"This is the body2",
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
- Match PUT request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
def test_header_as_str_tuple_list(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")]
)
with httpx.Client() as client:
response = client.get("https://test_url")
assert dict(response.cookies) == {"key": "value", "key2": "value2"}
def test_header_as_bytes_tuple_list(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
headers=[(b"set-cookie", b"key=value"), (b"set-cookie", b"key2=value2")]
)
with httpx.Client() as client:
response = client.get("https://test_url")
assert dict(response.cookies) == {"key": "value", "key2": "value2"}
def test_header_as_bytes_dict(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(headers={b"set-cookie": b"key=value"})
with httpx.Client() as client:
response = client.get("https://test_url")
assert dict(response.cookies) == {"key": "value"}
def test_header_as_httpx_headers(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(headers=httpx.Headers({"set-cookie": "key=value"}))
with httpx.Client() as client:
response = client.get("https://test_url")
assert dict(response.cookies) == {"key": "value"}
def test_elapsed_when_add_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.elapsed is not None
def test_elapsed_when_add_callback(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_callback(
callback=lambda req: httpx.Response(status_code=200, json={"foo": "bar"})
)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.elapsed is not None
def test_non_ascii_url_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?query_type=数据")
with httpx.Client() as client:
response = client.get("https://test_url?query_type=数据")
assert response.content == b""
def test_url_encoded_matching_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?a=%E6%95%B0%E6%8D%AE")
with httpx.Client() as client:
response = client.get("https://test_url?a=数据")
assert response.content == b""
def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
with httpx.Client() as client:
client.get("https://test_url")
assert len(httpx_mock.get_requests()) == 1
httpx_mock.reset()
assert len(httpx_mock.get_requests()) == 0
def test_mutating_json(httpx_mock: HTTPXMock) -> None:
mutating_json = {"content": "request 1"}
httpx_mock.add_response(json=mutating_json)
mutating_json["content"] = "request 2"
httpx_mock.add_response(json=mutating_json)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.json() == {"content": "request 1"}
response = client.get("https://test_url")
assert response.json() == {"content": "request 2"}
def test_custom_transport(httpx_mock: HTTPXMock) -> None:
class CustomTransport(httpx.HTTPTransport):
def __init__(self, prefix: str, *args, **kwargs):
super().__init__(*args, **kwargs)
self.prefix = prefix
def handle_request(
self,
request: httpx.Request,
) -> httpx.Response:
httpx_response = super().handle_request(request)
httpx_response.headers["x-prefix"] = self.prefix
return httpx_response
httpx_mock.add_response()
with httpx.Client(transport=CustomTransport(prefix="test")) as client:
response = client.post("https://test_url", content=b"This is the body")
assert response.read() == b""
assert response.headers["x-prefix"] == "test"
def test_response_selection_content_matching_with_iterable(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(match_content=b"full content 1", content=b"matched 1")
httpx_mock.add_response(match_content=b"full content 2", content=b"matched 2")
def stream_content_1() -> Iterable[bytes]:
yield b"full"
yield b" "
yield b"content"
yield b" 1"
def stream_content_2() -> Iterable[bytes]:
yield b"full"
yield b" "
yield b"content"
yield b" 2"
with httpx.Client() as client:
response_2 = client.put("https://test_url", content=stream_content_2())
response_1 = client.put("https://test_url", content=stream_content_1())
assert response_1.content == b"matched 1"
assert response_2.content == b"matched 2"
def test_request_selection_content_matching_with_iterable(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(match_content=b"full content 1")
httpx_mock.add_response(match_content=b"full content 2")
def stream_content_1() -> Iterable[bytes]:
yield b"full"
yield b" "
yield b"content"
yield b" 1"
def stream_content_2() -> Iterable[bytes]:
yield b"full"
yield b" "
yield b"content"
yield b" 2"
with httpx.Client() as client:
client.put("https://test_url_2", content=stream_content_2())
client.put("https://test_url_1", content=stream_content_1())
assert (
httpx_mock.get_request(match_content=b"full content 1").url
== "https://test_url_1"
)
assert (
httpx_mock.get_request(match_content=b"full content 2").url
== "https://test_url_2"
)
def test_files_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_files={"name": ("file_name", b"File content")})
with httpx.Client() as client:
response = client.put(
"https://test_url", files={"name": ("file_name", b"File content")}
)
assert response.content == b""
def test_files_and_data_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_files={"name": ("file_name", b"File content")},
match_data={"field": "value"},
)
with httpx.Client() as client:
response = client.put(
"https://test_url",
files={"name": ("file_name", b"File content")},
data={"field": "value"},
)
assert response.content == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> None:
# Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018
monkeypatch.setattr(
os,
"urandom",
lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18",
)
httpx_mock.add_response(
match_files={"name2": ("file_name", b"File content")}, is_optional=True
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.put(
"https://test_url", files={"name1": ("file_name", b"File content")}
)
assert (
str(exception_info.value)
== """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name1"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst:
- Match any request with {'name2': ('file_name', b'File content')} files"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) -> None:
# Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018
monkeypatch.setattr(
os,
"urandom",
lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18",
)
httpx_mock.add_response(
match_files={"name": ("file_name2", b"File content")}, is_optional=True
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.put(
"https://test_url", files={"name": ("file_name1", b"File content")}
)
assert (
str(exception_info.value)
== """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name1"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst:
- Match any request with {'name': ('file_name2', b'File content')} files"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> None:
# Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018
monkeypatch.setattr(
os,
"urandom",
lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18",
)
httpx_mock.add_response(
match_files={"name": ("file_name", b"File content2")}, is_optional=True
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.put(
"https://test_url", files={"name": ("file_name", b"File content1")}
)
assert (
str(exception_info.value)
== """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content1\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst:
- Match any request with {'name': ('file_name', b'File content2')} files"""
)
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_files_matching_but_data_not_matching(
httpx_mock: HTTPXMock, monkeypatch
) -> None:
# Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018
monkeypatch.setattr(
os,
"urandom",
lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18",
)
httpx_mock.add_response(
match_files={"name": ("file_name", b"File content")},
match_data={"field": "value"},
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.put(
"https://test_url", files={"name": ("file_name", b"File content")}
)
assert (
str(exception_info.value)
== """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst:
- Match any request with {'field': 'value'} multipart data and {'name': ('file_name', b'File content')} files"""
)
def test_data_without_files(httpx_mock: HTTPXMock) -> None:
with pytest.raises(ValueError) as exception_info:
httpx_mock.add_response(match_data={"field": "value"})
assert (
str(exception_info.value)
== "match_data is meant to be used for multipart matching (in conjunction with match_files).Use match_content to match url encoded data."
)
def test_timeout_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}}
)
with httpx.Client() as client:
response = client.put("https://test_url", timeout=httpx.Timeout(5, write=10))
assert response.content == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_timeout_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}},
is_optional=True,
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get("https://test_url", extensions={"test": "value"})
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'timeout': {'connect': 5.0, 'read': 5.0, 'write': 5.0, 'pool': 5.0}} extensions amongst:
- Match any request with {'timeout': {'connect': 5, 'read': 5, 'write': 10, 'pool': 5}} extensions"""
)
def test_extensions_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_extensions={"test": "value"})
with httpx.Client() as client:
response = client.put(
"https://test_url", extensions={"test": "value", "test2": "value2"}
)
assert response.content == b""
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_extensions_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_extensions={"test": "value"}, is_optional=True)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.get("https://test_url", extensions={"test": "value2"})
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'test': 'value2'} extensions amongst:
- Match any request with {'test': 'value'} extensions"""
)
def test_optional_response_not_matched(httpx_mock: HTTPXMock) -> None:
# This response is optional and the fact that it was never requested should not trigger anything
httpx_mock.add_response(url="https://test_url", is_optional=True)
httpx_mock.add_response(url="https://test_url2")
with httpx.Client() as client:
response = client.get("https://test_url2")
assert response.content == b""
def test_optional_response_matched(httpx_mock: HTTPXMock) -> None:
# This response is optional and the fact that it was never requested should not trigger anything
httpx_mock.add_response(url="https://test_url", is_optional=True)
httpx_mock.add_response(url="https://test_url2")
with httpx.Client() as client:
response1 = client.get("https://test_url")
response2 = client.get("https://test_url2")
assert response1.content == b""
assert response2.content == b""
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
def test_mandatory_response_matched(httpx_mock: HTTPXMock) -> None:
# This response is optional and the fact that it was never requested should not trigger anything
httpx_mock.add_response(url="https://test_url")
# This response MUST be requested (overrides global settings via marker)
httpx_mock.add_response(url="https://test_url2", is_optional=False)
with httpx.Client() as client:
response = client.get("https://test_url2")
assert response.content == b""
def test_multi_response_matched_once(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_reusable=True)
with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b""
def test_multi_response_matched_twice(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", is_reusable=True)
with httpx.Client() as client:
response1 = client.get("https://test_url")
response2 = client.get("https://test_url")
assert response1.content == b""
assert response2.content == b""
pytest_httpx-0.35.0/tests/test_plugin.py 0000664 0000000 0000000 00000064741 14722140600 0020437 0 ustar 00root root 0000000 0000000 from pytest import Testdir
def test_fixture_is_available(testdir: Testdir) -> None:
# create a temporary pytest test file
testdir.makepyfile(
"""
import httpx
def test_http(httpx_mock):
mock = httpx_mock.add_response(url="https://foo.tld")
r = httpx.get("https://foo.tld")
assert httpx_mock.get_request() is not None
"""
)
# run all tests with pytest
result = testdir.runpytest()
result.assert_outcomes(passed=1)
def test_httpx_mock_unused_response(testdir: Testdir) -> None:
"""
Unused responses should fail test case.
"""
testdir.makepyfile(
"""
def test_httpx_mock_unused_response(httpx_mock):
httpx_mock.add_response()
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, passed=1)
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
"* - Match any request",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
consecutive=True,
)
def test_httpx_mock_unused_response_without_assertion(testdir: Testdir) -> None:
"""
Unused responses should not fail test case if
assert_all_responses_were_requested option is set to False.
"""
testdir.makepyfile(
"""
import pytest
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
def test_httpx_mock_unused_response_without_assertion(httpx_mock):
httpx_mock.add_response()
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
def test_httpx_mock_unused_callback(testdir: Testdir) -> None:
"""
Unused callbacks should fail test case.
"""
testdir.makepyfile(
"""
def test_httpx_mock_unused_callback(httpx_mock):
def unused(*args, **kwargs):
pass
httpx_mock.add_callback(unused)
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, passed=1)
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
"* - Match any request",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
consecutive=True,
)
def test_httpx_mock_unused_callback_without_assertion(testdir: Testdir) -> None:
"""
Unused callbacks should not fail test case if
assert_all_responses_were_requested option is set to False.
"""
testdir.makepyfile(
"""
import pytest
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
def test_httpx_mock_unused_callback_without_assertion(httpx_mock):
def unused(*args, **kwargs):
pass
httpx_mock.add_callback(unused)
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
def test_httpx_mock_unexpected_request(testdir: Testdir) -> None:
"""
Unexpected request should fail test case if
assert_all_requests_were_expected option is set to True (default).
"""
testdir.makepyfile(
"""
import httpx
import pytest
def test_httpx_mock_unexpected_request(httpx_mock):
with httpx.Client() as client:
# Non mocked request
with pytest.raises(httpx.TimeoutException):
client.get("https://foo.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, passed=1)
result.stdout.fnmatch_lines(
[
"*AssertionError: The following requests were not expected:",
"* - GET request on https://foo.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
],
consecutive=True,
)
def test_httpx_mock_unexpected_request_without_assertion(testdir: Testdir) -> None:
"""
Unexpected request should not fail test case if
assert_all_requests_were_expected option is set to False.
"""
testdir.makepyfile(
"""
import httpx
import pytest
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_httpx_mock_unexpected_request(httpx_mock):
with httpx.Client() as client:
# Non mocked request
with pytest.raises(httpx.TimeoutException):
client.get("https://foo.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
def test_httpx_mock_already_matched_response(testdir: Testdir) -> None:
"""
Already matched response should fail test case if
can_send_already_matched_responses option is set to False (default).
"""
testdir.makepyfile(
"""
import httpx
import pytest
def test_httpx_mock_already_matched_response(httpx_mock):
httpx_mock.add_response()
with httpx.Client() as client:
client.get("https://foo.tld")
# Non mocked (already matched) request
with pytest.raises(httpx.TimeoutException):
client.get("https://foo.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, passed=1)
result.stdout.fnmatch_lines(
[
"*AssertionError: The following requests were not expected:",
"* - GET request on https://foo.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
],
consecutive=True,
)
def test_httpx_mock_reusing_matched_response(testdir: Testdir) -> None:
"""
Already matched response should not fail test case if
can_send_already_matched_responses option is set to True.
"""
testdir.makepyfile(
"""
import httpx
import pytest
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_httpx_mock_reusing_matched_response(httpx_mock):
httpx_mock.add_response()
with httpx.Client() as client:
client.get("https://foo.tld")
# Reusing response
client.get("https://foo.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
def test_httpx_mock_unmatched_request_without_responses(
testdir: Testdir,
) -> None:
testdir.makepyfile(
"""
import httpx
import pytest
def test_httpx_mock_unmatched_request_without_responses(httpx_mock):
with httpx.Client() as client:
# This request will not be matched
client.get("https://foo22.tld")
# This code will not be reached
client.get("https://foo3.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, failed=1)
# Assert the error that occurred
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld",
],
consecutive=True,
)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following requests were not expected:",
"* - GET request on https://foo22.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
],
consecutive=True,
)
def test_httpx_mock_unmatched_request_with_only_unmatched_responses(
testdir: Testdir,
) -> None:
testdir.makepyfile(
"""
import httpx
import pytest
def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock):
# This response will not be sent (because of a typo in the URL)
httpx_mock.add_response(url="https://foo2.tld")
# This response will not be sent (because test execution failed earlier)
httpx_mock.add_response(url="https://foo3.tld")
with httpx.Client() as client:
# This request will not be matched
client.get("https://foo22.tld")
# This code will not be reached
client.get("https://foo3.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, failed=1)
# Assert the error that occurred
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
"*- Match any request on https://foo2.tld",
"*- Match any request on https://foo3.tld",
],
consecutive=True,
)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
"* - Match any request on https://foo2.tld",
"* - Match any request on https://foo3.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
consecutive=True,
)
def test_httpx_mock_unmatched_request_with_only_unmatched_reusable_responses(
testdir: Testdir,
) -> None:
testdir.makepyfile(
"""
import httpx
import pytest
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock):
# This response will not be sent (because of a typo in the URL)
httpx_mock.add_response(url="https://foo2.tld", method="GET")
# This response will not be sent (because test execution failed earlier)
httpx_mock.add_response(url="https://foo3.tld")
with httpx.Client() as client:
# This request will not be matched
client.get("https://foo22.tld")
# This code will not be reached
client.get("https://foo3.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, failed=1)
# Assert the error that occurred
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
"*- Match GET request on https://foo2.tld",
"*- Match every request on https://foo3.tld",
],
consecutive=True,
)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
"* - Match GET request on https://foo2.tld",
"* - Match every request on https://foo3.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
consecutive=True,
)
def test_httpx_mock_unmatched_request_with_only_matched_responses(
testdir: Testdir,
) -> None:
testdir.makepyfile(
"""
import httpx
import pytest
def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock):
# Sent response
httpx_mock.add_response(url="https://foo.tld")
# Sent response
httpx_mock.add_response(url="https://foo.tld")
with httpx.Client() as client:
client.get("https://foo.tld")
client.get("https://foo.tld")
# This request will not be matched
client.get("https://foo22.tld")
# This code will not be reached
client.get("https://foo3.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, failed=1)
# Assert the error that occurred
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
"*- Already matched any request on https://foo.tld",
"*- Already matched any request on https://foo.tld",
"*",
"*If you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request",
],
consecutive=True,
)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following requests were not expected:",
"* - GET request on https://foo22.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
],
consecutive=True,
)
def test_httpx_mock_unmatched_request_with_only_matched_reusable_responses(
testdir: Testdir,
) -> None:
testdir.makepyfile(
"""
import httpx
import pytest
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock):
# Sent response
httpx_mock.add_response(url="https://foo.tld")
# Sent response
httpx_mock.add_response(url="https://foo3.tld")
with httpx.Client() as client:
client.get("https://foo.tld")
client.get("https://foo.tld")
client.get("https://foo3.tld")
# This request will not be matched
client.get("https://foo22.tld")
# This code will not be reached
client.get("https://foo3.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, failed=1)
# Assert the error that occurred
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
"*- Match every request on https://foo.tld",
"*- Match every request on https://foo3.tld",
],
consecutive=True,
)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following requests were not expected:",
"* - GET request on https://foo22.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
],
consecutive=True,
)
def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(
testdir: Testdir,
) -> None:
testdir.makepyfile(
"""
import httpx
import pytest
def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock):
# Sent response
httpx_mock.add_response(url="https://foo.tld")
# This response will not be sent (because of a typo in the URL)
httpx_mock.add_response(url="https://foo2.tld")
# Sent response
httpx_mock.add_response(url="https://foo.tld")
# This response will not be sent (because test execution failed earlier)
httpx_mock.add_response(url="https://foo3.tld")
with httpx.Client() as client:
client.get("https://foo.tld")
client.get("https://foo.tld")
# This request will not be matched
client.get("https://foo22.tld")
# This code will not be reached
client.get("https://foo3.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, failed=1)
# Assert the error that occurred
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
"*- Match any request on https://foo2.tld",
"*- Match any request on https://foo3.tld",
"*- Already matched any request on https://foo.tld",
"*- Already matched any request on https://foo.tld",
"*",
"*If you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request",
],
consecutive=True,
)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
"* - Match any request on https://foo2.tld",
"* - Match any request on https://foo3.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
consecutive=True,
)
def test_httpx_mock_unmatched_request_with_matched_and_unmatched_reusable_responses(
testdir: Testdir,
) -> None:
testdir.makepyfile(
"""
import httpx
import pytest
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock):
# Sent response
httpx_mock.add_response(url="https://foo.tld")
# This response will not be sent (because of a typo in the URL)
httpx_mock.add_response(url="https://foo33.tld")
# Sent response
httpx_mock.add_response(url="https://foo2.tld")
# This response will not be sent (because test execution failed earlier)
httpx_mock.add_response(url="https://foo4.tld")
with httpx.Client() as client:
client.get("https://foo.tld")
client.get("https://foo2.tld")
client.get("https://foo.tld")
# This request will not be matched
client.get("https://foo3.tld")
# This code will not be reached
client.get("https://foo2.tld")
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, failed=1)
# Assert the error that occurred
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo3.tld amongst:",
"*- Match every request on https://foo33.tld",
"*- Match every request on https://foo4.tld",
"*- Match every request on https://foo.tld",
"*- Match every request on https://foo2.tld",
],
consecutive=True,
)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
"* - Match every request on https://foo33.tld",
"* - Match every request on https://foo4.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
consecutive=True,
)
def test_httpx_mock_should_mock_sync(testdir: Testdir) -> None:
"""
Non mocked requests should go through while other requests should be mocked.
"""
testdir.makepyfile(
"""
import httpx
import pytest
@pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "localhost")
def test_httpx_mock_should_mock_sync(httpx_mock):
httpx_mock.add_response()
with httpx.Client() as client:
# Mocked request
client.get("https://foo.tld")
# Non mocked request
with pytest.raises(httpx.ConnectError):
client.get("https://localhost:5005")
# Assert that a single request was mocked
assert len(httpx_mock.get_requests()) == 1
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
def test_httpx_mock_should_mock_async(testdir: Testdir) -> None:
"""
Non mocked requests should go through while other requests should be mocked.
"""
testdir.makepyfile(
"""
import httpx
import pytest
@pytest.mark.asyncio
@pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "localhost")
async def test_httpx_mock_should_mock_async(httpx_mock):
httpx_mock.add_response()
async with httpx.AsyncClient() as client:
# Mocked request
await client.get("https://foo.tld")
# Non mocked request
with pytest.raises(httpx.ConnectError):
await client.get("https://localhost:5005")
# Assert that a single request was mocked
assert len(httpx_mock.get_requests()) == 1
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
def test_httpx_mock_options_on_multi_levels_are_aggregated(testdir: Testdir) -> None:
"""
Test case ensures that every level provides one parameter that should be used in the end
global (actually registered AFTER module): assert_all_responses_were_requested (tested by putting unused response)
module: assert_all_requests_were_expected (tested by not mocking one URL)
test: should_mock (tested by calling 3 URls, 2 mocked, the other one not)
"""
testdir.makeconftest(
"""
import pytest
def pytest_collection_modifyitems(session, config, items):
for item in items:
item.add_marker(pytest.mark.httpx_mock(assert_all_responses_were_requested=False))
"""
)
testdir.makepyfile(
"""
import httpx
import pytest
pytestmark = pytest.mark.httpx_mock(assert_all_requests_were_expected=False, should_mock=lambda request: request.url.host != "https://foo.tld")
@pytest.mark.asyncio
@pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "localhost")
async def test_httpx_mock_options_on_multi_levels_are_aggregated(httpx_mock):
httpx_mock.add_response(url="https://foo.tld", headers={"x-pytest-httpx": "this was mocked"})
# This response will never be used, testing that assert_all_responses_were_requested is handled
httpx_mock.add_response(url="https://never_called.url")
async with httpx.AsyncClient() as client:
# Assert that previously set should_mock was overridden
response = await client.get("https://foo.tld")
assert response.headers["x-pytest-httpx"] == "this was mocked"
# Assert that latest should_mock is handled
with pytest.raises(httpx.ConnectError):
await client.get("https://localhost:5005")
# Assert that assert_all_requests_were_expected is the one at module level
with pytest.raises(httpx.TimeoutException):
await client.get("https://unexpected.url")
# Assert that 2 requests out of 3 were mocked
assert len(httpx_mock.get_requests()) == 2
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
def test_invalid_marker(testdir: Testdir) -> None:
"""
Unknown marker keyword arguments should raise a TypeError.
"""
testdir.makepyfile(
"""
import pytest
@pytest.mark.httpx_mock(foo=123)
def test_invalid_marker(httpx_mock):
pass
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1)
result.stdout.re_match_lines([r".*got an unexpected keyword argument 'foo'"])
def test_mandatory_response_not_matched(testdir: Testdir) -> None:
"""
is_optional MUST take precedence over assert_all_responses_were_requested.
"""
testdir.makepyfile(
"""
import httpx
import pytest
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
def test_mandatory_response_not_matched(httpx_mock):
# This response is optional and the fact that it was never requested should not trigger anything
httpx_mock.add_response(url="https://test_url")
# This response MUST be requested
httpx_mock.add_response(url="https://test_url2", is_optional=False)
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, passed=1)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
"* - Match any request on https://test_url2",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
consecutive=True,
)
def test_reusable_response_not_matched(testdir: Testdir) -> None:
testdir.makepyfile(
"""
import httpx
def test_reusable_response_not_matched(httpx_mock):
httpx_mock.add_response(url="https://test_url2", is_reusable=True)
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, passed=1)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
"* - Match every request on https://test_url2",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
consecutive=True,
)