├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── appveyor.yml ├── docs ├── Makefile ├── conf.py ├── development.md ├── index.md └── make.bat ├── mkdocs.yml ├── old_README.md ├── pytest_requests ├── __init__.py ├── patch.py ├── plugin.py └── response.py ├── setup.cfg ├── setup.py ├── tests ├── conftest.py ├── test_pytest_requests.py └── test_simple.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .pytest_cache 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | venv/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask instance folder 59 | instance/ 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # MkDocs documentation 65 | /site/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # pycharm 77 | .idea 78 | 79 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | sudo: false 4 | language: python 5 | 6 | matrix: 7 | include: 8 | - python: 2.7 9 | - python: 3.4 10 | - python: 3.5 11 | - python: 3.6 12 | - python: pypy 13 | - python: 3.6 14 | env: TOX_ENV=flake8 15 | 16 | install: 17 | - pip install tox-travis 18 | 19 | script: 20 | - tox 21 | 22 | before_cache: 23 | - rm -rf $HOME/.cache/pip/log 24 | 25 | cache: 26 | directories: 27 | - $HOME/.cache/pip 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 Brian Okken 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | 4 | recursive-include pytest_requests * 5 | recursive-include tests * 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-requests 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/pytest-requests.svg)](https://pypi.org/project/pytest-requests) [![Python versions](https://img.shields.io/pypi/pyversions/pytest-requests.svg)](https://pypi.org/project/pytest-requests) [![See Build Status on Travis CI](https://travis-ci.org/okken/pytest-requests.svg?branch=master)](https://travis-ci.org/okken/pytest-requests) [![See Build Status on AppVeyor](https://ci.appveyor.com/api/projects/status/github/okken/pytest-requests?branch=master)](https://ci.appveyor.com/project/okken/pytest-requests/branch/master) 4 | 5 | ------------------------------------------------------------------------ 6 | 7 | This project is inactive. 8 | 9 | It is a project for providing fixtures for mocking requests. 10 | 11 | However, we have discovered that [the responses project](https://github.com/getsentry/responses) is quite good. 12 | 13 | Please, try responses first. 14 | 15 | ------------------------------------------------------------------------ 16 | 17 | The [old README](old_README.md) is still around, if the above note hasn't discouraged you. 18 | 19 | Cheers, 20 | Brian 21 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # What Python version is installed where: 2 | # http://www.appveyor.com/docs/installed-software#python 3 | 4 | environment: 5 | matrix: 6 | - PYTHON: "C:\\Python27" 7 | TOX_ENV: "py27" 8 | 9 | - PYTHON: "C:\\Python34" 10 | TOX_ENV: "py34" 11 | 12 | - PYTHON: "C:\\Python35" 13 | TOX_ENV: "py35" 14 | 15 | - PYTHON: "C:\\Python36" 16 | TOX_ENV: "py36" 17 | 18 | init: 19 | - "%PYTHON%/python -V" 20 | - "%PYTHON%/python -c \"import struct;print( 8 * struct.calcsize(\'P\'))\"" 21 | 22 | install: 23 | - "%PYTHON%/Scripts/easy_install -U pip" 24 | - "%PYTHON%/Scripts/pip install tox" 25 | - "%PYTHON%/Scripts/pip install wheel" 26 | 27 | build: false # Not a C# project, build stuff at the test step instead. 28 | 29 | test_script: 30 | - "%PYTHON%/Scripts/tox -e %TOX_ENV%" 31 | 32 | after_test: 33 | - "%PYTHON%/python setup.py bdist_wheel" 34 | - ps: "ls dist" 35 | 36 | artifacts: 37 | - path: dist\* 38 | 39 | #on_success: 40 | # - TODO: upload the content of dist/*.whl to a public wheelhouse 41 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Pytest-requests 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('.')) 18 | 19 | import pytest_requests 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'Pytest-requests' 24 | copyright = '2018, Brian Okken, Anthony Shaw' 25 | author = 'Brian Okken, Anthony Shaw' 26 | 27 | from recommonmark.parser import CommonMarkParser 28 | 29 | source_parsers = { 30 | '.md': CommonMarkParser, 31 | } 32 | 33 | source_suffix = ['.rst', '.md'] 34 | 35 | # The short X.Y version 36 | version = pytest_requests.__version__ 37 | # The full version, including alpha/beta/rc tags 38 | release = pytest_requests.__version__ 39 | 40 | 41 | # -- General configuration --------------------------------------------------- 42 | 43 | # If your documentation needs a minimal Sphinx version, state it here. 44 | # 45 | # needs_sphinx = '1.0' 46 | 47 | # Add any Sphinx extension module names here, as strings. They can be 48 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 49 | # ones. 50 | extensions = [ 51 | ] 52 | 53 | # Add any paths that contain templates here, relative to this directory. 54 | templates_path = ['_templates'] 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | # 62 | # This is also used if you do content translation via gettext catalogs. 63 | # Usually you set "language" from the command line for these cases. 64 | language = None 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path . 69 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 70 | 71 | # The name of the Pygments (syntax highlighting) style to use. 72 | pygments_style = 'sphinx' 73 | 74 | 75 | # -- Options for HTML output ------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | # 80 | html_theme = 'alabaster' 81 | 82 | # Theme options are theme-specific and customize the look and feel of a theme 83 | # further. For a list of options available for each theme, see the 84 | # documentation. 85 | # 86 | # html_theme_options = {} 87 | 88 | # Add any paths that contain custom static files (such as style sheets) here, 89 | # relative to this directory. They are copied after the builtin static files, 90 | # so a file named "default.css" will overwrite the builtin "default.css". 91 | html_static_path = ['_static'] 92 | 93 | # Custom sidebar templates, must be a dictionary that maps document names 94 | # to template names. 95 | # 96 | # The default sidebars (for documents that don't match any pattern) are 97 | # defined by theme itself. Builtin themes are using these templates by 98 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 99 | # 'searchbox.html']``. 100 | # 101 | # html_sidebars = {} 102 | 103 | 104 | # -- Options for HTMLHelp output --------------------------------------------- 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'Pytest-requestsdoc' 108 | 109 | 110 | # -- Options for LaTeX output ------------------------------------------------ 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'Pytest-requests.tex', 'Pytest-requests Documentation', 135 | 'Brian Okken, Anthony Shaw', 'manual'), 136 | ] 137 | 138 | 139 | # -- Options for manual page output ------------------------------------------ 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | (master_doc, 'pytest-requests', 'Pytest-requests Documentation', 145 | [author], 1) 146 | ] 147 | 148 | 149 | # -- Options for Texinfo output ---------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'Pytest-requests', 'Pytest-requests Documentation', 156 | author, 'Pytest-requests', 'One line description of project.', 157 | 'Miscellaneous'), 158 | ] -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | Maintainers guide to the `pytest-requests` package. 4 | 5 | ## Release Checklist 6 | 7 | ### PyPi releases 8 | 9 | - [ ] Check that build passes on all versions 10 | 11 | ### Owner Todos: 12 | 13 | - [ ] Ensure `CHANGES.rst` is up-to-date. 14 | - [ ] Update `CHANGES.rst`, replacing `master` with release version and date. 15 | - [ ] Bumpversion to `release` (with tag). 16 | - [ ] Bumpversion to `minor`. 17 | - [ ] Push release cycle commits and tag to `origin/master`. 18 | - [ ] Update Read the Docs space. 19 | 20 | ### Owner/Contributor Todos: 21 | 22 | - [ ] Clear dist folder using `rm -f dist/*` 23 | - [ ] From *release tag*, build using `python setup.py sdist bdist_wheel` and publish to PyPI (using `twine upload dist/*`). 24 | - [ ] Tell a friend 25 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to pytest-requests 2 | 3 | Fixtures to mock requests 4 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=Pytest-requests 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pytest-requests 2 | site_description: Fixtures to mock requests 3 | site_author: Brian Okken 4 | 5 | theme: readthedocs 6 | 7 | repo_url: https://github.com/okken/pytest-requests 8 | 9 | pages: 10 | - Home: docs/index.md 11 | -------------------------------------------------------------------------------- /old_README.md: -------------------------------------------------------------------------------- 1 | # pytest-requests 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/pytest-requests.svg)](https://pypi.org/project/pytest-requests) [![Python versions](https://img.shields.io/pypi/pyversions/pytest-requests.svg)](https://pypi.org/project/pytest-requests) [![See Build Status on Travis CI](https://travis-ci.org/okken/pytest-requests.svg?branch=master)](https://travis-ci.org/okken/pytest-requests) [![See Build Status on AppVeyor](https://ci.appveyor.com/api/projects/status/github/okken/pytest-requests?branch=master)](https://ci.appveyor.com/project/okken/pytest-requests/branch/master) 4 | 5 | Fixtures to mock requests 6 | 7 | ------------------------------------------------------------------------ 8 | 9 | ## Features 10 | 11 | - Patch responses to APIs with static or dynamic data 12 | - Support for both requests sessions and regular method calls 13 | - Native support for setting responses as dicitonaries for JSON APIs 14 | 15 | ## Requirements 16 | 17 | - PyTest 3.5+ 18 | 19 | ## Installation 20 | 21 | You can install \"pytest-requests\" via 22 | [pip](https://pypi.org/project/pip/) from 23 | [PyPI](https://pypi.org/project): 24 | 25 | ```bash 26 | $ pip install pytest-requests 27 | ``` 28 | 29 | ## Usage 30 | 31 | In the most simple use case, just use the `requests_mock` fixture, which provides 32 | a context manager called `patch`. It returns a patch instance which you can set the `.returns` value to a response. There is a response factory in `.good` or `.bad` which can take a string or a dictionary. 33 | 34 | ```python 35 | import requests 36 | 37 | def test_simple(requests_mock): 38 | with requests_mock.patch('/api/test') as patch: 39 | patch.returns = requests_mock.good('hello') 40 | response = requests.get('https://test.api/api/test') 41 | assert response.text == 'hello' 42 | ``` 43 | 44 | With sessions 45 | 46 | ```python 47 | import requests 48 | from requests.sessions import Session 49 | 50 | def test_simple_with_session(requests_mock): 51 | with requests_mock.patch('/api/test') as patch: 52 | patch.returns = requests_mock.good('hello') 53 | with Session() as s: 54 | response = s.get('https://test.api/api/test') 55 | assert response.text == 'hello' 56 | ``` 57 | 58 | `requests_mock.good` or `requests_mock.bad` can also take a dictionary, which will be converted to a JSON string implicitly. 59 | 60 | ```python 61 | import requests 62 | import pytest 63 | 64 | def test_json(requests_mock): 65 | test_dict = {'a': 'b'} 66 | with requests_mock.patch('/api/test') as patch: 67 | patch.returns = requests_mock.good(test_dict).as_json() 68 | response = requests.get('https://test.api/api/test') 69 | assert response.json() == test_dict 70 | ``` 71 | 72 | Returning specific headers. 73 | 74 | ```python 75 | import requests 76 | import pytest 77 | 78 | def test_json(requests_mock): 79 | with requests_mock.patch('/api/test') as patch: 80 | patch.returns = requests_mock.good('hello', headers={'X-Special': 'value'}) 81 | response = requests.get('https://test.api/api/test') 82 | assert response.text == 'hello' 83 | assert response.headers['X-Special'] == 'value' 84 | ``` 85 | 86 | ## Contributing 87 | 88 | Contributions are very welcome. Tests can be run with 89 | [tox](https://tox.readthedocs.io/en/latest/), please ensure the coverage 90 | at least stays the same before you submit a pull request. 91 | 92 | ## License 93 | 94 | Distributed under the terms of the 95 | [MIT](http://opensource.org/licenses/MIT) license, \"pytest-requests\" 96 | is free and open source software 97 | 98 | ## Issues 99 | 100 | If you encounter any problems, please [file an 101 | issue](https://github.com/okken/pytest-requests/issues) along with a 102 | detailed description. 103 | 104 | ## Credits 105 | 106 | This [pytest](https://github.com/pytest-dev/pytest) plugin was generated 107 | with [Cookiecutter](https://github.com/audreyr/cookiecutter) along with 108 | [\@hackebrot](https://github.com/hackebrot)\'s 109 | [cookiecutter-pytest-plugin](https://github.com/pytest-dev/cookiecutter-pytest-plugin) 110 | template. -------------------------------------------------------------------------------- /pytest_requests/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.0" 2 | -------------------------------------------------------------------------------- /pytest_requests/patch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from mock import patch as mock_patch 4 | from requests.adapters import BaseAdapter 5 | from requests.compat import urlparse 6 | import contextlib 7 | from .response import RequestsResponse 8 | 9 | __all__ = ["patch"] 10 | 11 | 12 | @contextlib.contextmanager 13 | def patch(uri): 14 | adapter = RequestsPatchedAdapter(uri) 15 | patched_adapter = mock_patch("requests.sessions.HTTPAdapter", new=adapter) 16 | patched_adapter.start() 17 | yield adapter 18 | patched_adapter.stop() 19 | 20 | 21 | class RequestsPatchedAdapter(BaseAdapter): 22 | """ 23 | Context-Wrapper for the patched requests HTTP Adapter 24 | """ 25 | 26 | def __init__(self, uri=None): 27 | """ 28 | Instantiate a RequestsPatchedAdapter 29 | 30 | :param uri: The URI to patch 31 | :type uri: ``str`` 32 | """ 33 | self.uri = uri 34 | self._response = None 35 | self._call_count = 0 36 | self._request = None 37 | 38 | def __call__(self): 39 | return self 40 | 41 | @property 42 | def returns(self): 43 | return self._response 44 | 45 | @returns.setter 46 | def returns(self, value): 47 | """ 48 | Set the value that the patch returns 49 | 50 | :param value: The response to patch 51 | :type value: :class:`pytest_requests.response.RequestsResponse` 52 | """ 53 | if not isinstance(value, RequestsResponse): 54 | raise TypeError("Returns value must be an instance of `RequestsResponse`") 55 | self._response = value 56 | 57 | def send( 58 | self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None 59 | ): 60 | self._request = request 61 | self._call_count += 1 62 | url_parts = urlparse(request.url) 63 | if url_parts.path != self.uri: 64 | raise AssertionError( 65 | "URI path not matched, was {0} not {1}".format(url_parts.path, self.uri) 66 | ) 67 | return self._response.to_response(request) 68 | 69 | def was_called_once(self): 70 | """ 71 | Returns a ``bool`` for whether this URL has been called only once. 72 | 73 | :rtype: ``bool`` 74 | """ 75 | if self._call_count != 1: 76 | raise AssertionError( 77 | "URL was called {0} times, not 1".format(self._call_count) 78 | ) 79 | else: 80 | return True 81 | 82 | def was_called_with_headers(self, headers): 83 | """ 84 | Assert that URL was called with specific headers 85 | 86 | :rtype: ``bool`` 87 | """ 88 | for key, value in headers.items(): 89 | assert key in self._request.headers 90 | assert value == self._request.headers[key] 91 | return True 92 | 93 | def close(self): 94 | pass 95 | -------------------------------------------------------------------------------- /pytest_requests/plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import namedtuple 4 | import pytest 5 | from .response import good, bad 6 | from .patch import patch 7 | 8 | 9 | def pytest_addoption(parser): 10 | group = parser.getgroup("requests") 11 | group.addoption( 12 | "--foo", 13 | action="store", 14 | dest="dest_foo", 15 | default="2018", 16 | help='Set the value for the fixture "bar".', 17 | ) 18 | 19 | parser.addini("HELLO", "Dummy pytest.ini setting") 20 | 21 | 22 | @pytest.fixture 23 | def requests_mock(request): 24 | namespace = namedtuple("Namespace", ["good", "bad", "patch"]) 25 | return namespace(good, bad, patch) 26 | -------------------------------------------------------------------------------- /pytest_requests/response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | from requests import Response 5 | from io import BytesIO 6 | import json 7 | 8 | 9 | # Instead of depending on six 10 | if sys.version_info.major == 3: 11 | 12 | def ensure_bytes(string): 13 | if isinstance(string, bytes): 14 | return string 15 | else: 16 | return string.encode() 17 | 18 | 19 | else: 20 | 21 | def ensure_bytes(string): 22 | if isinstance(string, unicode): 23 | return string.encode() 24 | else: 25 | return string 26 | 27 | 28 | def good(body, status_code=200, headers={}): 29 | """ 30 | Return a "good" response, e.g. HTTP 200 OK 31 | with a given body. 32 | 33 | :param body: The body of the message 34 | :type body: ``str`` 35 | 36 | :param status_code: The HTTP status code 37 | :type status_code: ``int`` 38 | 39 | :param headers: Optional HTTP headers 40 | :type headers: ``dict`` 41 | 42 | >>> mock.returns = pytest_requests.good("hello") 43 | """ 44 | if not isinstance(status_code, int): 45 | raise TypeError("Status Code must be of type `int`") 46 | return RequestsResponse(body, status_code=status_code, headers=headers) 47 | 48 | 49 | def bad(body, status_code=500, headers={}): 50 | """ 51 | Return a "bad" response, e.g. HTTP 500 Server-Error 52 | with a given body. 53 | 54 | :param body: The body of the message 55 | :type body: ``str`` 56 | 57 | :param status_code: The HTTP status code 58 | :type status_code: ``int`` 59 | 60 | :param headers: Optional HTTP headers 61 | :type headers: ``dict`` 62 | 63 | >>> mock.returns = pytest_requests.good("hello") 64 | """ 65 | if not isinstance(status_code, int): 66 | raise TypeError("Status Code must be of type `int`") 67 | return RequestsResponse(body, status_code=status_code, headers=headers) 68 | 69 | 70 | class RequestsResponse(object): 71 | """ 72 | Abstraction of :class:`requests.Response` 73 | """ 74 | 75 | def __init__(self, body, status_code, headers={}): 76 | """ 77 | Instantiate a :class:`RequestsResponse` 78 | 79 | :param body: The body of the response, or a dictionary for JSON data 80 | :type body: ``str`` or ``dict`` 81 | 82 | :param status_code: The HTTP status code 83 | :type status_code: ``int`` 84 | 85 | :param headers: Headers for the response 86 | :type headers: ``dict`` 87 | """ 88 | if isinstance(body, dict): 89 | body = json.dumps(body) 90 | 91 | self.body = body 92 | self.status_code = status_code 93 | self.headers = headers 94 | 95 | def as_json(self): 96 | """ 97 | Set the response as a application/json MIME type 98 | """ 99 | self.headers["Content-Type"] = "application/json" 100 | return self 101 | 102 | def as_html(self): 103 | """ 104 | Set the response as a text/html MIME type 105 | """ 106 | self.headers["Content-Type"] = "text/html" 107 | return self 108 | 109 | def as_type(self, mime_type): 110 | """ 111 | Set the Content-Type header of the response 112 | 113 | :param mime_type: The MIME type, e.g. text/html 114 | :type mime_type: ``str`` 115 | """ 116 | self.headers["Content-Type"] = mime_type 117 | return self 118 | 119 | def to_response(self, request): 120 | """ 121 | Convert the response to a native :class:`requests.Response` 122 | instance 123 | 124 | :param request: The request instance 125 | :type request: :class:`requests.Request` 126 | 127 | :rtype: :class:`requests.Response` 128 | """ 129 | response = Response() 130 | response.url = request.url 131 | response.raw = BytesIO(ensure_bytes(self.body)) 132 | response.status_code = self.status_code 133 | response.headers = self.headers 134 | response.request = request 135 | response._content = ensure_bytes(self.body) 136 | return response 137 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:pytest_requests/__init__.py] 9 | 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import codecs 6 | from setuptools import setup 7 | 8 | 9 | with open("README.md", "r") as fh: 10 | long_description = fh.read() 11 | 12 | 13 | setup( 14 | name='pytest-requests', 15 | version='0.2.0', 16 | author='Brian Okken / Anthony Shaw', 17 | author_email='brian@pythontesting.net', 18 | maintainer='Brian Okken / Anthony Shaw', 19 | maintainer_email='brian@pythontesting.net', 20 | license='MIT', 21 | url='https://github.com/okken/pytest-requests', 22 | description='Fixtures to mock requests', 23 | long_description=long_description, 24 | long_description_content_type="text/markdown", 25 | packages=['pytest_requests'], 26 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', 27 | install_requires=['pytest>=3.5.0', 'requests>=2.0.0,<3.0.0', 'mock>=2.0.0'], 28 | classifiers=[ 29 | 'Development Status :: 4 - Beta', 30 | 'Framework :: Pytest', 31 | 'Intended Audience :: Developers', 32 | 'Topic :: Software Development :: Testing', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | 'Programming Language :: Python :: 3.6', 40 | 'Programming Language :: Python :: 3.7', 41 | 'Programming Language :: Python :: Implementation :: CPython', 42 | 'Programming Language :: Python :: Implementation :: PyPy', 43 | 'Operating System :: OS Independent', 44 | 'License :: OSI Approved :: MIT License', 45 | ], 46 | entry_points={ 47 | 'pytest11': [ 48 | 'requests = pytest_requests.plugin', 49 | ], 50 | }, 51 | ) 52 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = "pytester" 2 | -------------------------------------------------------------------------------- /tests/test_pytest_requests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | 6 | def test_fixture_simple_patch(testdir): 7 | """Most basic use case. Patch a simple """ 8 | 9 | # create a temporary pytest test module 10 | testdir.makepyfile( 11 | """ 12 | import requests 13 | 14 | def test_simple(requests_mock): 15 | with requests_mock.patch('/api/test') as patch: 16 | patch.returns = requests_mock.good('hello') 17 | response = requests.get('https://test.api/api/test') 18 | assert response.text == 'hello' 19 | assert patch.was_called_once() 20 | """ 21 | ) 22 | 23 | # run pytest with the following cmd args 24 | result = testdir.runpytest("-v") 25 | 26 | # fnmatch_lines does an assertion internally 27 | result.stdout.fnmatch_lines(["*::test_simple PASSED*"]) 28 | 29 | # make sure that that we get a '0' exit code for the testsuite 30 | assert result.ret == 0 31 | 32 | 33 | def test_fixture_simple_patch_with_session(testdir): 34 | """Use the patch with a requests session """ 35 | 36 | # create a temporary pytest test module 37 | testdir.makepyfile( 38 | """ 39 | import requests 40 | from requests.sessions import Session 41 | 42 | def test_simple_with_session(requests_mock): 43 | with requests_mock.patch('/api/test') as patch: 44 | patch.returns = requests_mock.good('hello') 45 | with Session() as s: 46 | response = s.get('https://test.api/api/test') 47 | assert response.text == 'hello' 48 | """ 49 | ) 50 | 51 | # run pytest with the following cmd args 52 | result = testdir.runpytest("-v") 53 | 54 | # fnmatch_lines does an assertion internally 55 | result.stdout.fnmatch_lines(["*::test_simple_with_session PASSED*"]) 56 | 57 | # make sure that that we get a '0' exit code for the testsuite 58 | assert result.ret == 0 59 | 60 | 61 | def test_fixture_simple_patch_with_session_raises_error(testdir): 62 | """Use the patch with a requests session """ 63 | 64 | # create a temporary pytest test module 65 | testdir.makepyfile( 66 | """ 67 | import requests 68 | from requests.sessions import Session 69 | import requests.exceptions 70 | import pytest 71 | 72 | def test_simple_with_session(requests_mock): 73 | with requests_mock.patch('/api/test') as patch: 74 | patch.returns = requests_mock.bad('hello') 75 | with Session() as s: 76 | response = s.get('https://test.api/api/test') 77 | assert response.text == 'hello' 78 | with pytest.raises(requests.exceptions.HTTPError): 79 | response.raise_for_status() 80 | """ 81 | ) 82 | result = testdir.runpytest("-v") 83 | result.stdout.fnmatch_lines(["*::test_simple_with_session PASSED*"]) 84 | assert result.ret == 0 85 | 86 | 87 | def test_fixture_json_api(testdir): 88 | """ Test a typical JSON API pattern""" 89 | 90 | # create a temporary pytest test module 91 | testdir.makepyfile( 92 | """ 93 | import requests 94 | import pytest 95 | 96 | def test_json(requests_mock): 97 | test_dict = {'a': 'b'} 98 | with requests_mock.patch('/api/test') as patch: 99 | patch.returns = requests_mock.good(test_dict).as_json() 100 | response = requests.get('https://test.api/api/test') 101 | assert response.json() == test_dict 102 | assert 'Content-Type' in response.headers 103 | assert response.headers['Content-Type'] == 'application/json' 104 | """ 105 | ) 106 | 107 | result = testdir.runpytest("-v") 108 | result.stdout.fnmatch_lines(["*::test_json PASSED*"]) 109 | assert result.ret == 0 110 | 111 | 112 | def test_fixture_bad_path(testdir): 113 | # create a temporary pytest test module 114 | testdir.makepyfile( 115 | """ 116 | import requests 117 | import pytest 118 | 119 | def test_simple(requests_mock): 120 | with requests_mock.patch('/api/not_test') as patch: 121 | patch.returns = requests_mock.good('hello') 122 | with pytest.raises(AssertionError): 123 | response = requests.get('https://test.api/api/test') 124 | """ 125 | ) 126 | 127 | result = testdir.runpytest("-v") 128 | result.stdout.fnmatch_lines(["*::test_simple PASSED*"]) 129 | assert result.ret == 0 130 | 131 | 132 | def test_mock_context(testdir): 133 | """Check that the patched HTTPAdapter is reset""" 134 | testdir.makepyfile( 135 | """ 136 | import requests 137 | import requests.sessions 138 | import pytest 139 | 140 | def test_context(requests_mock): 141 | original = requests.sessions.HTTPAdapter 142 | with requests_mock.patch('/api/not_test') as patch: 143 | assert requests.sessions.HTTPAdapter is not original 144 | assert requests.sessions.HTTPAdapter is original 145 | """ 146 | ) 147 | 148 | result = testdir.runpytest("-v") 149 | result.stdout.fnmatch_lines(["*::test_context PASSED*"]) 150 | assert result.ret == 0 151 | 152 | 153 | def test_returned_headers(testdir): 154 | testdir.makepyfile( 155 | """ 156 | import requests 157 | import pytest 158 | 159 | def test_headers(requests_mock): 160 | with requests_mock.patch('/api/test') as patch: 161 | patch.returns = requests_mock.good('hello', headers={'X-Special': 'value'}) 162 | response = requests.get('https://test.api/api/test') 163 | assert response.text == 'hello' 164 | assert response.headers['X-Special'] == 'value' 165 | """ 166 | ) 167 | 168 | result = testdir.runpytest("-v") 169 | result.stdout.fnmatch_lines(["*::test_headers PASSED*"]) 170 | assert result.ret == 0 171 | 172 | 173 | def test_call_count(testdir): 174 | testdir.makepyfile( 175 | """ 176 | import pytest 177 | def test_simple(requests_mock): 178 | with requests_mock.patch('/api/test') as patch: 179 | patch.returns = requests_mock.good('hello') 180 | with pytest.raises(AssertionError): 181 | assert patch.was_called_once() 182 | """ 183 | ) 184 | 185 | result = testdir.runpytest("-v") 186 | result.stdout.fnmatch_lines(["*::test_simple PASSED*"]) 187 | assert result.ret == 0 188 | 189 | 190 | def test_assert_headers(testdir): 191 | testdir.makepyfile( 192 | """ 193 | import pytest 194 | import requests 195 | def test_simple(requests_mock): 196 | test_headers = {'X-Special': 'value'} 197 | with requests_mock.patch('/api/test') as patch: 198 | patch.returns = requests_mock.good('hello') 199 | requests.get('https://api.com/api/test', headers=test_headers) 200 | assert patch.was_called_with_headers(test_headers) 201 | """ 202 | ) 203 | 204 | result = testdir.runpytest("-v") 205 | result.stdout.fnmatch_lines(["*::test_simple PASSED*"]) 206 | assert result.ret == 0 207 | 208 | 209 | def test_assert_headers_invalid(testdir): 210 | testdir.makepyfile( 211 | """ 212 | import pytest 213 | import requests 214 | def test_simple(requests_mock): 215 | test_headers = {'X-Special': 'value'} 216 | test_headers_invalid = {'X-Special': 'not-value'} 217 | 218 | with requests_mock.patch('/api/test') as patch: 219 | patch.returns = requests_mock.good('hello') 220 | requests.get('https://api.com/api/test', headers=test_headers_invalid) 221 | with pytest.raises(AssertionError): 222 | assert patch.was_called_with_headers(test_headers) 223 | """ 224 | ) 225 | 226 | result = testdir.runpytest("-v") 227 | result.stdout.fnmatch_lines(["*::test_simple PASSED*"]) 228 | assert result.ret == 0 229 | 230 | 231 | def test_assert_headers_casing(testdir): 232 | testdir.makepyfile( 233 | """ 234 | import pytest 235 | import requests 236 | def test_simple(requests_mock): 237 | test_headers = {'X-Special': 'value'} 238 | test_headers_lower = {'x-special': 'value'} 239 | 240 | with requests_mock.patch('/api/test') as patch: 241 | patch.returns = requests_mock.good('hello') 242 | requests.get('https://api.com/api/test', headers=test_headers_lower) 243 | assert patch.was_called_with_headers(test_headers) 244 | """ 245 | ) 246 | 247 | result = testdir.runpytest("-v") 248 | result.stdout.fnmatch_lines(["*::test_simple PASSED*"]) 249 | assert result.ret == 0 250 | 251 | 252 | def test_assert_headers_ordering(testdir): 253 | testdir.makepyfile( 254 | """ 255 | import pytest 256 | import requests 257 | 258 | def test_simple(requests_mock): 259 | test_headers = {'X-Special': 'value', 'X-Special-2': 'value2'} 260 | test_headers_2 = {'X-Special-2': 'value2', 'X-Special': 'value'} 261 | 262 | with requests_mock.patch('/api/test') as patch: 263 | patch.returns = requests_mock.good('hello') 264 | requests.get('https://api.com/api/test', headers=test_headers_2) 265 | assert patch.was_called_with_headers(test_headers) 266 | """ 267 | ) 268 | 269 | result = testdir.runpytest("-v") 270 | result.stdout.fnmatch_lines(["*::test_simple PASSED*"]) 271 | assert result.ret == 0 272 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | import requests 5 | from requests.sessions import Session 6 | import requests.sessions 7 | import requests.exceptions 8 | 9 | 10 | def test_fixture_simple_patch(requests_mock): 11 | with requests_mock.patch("/api/test") as patch: 12 | patch.returns = requests_mock.good("hello") 13 | response = requests.get("https://test.api/api/test") 14 | assert response.text == "hello" 15 | assert patch.was_called_once() 16 | 17 | 18 | def test_fixture_simple_patch_with_session(requests_mock): 19 | with requests_mock.patch("/api/test") as patch: 20 | patch.returns = requests_mock.good("hello") 21 | with Session() as s: 22 | response = s.get("https://test.api/api/test") 23 | assert response.text == "hello" 24 | 25 | 26 | def test_fixture_simple_patch_with_session_raises_error(requests_mock): 27 | with requests_mock.patch("/api/test") as patch: 28 | patch.returns = requests_mock.bad("hello") 29 | with Session() as s: 30 | response = s.get("https://test.api/api/test") 31 | assert response.text == "hello" 32 | with pytest.raises(requests.exceptions.HTTPError): 33 | response.raise_for_status() 34 | 35 | 36 | def test_fixture_json_api(requests_mock): 37 | test_dict = {"a": "b"} 38 | with requests_mock.patch("/api/test") as patch: 39 | patch.returns = requests_mock.good(test_dict).as_json() 40 | response = requests.get("https://test.api/api/test") 41 | assert response.json() == test_dict 42 | assert "Content-Type" in response.headers 43 | assert response.headers["Content-Type"] == "application/json" 44 | 45 | 46 | def test_fixture_bad_path(requests_mock): 47 | with requests_mock.patch("/api/not_test") as patch: 48 | patch.returns = requests_mock.good("hello") 49 | with pytest.raises(AssertionError): 50 | requests.get("https://test.api/api/test") 51 | 52 | 53 | def test_mock_context(requests_mock): 54 | original = requests.sessions.HTTPAdapter 55 | with requests_mock.patch("/api/not_test"): 56 | assert requests.sessions.HTTPAdapter is not original 57 | assert requests.sessions.HTTPAdapter is original 58 | 59 | 60 | def test_returned_headers(requests_mock): 61 | with requests_mock.patch("/api/test") as patch: 62 | patch.returns = requests_mock.good("hello", headers={"X-Special": "value"}) 63 | response = requests.get("https://test.api/api/test") 64 | assert response.text == "hello" 65 | assert response.headers["X-Special"] == "value" 66 | 67 | 68 | def test_call_count(requests_mock): 69 | with requests_mock.patch("/api/test") as patch: 70 | patch.returns = requests_mock.good("hello") 71 | with pytest.raises(AssertionError): 72 | assert patch.was_called_once() 73 | 74 | 75 | def test_assert_headers(requests_mock): 76 | test_headers = {"X-Special": "value"} 77 | with requests_mock.patch("/api/test") as patch: 78 | patch.returns = requests_mock.good("hello") 79 | requests.get("https://api.com/api/test", headers=test_headers) 80 | assert patch.was_called_with_headers(test_headers) 81 | 82 | 83 | def test_assert_headers_invalid(requests_mock): 84 | test_headers = {"X-Special": "value"} 85 | test_headers_invalid = {"X-Special": "not-value"} 86 | 87 | with requests_mock.patch("/api/test") as patch: 88 | patch.returns = requests_mock.good("hello") 89 | requests.get("https://api.com/api/test", headers=test_headers_invalid) 90 | with pytest.raises(AssertionError): 91 | assert patch.was_called_with_headers(test_headers) 92 | 93 | 94 | def test_assert_headers_casing(requests_mock): 95 | test_headers = {"X-Special": "value"} 96 | test_headers_lower = {"x-special": "value"} 97 | 98 | with requests_mock.patch("/api/test") as patch: 99 | patch.returns = requests_mock.good("hello") 100 | requests.get("https://api.com/api/test", headers=test_headers_lower) 101 | assert patch.was_called_with_headers(test_headers) 102 | 103 | 104 | def test_assert_headers_ordering(requests_mock): 105 | test_headers = {"X-Special": "value", "X-Special-2": "value2"} 106 | test_headers_2 = {"X-Special-2": "value2", "X-Special": "value"} 107 | 108 | with requests_mock.patch("/api/test") as patch: 109 | patch.returns = requests_mock.good("hello") 110 | requests.get("https://api.com/api/test", headers=test_headers_2) 111 | assert patch.was_called_with_headers(test_headers) 112 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # For more information about tox, see https://tox.readthedocs.io/en/latest/ 2 | [tox] 3 | envlist = py27,py34,py35,py36,py37,pypy 4 | 5 | [testenv] 6 | deps = pytest>=3.0 7 | setenv = 8 | PYTHONPATH = {toxinidir}:{env:PYTHONPATH:} 9 | 10 | commands = pytest {posargs:tests} 11 | 12 | [testenv:flake8] 13 | skip_install = true 14 | deps = flake8 15 | commands = flake8 pytest_requests/ setup.py tests 16 | --------------------------------------------------------------------------------