├── .coveragerc ├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE ├── Makefile ├── README.rst ├── docs ├── Makefile ├── changelog.rst └── source │ ├── api.rst │ ├── changelog.rst │ ├── conf.py │ ├── faq.rst │ ├── index.rst │ ├── installation.rst │ ├── integration.rst │ ├── mock.rst │ ├── terminology.rst │ └── usage.rst ├── doubles ├── __init__.py ├── allowance.py ├── call_count_accumulator.py ├── class_double.py ├── exceptions.py ├── expectation.py ├── instance_double.py ├── lifecycle.py ├── method_double.py ├── nose.py ├── object_double.py ├── patch.py ├── proxy.py ├── proxy_method.py ├── proxy_property.py ├── pytest_plugin.py ├── space.py ├── target.py ├── targets │ ├── __init__.py │ ├── allowance_target.py │ ├── expectation_target.py │ └── patch_target.py ├── testing.py ├── unittest.py ├── utils.py └── verification.py ├── requirements-dev.txt ├── setup.cfg ├── setup.py └── test ├── allow_test.py ├── class_double_test.py ├── conftest.py ├── expect_test.py ├── instance_double_test.py ├── lifecycle_test.py ├── nose_test.py ├── object_double_test.py ├── partial_double_test.py ├── patch_test.py ├── pytest_test.py ├── return_values_test.py └── unittest_test.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = doubles/testing.py 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | /.coverage 4 | /.pytest_cache 5 | /.python-version 6 | /*.egg-info 7 | build 8 | dist 9 | docs/build 10 | htmlcov 11 | venv 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | matrix: 9 | include: 10 | - { python: "3.7", dist: xenial, sudo: true } 11 | install: 12 | - make bootstrap 13 | script: 14 | - make 15 | branches: 16 | except: 17 | - /^v[0-9]/ 18 | after_success: coveralls 19 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.5.3 (2018-10-18) 5 | ---------------- 6 | 7 | - Add six library to install_requires 8 | 9 | 1.5.2 (2018-10-12) 10 | ---------------- 11 | 12 | - Add support for inspect.signature 13 | - Add support of Python 3.7 14 | - Fix inspect DeprecationWarning 15 | 16 | 1.5.1 (2018-7-24) 17 | ---------------- 18 | 19 | - Fix bug which breaks automatic teardown of top-level expectations between test cases 20 | 21 | 1.5.0 (2018-6-07) 22 | ---------------- 23 | 24 | - Report unsatisfied expectations as failures instead of errors. 25 | 26 | 1.4.0 (2018-4-25) 27 | ---------------- 28 | 29 | - Fix bug in unsatisfied `with_args_validator` exceptions. Note this may cause some tests being run with the `unittest` 30 | runner that used to pass to fail. 31 | 32 | 1.3.2 (2018-4-17) 33 | ---------------- 34 | 35 | - Fix bug in `and_raise` 36 | 37 | 1.3.1 (2018-4-16) 38 | ---------------- 39 | 40 | - Support Pytest 3.5 41 | - Support Exceptions with custom args 42 | - Cleanup test runner integration docs 43 | - Update is_class check, use builtin method 44 | - Cleanup some grammar in failure messages 45 | 46 | 1.2.1 (2016-3-20) 47 | ---------------- 48 | 49 | - Make expectation failure messages clearer 50 | 51 | 1.2.0 (2016-3-2) 52 | ---------------- 53 | 54 | - update pytest integration for version >=2.8 55 | - Support arbitrary callables on class 56 | 57 | 1.1.3 (2015-10-3) 58 | ----------------- 59 | 60 | - Fix bug when restoring stubbed attributes. 61 | 62 | 1.1.2 (2015-10-3) 63 | ----------------- 64 | 65 | - Support stubbing callable attributes. 66 | 67 | 1.1.1 (2015-9-23) 68 | ----------------- 69 | 70 | - Optimized suite by using a faster method of retrieving stack frames. 71 | 72 | 1.1.0 (2015-8-23) 73 | ----------------- 74 | 75 | - Native support for futures: `and_return_future` and `and_raise_future` 76 | 77 | 1.0.8 (2015-3-31) 78 | ----------------- 79 | 80 | - Allow with_args_validator to work with expectations 81 | 82 | 1.0.7 (2015-3-17) 83 | ----------------- 84 | 85 | - Added __name__ and __doc__ proxying to ProxyMethod objects. 86 | - Expectations can return values and raise exceptions. 87 | - Add with_args_validator, user_defined arg validators. 88 | - Validate arguments of a subset builtin objects (dict, tuple, list, set). 89 | - Update FAQ. 90 | 91 | 1.0.6 (2015-02-16) 92 | ------------------ 93 | 94 | - Add with_args short hand syntax 95 | - Improve argument verification for mock.ANY and equals 96 | - Fix pep issues that were added to flake8 97 | 98 | 1.0.5 (2015-01-29) 99 | ------------------ 100 | 101 | - Started tracking changes 102 | - Add expect_constructor and allow_constructor 103 | - Add patch and patch_class 104 | - Add clear 105 | - Clarify some error messages 106 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to Doubles 2 | ======================= 3 | 4 | Thank you for using Doubles and for helping to improve it! 5 | 6 | Bugs 7 | ---- 8 | 9 | If you find a bug while using Doubles, please open an issue on GitHub Issues: https://github.com/uber/doubles/issues. 10 | Any exceptions raised by Doubles that are not one of the exception classes in the ``doubles.exceptions`` module are 11 | considered bugs. Unexpected or surprising behavior may also be considered bugs. The goal is for Doubles to be intuitive 12 | and consistent. 13 | 14 | Features 15 | -------- 16 | 17 | Before working on a new feature, please open an issue to propose your idea. This is out of respect for your time. 18 | We want to discuss new features first and don't want you to spend your time working on patches that won't be accepted. 19 | If we agree that the feature is a good fit, you're free to make a pull request with the necessary changes. 20 | And of course, you can always maintain a fork with any additional features you want without discussing them here first. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Uber Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: clean lint 3 | @py.test -s -p no:doubles test 4 | 5 | .PHONY: lint 6 | lint: 7 | @flake8 doubles test 8 | 9 | .PHONY: clean 10 | clean: 11 | @find . -type f -name '*.pyc' -exec rm {} ';' 12 | 13 | .PHONY: bootstrap 14 | bootstrap: 15 | @pip install -r requirements-dev.txt 16 | @pip install -e . 17 | 18 | .PHONY: docs 19 | docs: 20 | @$(MAKE) -C docs html 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | doubles 2 | ======= 3 | 4 | .. image:: https://badge.fury.io/py/doubles.svg 5 | :target: http://badge.fury.io/py/doubles 6 | 7 | .. image:: https://travis-ci.org/uber/doubles.svg?branch=master 8 | :target: https://travis-ci.org/uber/doubles 9 | 10 | .. image:: https://readthedocs.org/projects/doubles/badge/?version=latest 11 | :target: https://doubles.readthedocs.io/en/latest/?badge=latest 12 | 13 | .. image:: https://coveralls.io/repos/github/uber/doubles/badge.svg?branch=master 14 | :target: https://coveralls.io/github/uber/doubles?branch=master 15 | 16 | 17 | **Doubles** is a Python package that provides test doubles for use in automated tests. 18 | 19 | It provides functionality for stubbing, mocking, and verification of test doubles against the real objects they double. 20 | In contrast to the Mock package, it provides a clear, expressive syntax and better safety guarantees to prevent API 21 | drift and to improve confidence in tests using doubles. It comes with drop-in support for test suites run by Pytest, 22 | Nose, or standard unittest. 23 | 24 | Documentation 25 | ------------- 26 | 27 | Documentation is available at http://doubles.readthedocs.org/en/latest/. 28 | 29 | Development 30 | ----------- 31 | 32 | Source code is available at https://github.com/uber/doubles. 33 | 34 | To install the dependencies on a fresh clone of the repository, run ``make bootstrap``. 35 | 36 | To run the test suite, run ``make test``. 37 | 38 | To build the documentation locally, run ``make docs``. 39 | 40 | License 41 | ------- 42 | 43 | MIT: http://opensource.org/licenses/MIT 44 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/doubles.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/doubles.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/doubles" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/doubles" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | Stubs and mocks 5 | --------------- 6 | 7 | .. autofunction:: doubles.allow 8 | .. autofunction:: doubles.expect 9 | .. autofunction:: doubles.allow_constructor 10 | .. autofunction:: doubles.expect_constructor 11 | .. autofunction:: doubles.patch 12 | .. autofunction:: doubles.patch_class 13 | 14 | .. autoclass:: doubles.allowance.Allowance 15 | :members: and_raise, and_return, and_return_result_of, with_args, with_no_args 16 | .. autoclass:: doubles.expectation.Expectation 17 | :members: with_args, with_no_args 18 | 19 | Pure doubles 20 | ------------ 21 | 22 | .. autoclass:: doubles.InstanceDouble 23 | :inherited-members: 24 | .. autoclass:: doubles.ClassDouble 25 | :members: 26 | :inherited-members: 27 | .. autoclass:: doubles.ObjectDouble 28 | :members: 29 | 30 | Test lifecycle 31 | -------------- 32 | .. autofunction:: doubles.verify 33 | .. autofunction:: doubles.teardown 34 | 35 | Exceptions 36 | ---------- 37 | 38 | .. automodule:: doubles.exceptions 39 | :members: 40 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # doubles documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Jun 18 14:43:13 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | import sphinx_rtd_theme 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.coverage', 36 | 'sphinx.ext.viewcode', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix of source filenames. 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = u'doubles' 53 | copyright = u'2014, Uber Technologies, Inc.' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | import pkg_resources 60 | try: 61 | # The full version, including alpha/beta/rc tags. 62 | release = pkg_resources.get_distribution('doubles').version 63 | except pkg_resources.DistributionNotFound: 64 | print("Distribution information not found. Run 'setup.py develop'") 65 | sys.exit(1) 66 | del pkg_resources 67 | 68 | # The short X.Y version. 69 | version = '.'.join(release.split('.')[:2]) 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | #language = None 74 | 75 | # There are two options for replacing |today|: either, you set today to some 76 | # non-false value, then it is used: 77 | #today = '' 78 | # Else, today_fmt is used as the format for a strftime call. 79 | #today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | exclude_patterns = [] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | #default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | #add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | #add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | #show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | #modindex_common_prefix = [] 105 | 106 | # If true, keep warnings as "system message" paragraphs in the built documents. 107 | #keep_warnings = False 108 | 109 | 110 | # -- Options for HTML output ---------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = 'sphinx_rtd_theme' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | #html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (within the static path) to use as favicon of the 136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | html_static_path = ['_static'] 144 | 145 | # Add any extra paths that contain custom files (such as robots.txt or 146 | # .htaccess) here, relative to this directory. These files are copied 147 | # directly to the root of the documentation. 148 | #html_extra_path = [] 149 | 150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 151 | # using the given strftime format. 152 | #html_last_updated_fmt = '%b %d, %Y' 153 | 154 | # If true, SmartyPants will be used to convert quotes and dashes to 155 | # typographically correct entities. 156 | #html_use_smartypants = True 157 | 158 | # Custom sidebar templates, maps document names to template names. 159 | #html_sidebars = {} 160 | 161 | # Additional templates that should be rendered to pages, maps page names to 162 | # template names. 163 | #html_additional_pages = {} 164 | 165 | # If false, no module index is generated. 166 | #html_domain_indices = True 167 | 168 | # If false, no index is generated. 169 | #html_use_index = True 170 | 171 | # If true, the index is split into individual pages for each letter. 172 | #html_split_index = False 173 | 174 | # If true, links to the reST sources are added to the pages. 175 | #html_show_sourcelink = True 176 | 177 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 178 | #html_show_sphinx = True 179 | 180 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 181 | #html_show_copyright = True 182 | 183 | # If true, an OpenSearch description file will be output, and all pages will 184 | # contain a tag referring to it. The value of this option must be the 185 | # base URL from which the finished HTML is served. 186 | #html_use_opensearch = '' 187 | 188 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 189 | #html_file_suffix = None 190 | 191 | # Output file base name for HTML help builder. 192 | htmlhelp_basename = 'doublesdoc' 193 | 194 | 195 | # -- Options for LaTeX output --------------------------------------------- 196 | 197 | latex_elements = { 198 | # The paper size ('letterpaper' or 'a4paper'). 199 | #'papersize': 'letterpaper', 200 | 201 | # The font size ('10pt', '11pt' or '12pt'). 202 | #'pointsize': '10pt', 203 | 204 | # Additional stuff for the LaTeX preamble. 205 | #'preamble': '', 206 | } 207 | 208 | # Grouping the document tree into LaTeX files. List of tuples 209 | # (source start file, target name, title, 210 | # author, documentclass [howto, manual, or own class]). 211 | latex_documents = [ 212 | ('index', 'doubles.tex', u'doubles Documentation', 213 | u'Jimmy Cuadra', 'manual'), 214 | ] 215 | 216 | # The name of an image file (relative to this directory) to place at the top of 217 | # the title page. 218 | #latex_logo = None 219 | 220 | # For "manual" documents, if this is true, then toplevel headings are parts, 221 | # not chapters. 222 | #latex_use_parts = False 223 | 224 | # If true, show page references after internal links. 225 | #latex_show_pagerefs = False 226 | 227 | # If true, show URL addresses after external links. 228 | #latex_show_urls = False 229 | 230 | # Documents to append as an appendix to all manuals. 231 | #latex_appendices = [] 232 | 233 | # If false, no module index is generated. 234 | #latex_domain_indices = True 235 | 236 | 237 | # -- Options for manual page output --------------------------------------- 238 | 239 | # One entry per manual page. List of tuples 240 | # (source start file, name, description, authors, manual section). 241 | man_pages = [ 242 | ('index', 'doubles', u'doubles Documentation', 243 | [u'Jimmy Cuadra'], 1) 244 | ] 245 | 246 | # If true, show URL addresses after external links. 247 | #man_show_urls = False 248 | 249 | 250 | # -- Options for Texinfo output ------------------------------------------- 251 | 252 | # Grouping the document tree into Texinfo files. List of tuples 253 | # (source start file, target name, title, author, 254 | # dir menu entry, description, category) 255 | texinfo_documents = [ 256 | ('index', 'doubles', u'doubles Documentation', 257 | u'Jimmy Cuadra', 'doubles', 'One line description of project.', 258 | 'Miscellaneous'), 259 | ] 260 | 261 | # Documents to append as an appendix to all manuals. 262 | #texinfo_appendices = [] 263 | 264 | # If false, no module index is generated. 265 | #texinfo_domain_indices = True 266 | 267 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 268 | #texinfo_show_urls = 'footnote' 269 | 270 | # If true, do not generate a @detailmenu in the "Top" node's menu. 271 | #texinfo_no_detailmenu = False 272 | -------------------------------------------------------------------------------- /docs/source/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | Common Issues 5 | +++++++++++++ 6 | 7 | 8 | When I double __new__, it breaks other tests, why? 9 | -------------------------------------------------- 10 | 11 | This feature is deprecated, I recommend using the ``patch_class`` method, which fixes this issue and is much cleaner. 12 | 13 | 14 | I get a ``VerifyingDoubleError`` "Cannot double method ... does not implement it", whats going on? 15 | -------------------------------------------------------------------------------------------------- 16 | 17 | Make sure you are using a version of doubles greater than 1.0.1. There was a bug prior to 1.0.1 that would not allow you to mock callable objects. 18 | 19 | 20 | I get a ``VerifyingBuiltinDoubleArgumentError`` "... is not a Python func", what is going on? 21 | --------------------------------------------------------------------------------------------- 22 | 23 | Python does not allow doubles to look into builtin functions and asked them what their call signatures are. Since we can't do this it is impossible to verify the arguments passed into a stubbed method. By default if doubles cannot inspect a function it raises a ``VerifyingBuiltinDoubleArgumentError``, this is most common with builtins. To bypass this functionality for builtins you can do:: 24 | 25 | import functools 26 | 27 | from doubles import no_builtin_verification, allow 28 | 29 | 30 | with no_builtin_verification(): 31 | allow(functools).reduce 32 | 33 | # The test that uses this allowance must be within the context manager. 34 | run_test() 35 | 36 | 37 | Patches 38 | ++++++++ 39 | 40 | How can I make SomeClass(args, kwargs) return my double? 41 | -------------------------------------------------------- 42 | 43 | Use ``patch_class`` and ``allow_constructor``:: 44 | 45 | from doubles import patch_class, allow_constructor 46 | 47 | import myapp 48 | 49 | def test_something_that_uses_user(): 50 | patched_class = patch_class('myapp.user.User') 51 | allow_constructor(patched_class).and_return('Bob Barker') 52 | 53 | assert myapp.user.User() == 'Bob Barker' 54 | 55 | 56 | ``patch_class`` creates a ``ClassDouble`` of the class specified, patches the original class and returns the ``ClassDouble``. We then stub the constructor which controls what is is returned when an instance is created. 57 | 58 | ``allow_constructor`` supports all of the same functionality as ``allow``:: 59 | 60 | from doubles import patch_class, allow_constructor 61 | 62 | import myapp 63 | 64 | def test_something_that_uses_user(): 65 | bob_barker = InstanceDouble('myapp.user.User') 66 | 67 | patched_class = patch_class('myapp.user.User') 68 | allow_constructor(patched_class).with_args('Bob Barker', 100).and_return(bob_barker) 69 | 70 | assert myapp.user.User('Bob Barker', 100) is bob_barker 71 | 72 | 73 | How can I patch something like I do with mock? 74 | ---------------------------------------------- 75 | 76 | Doubles also has ``patch`` but it isn't a decorator:: 77 | 78 | from doubles import allow, patch 79 | 80 | import myapp 81 | 82 | def test_something_that_uses_user(): 83 | patch('myapp.user.User', 'Bob Barker') 84 | 85 | assert myapp.user.User == 'Bob Barker' 86 | 87 | Patches do not verify against the underlying object, so use them carefully. Patches are automatically restored at the end of the test. 88 | 89 | Expectations 90 | +++++++++++++ 91 | 92 | How do I assert that a function is not called? 93 | ---------------------------------------------- 94 | 95 | If you expect the ``send_mail`` method never to be called on ``user``:: 96 | 97 | expect(user).send_mail.never() 98 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Doubles 2 | ======= 3 | 4 | **Doubles** is a Python package that provides test doubles for use in automated tests. 5 | 6 | It provides functionality for stubbing, mocking, and verification of test doubles against the real objects they double. In contrast to the Mock package, it provides a clear, expressive syntax and better safety guarantees to prevent API drift and to improve confidence in tests using doubles. It comes with drop-in support for test suites run by Pytest, Nose, or standard unittest. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | installation 12 | Integration with test frameworks 13 | Differences from Mock 14 | terminology 15 | usage 16 | api 17 | faq 18 | changelog 19 | 20 | 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | 29 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | From PyPI:: 5 | 6 | $ pip install doubles 7 | 8 | 9 | From source:: 10 | 11 | $ git clone https://github.com/uber/doubles 12 | $ cd doubles 13 | $ python setup.py install 14 | -------------------------------------------------------------------------------- /docs/source/integration.rst: -------------------------------------------------------------------------------- 1 | Integration with test frameworks 2 | ================================ 3 | 4 | Doubles includes plugins for automatic integration with popular test runners. 5 | 6 | Pytest 7 | ------ 8 | 9 | Pytest integration will automatically be loaded and activated via setuptools entry points. To disable Doubles for a particular test run, run Pytest as:: 10 | 11 | $ py.test -p no:doubles file_or_directory 12 | 13 | Nose 14 | ---- 15 | 16 | Nose integration will be loaded and activated by running Nose as:: 17 | 18 | $ nosetests --with-doubles file_or_directory 19 | 20 | unittest 21 | -------- 22 | 23 | Inherit from ``doubles.unittest.TestCase`` in your test case classes and the Doubles lifecycle will be managed automatically. 24 | 25 | Manual integration 26 | ------------------ 27 | 28 | If you are using another test runner or need manual control of the Doubles lifecycle, these are the two methods you'll need to use: 29 | 30 | 1. ``doubles.verify`` should be called after each test to verify any expectations made. It can be skipped if the test case has already failed for another reason. 31 | 2. ``doubles.teardown`` must be called after each test and after the call to ``doubles.verify``. 32 | -------------------------------------------------------------------------------- /docs/source/mock.rst: -------------------------------------------------------------------------------- 1 | Differences from Mock 2 | ===================== 3 | 4 | If you've previously used the `Mock `_ package, you may be wondering how **Doubles** is different and why you might want to use it. There are a few main differences: 5 | 6 | * **Mock** follows what it describes as the "action --> assertion" pattern, meaning that you make calls to test doubles and then make assertions afterwards about how they are used. **Doubles** takes the reverse approach: you declare explicitly how your test doubles should behave, and any expectations you've made will be verified automatically at the end of the test. 7 | * **Mock** has one primary class, also called Mock, which can serve the purpose of different types of test doubles depending on how it's used. **Doubles** uses explicit terminology to help your tests better convey their intent. In particulary, there is a clear distinction between a stub and a mock, with separate syntax for each. 8 | * **Doubles** ensures that all test doubles adhere to the interface of the real objects they double. This is akin to **Mock**'s "spec" feature, but is *not* optional. This prevents drift between test double usage and real implementation. Without this feature, it's very easy to have a passing test but broken behavior in production. 9 | * **Doubles** has a fluid interface, using method chains to build up a specification about how a test double should be used which matches closely with how you might describe it in words. 10 | -------------------------------------------------------------------------------- /docs/source/terminology.rst: -------------------------------------------------------------------------------- 1 | Terminology 2 | =========== 3 | 4 | Terminology used when discussing test doubles has often been confused, historically. To alleviate confusion, at least within the scope of using the Doubles library, the following definitions are provided: 5 | 6 | test double 7 | An object that stands in for another object during the course of a test. This is a generic term that describes all the different types of objects the Doubles library provides. 8 | stub 9 | A test double that returns a predetermined value when called. 10 | fake 11 | A test double that has a full implementation that determines what value it will return when called. 12 | mock 13 | A test double that expects to be called in a certain way, and will cause the test to fail if it is not. 14 | pure double 15 | A basic test double that does not modify any existing object in the system. 16 | partial double 17 | A test double that modifies a real object from the production code, doubling some of its methods but leaving others unmodified. 18 | verifying double 19 | A test double that ensures any methods that are doubled on it match the contract of the real object they are standing in for. 20 | allowance 21 | A declaration that one of an object's methods can be called. This is the manner by which stubs are created. 22 | expectation 23 | A declaration that one of an object's methods must be called. This is the manner by which mocks are created. 24 | 25 | Examples of each of these are provided in the :doc:`usage` section. 26 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | **Doubles** is used by creating stubs and mocks with the ``allow`` and ``expect`` functions. Each of these functions takes a "target" object as an argument. The target is the object whose methods will be allowed or expected to be called. For example, if you wanted to expect a call to something like ``User.find_by_id``, then ``User`` would be the target. Using a real object from your system as the target creates a so-called "partial double." 5 | 6 | There are also three constructors, ``InstanceDouble``, ``ClassDouble``, and ``ObjectDouble``, which can be used to create so-called "pure double" targets, meaning they are unique objects which don't modify any existing object in the system. 7 | 8 | The details of ``allow``, ``expect``, and the three pure double constructors follow. 9 | 10 | Stubs and allowances 11 | -------------------- 12 | 13 | Stubs are doubles which have a predetermined result when called. To stub out a method on an object, use the ``allow`` function:: 14 | 15 | from doubles import allow 16 | 17 | from myapp import User 18 | 19 | 20 | def test_allows_get_name(): 21 | user = User('Carl') 22 | 23 | allow(user).get_name 24 | 25 | assert user.get_name() is None 26 | 27 | On the first line of the test, we create a user object from a theoretical class called ``User`` in the application we're testing. The second line declares an *allowance*, after which ``user.get_name`` will use the stub method rather than the real implementation when called. The default return value of a stub is ``None``, which the third line asserts. 28 | 29 | To instruct the stub to return a predetermined value, use the ``and_return`` method:: 30 | 31 | 32 | from doubles import allow 33 | 34 | from myapp import User 35 | 36 | 37 | def test_allows_get_name(): 38 | user = User('Carl') 39 | 40 | allow(user).get_name.and_return('Henry') 41 | 42 | assert user.get_name() == 'Henry' 43 | 44 | By default, once a method call has been allowed, it can be made any number of times and it will always return the value specified. 45 | 46 | The examples shown so far will allow the stubbed method to be called with any arguments that match its signature. To specify that a method call is allowed only with specific arguments, use ``with_args``:: 47 | 48 | from doubles import allow 49 | 50 | from myapp import User 51 | 52 | 53 | def test_allows_set_name_with_args(): 54 | user = User('Carl') 55 | 56 | allow(user).set_name.with_args('Henry') 57 | 58 | user.set_name('Henry') # Returns None 59 | user.set_name('Teddy') # Raises an UnallowedMethodCallError 60 | 61 | You do not need to specifically call ``with_args``, calling the allowance directly is the same as calling ``with_args``. The following example is identical to the code above:: 62 | 63 | from doubles import allow 64 | 65 | from myapp import User 66 | 67 | 68 | def test_allows_set_name_with_args(): 69 | user = User('Carl') 70 | 71 | allow(user).set_name('Henry') 72 | 73 | user.set_name('Henry') # Returns None 74 | user.set_name('Teddy') # Raises an UnallowedMethodCallError 75 | 76 | Multiple allowances can be specified for the same method with different arguments and return values:: 77 | 78 | from doubles import allow 79 | 80 | from myapp import User 81 | 82 | def test_returns_different_values_for_different_arguments(): 83 | user = User('Carl') 84 | 85 | allow(user).speak.with_args('hello').and_return('Carl says hello') 86 | allow(user).speak.with_args('thanks').and_return('Carl says thanks') 87 | 88 | assert user.speak('hello') == 'Carl says hello' 89 | assert user.speak('thanks') == 'Carl says thanks' 90 | 91 | To specify that a method can only be called *with no arguments*, use ``with_no_args``:: 92 | 93 | from doubles import allow 94 | 95 | from myapp import User 96 | 97 | 98 | def test_allows_greet_with_no_args(): 99 | user = User('Carl') 100 | 101 | allow(user).greet.with_no_args().and_return('Hello!') 102 | 103 | user.greet() # Returns 'Hello!' 104 | user.greet('Henry') # Raises an UnallowedMethodCallError 105 | 106 | Without the call to ``with_no_args``, ``user.greet('Henry')`` would have returned ``'Hello!'``. 107 | 108 | Mocks and expectations 109 | ---------------------- 110 | 111 | Stubs are useful for returning predetermined values, but they do not verify that they were interacted with. To add assertions about double interaction into the mix, create a mock object by declaring an *expectation*. This follows a very similar syntax, but uses ``expect`` instead of ``allow``:: 112 | 113 | from doubles import expect 114 | 115 | from myapp import User 116 | 117 | 118 | def test_allows_get_name(): 119 | user = User('Carl') 120 | 121 | expect(user).get_name 122 | 123 | The above test will fail with a ``MockExpectationError`` exception, because we expected ``user.get_name`` to be called, but it was not. To satisfy the mock and make the test pass:: 124 | 125 | from doubles import expect 126 | 127 | from myapp import User 128 | 129 | 130 | def test_allows_get_name(): 131 | user = User('Carl') 132 | 133 | expect(user).get_name 134 | 135 | user.get_name() 136 | 137 | Mocks support the same interface for specifying arguments that stubs do. Mocks do not, however, support specification of return values or exceptions. If you want a test double to return a value or raise an exception, use a stub. Mocks are intended for verifying calls to methods that do not return a meaningful value. If the method does return a value, write an assertion about that value instead of using a mock. 138 | 139 | Doubling top-level functions 140 | ---------------------------- 141 | 142 | The previous sections have shown examples where methods on classes are stubbed or mocked. It's also possible to double a top-level function by importing the module where the function is defined into your test file. Pass the module to ``allow`` or ``expect`` and proceed as normal. In the follow example, imagine that we want to stub a function called ``generate_user_token`` in the ``myapp.util`` module:: 143 | 144 | from doubles import allow 145 | 146 | from myapp import util, User 147 | 148 | def test_get_token_returns_a_newly_generated_token_for_the_user(): 149 | user = User('Carl') 150 | 151 | allow(util).generate_user_token.with_args(user).and_return('dummy user token') 152 | 153 | assert user.get_token() == 'dummy user token' 154 | 155 | Fakes 156 | ----- 157 | 158 | Fakes are doubles that have special logic to determine their return values, rather than returning a simple static value. A double can be given a fake implementation with the ``and_return_result_of`` method, which accepts any callable object:: 159 | 160 | from doubles import allow 161 | 162 | from myapp import User 163 | 164 | 165 | def test_fake(): 166 | user = User('Carl') 167 | 168 | allow(user).greet.and_return_result_of(lambda: 'Hello!') 169 | 170 | assert user.greet() == 'Hello!' 171 | 172 | Although this example is functionally equivalent to calling ``and_return('Hello!')``, the callable passed to ``and_return_result_of`` can be arbitrarily complex. Fake functionality is available for both stubs and mocks. 173 | 174 | Raising exceptions 175 | ------------------ 176 | 177 | Both stubs and mocks allow a method call to raise an exception instead of returning a result using the ``and_raise`` method. Simply pass the object you want to raise as an argument. The following test will pass:: 178 | 179 | from doubles import allow 180 | 181 | from myapp import User 182 | 183 | 184 | def test_raising_an_exception(): 185 | user = User('Carl') 186 | 187 | allow(user).get_name.and_raise(StandardError) 188 | 189 | try: 190 | user.get_name() 191 | except StandardError: 192 | pass 193 | else: 194 | raise AssertionError('Expected test to raise StandardError.') 195 | 196 | If the exception to be raised requires arguments, they can be passed to the Exception constructor directly before ``and_raises`` is invoked:: 197 | 198 | from doubles import allow 199 | 200 | from myapp import User 201 | 202 | 203 | def test_raising_an_exception(): 204 | user = User('Carl') 205 | 206 | allow(user).get_name.and_raise(NonStandardError('an argument', arg2='another arg')) 207 | 208 | try: 209 | user.get_name() 210 | except NonStandardError: 211 | pass 212 | else: 213 | raise AssertionError('Expected test to raise NonStandardError.') 214 | 215 | Call counts 216 | ----------- 217 | 218 | Limits can be set on how many times a doubled method can be called. In most cases, you'll specify an exact call count with the syntax ``exactly(n).times``, which will cause the test to fail if the doubled method is called fewer or more times than you declared:: 219 | 220 | from doubles import expect 221 | 222 | from myapp import User 223 | 224 | def test_expect_one_call(): 225 | user = User('Carl') 226 | 227 | expect(user).get_name.exactly(1).time 228 | 229 | user.get_name() 230 | user.get_name() # Raises a MockExpectationError because it should only be called once 231 | 232 | The convenience methods ``once``, ``twice`` and ``never`` are provided for the most common use cases. The following test will pass:: 233 | 234 | from doubles import expect 235 | 236 | from myapp import User 237 | 238 | def test_call_counts(): 239 | user = User('Carl') 240 | 241 | expect(user).get_name.once() 242 | expect(user).speak.twice() 243 | expect(user).not_called.never() 244 | 245 | user.get_name() 246 | user.speak('hello') 247 | user.speak('good bye') 248 | 249 | To specify lower or upper bounds on call count instead of an exact number, use ``at_least`` and ``at_most``:: 250 | 251 | from doubles import expect 252 | 253 | from myapp import User 254 | 255 | def test_bounded_call_counts(): 256 | user = User('Carl') 257 | 258 | expect(user).get_name.at_least(1).time 259 | expect(user).speak.at_most(2).times 260 | 261 | user.get_name # The test would fail if this wasn't called at least once 262 | user.speak('hello') 263 | user.speak('good bye') 264 | user.speak('oops') # Raises a MockExpectationError because we expected at most two calls 265 | 266 | Call counts can be specified for allowances in addition to expectations, with the caveat that only upper bounds are enforced for allowances, making ``at_least`` a no-op. 267 | 268 | Partial doubles 269 | --------------- 270 | 271 | In all of the examples so far, we added stubs and mocks to an instance of our production ``User`` class. These are called a partial doubles, because only the parts of the object that were explicitly declared as stubs or mocks are affected. The untouched methods on the object behave as usual. Let's take a look at an example that illustrates this.:: 272 | 273 | from doubles import allow 274 | 275 | 276 | class User(object): 277 | @classmethod 278 | def find_by_email(cls, email): 279 | pass 280 | 281 | @classmethod 282 | def find_by_id(cls, user_id): 283 | pass 284 | 285 | def test_partial_double(): 286 | dummy_user = object() 287 | 288 | allow(User).find_by_email.and_return(dummy_user) 289 | 290 | User.find_by_email('alice@example.com') # Returns 291 | User.find_by_id(1) # Returns 292 | 293 | For the sake of the example, assume that the two class methods on ``User`` are implemented to return an instance of the class. We create a sentinel value to use as a dummy user, and stub ``User`` to return that specific object when ``User.find_by_email`` is called. When we then call the two class methods, we see that the method we stubbed returns the sentinel value as we declared, and ``User.find_by_id`` retains its real implementation, returning a ``User`` object. 294 | 295 | After a test has run, all partial doubles will be restored to their pristine, undoubled state. 296 | 297 | Verifying doubles 298 | ----------------- 299 | 300 | One of the trade offs of using test doubles is that production code may change after tests are written, and the doubles may no longer match the interface of the real object they are doubling. This is known as "API drift" and is one possible cause of the situation where a test suite is passing but the production code is broken. The potential for API drift is often used as an argument against using test doubles. **Doubles** provides a feature called verifying doubles to help address API drift and to increase confidence in test suites. 301 | 302 | All test doubles created by **Doubles** are verifying doubles. They will cause the test to fail by raising a ``VerifyingDoubleError`` if an allowance or expectation is declared for a method that does not exist on the real object. In addition, the test will fail if the method exists but is specified with arguments that don't match the real method's signature. 303 | 304 | In all the previous examples, we added stubs and mocks for real methods on the ``User`` object. Let's see what happens if we try to stub a method that doesn't exist:: 305 | 306 | from doubles import allow 307 | 308 | from myapp import User 309 | 310 | 311 | def test_verification(): 312 | user = User('Carl') 313 | 314 | allow(user).foo # Raises a VerifyingDoubleError, because User objects have no foo method 315 | 316 | Similarly, we cannot declare an allowance or expectation with arguments that don't match the actual signature of the doubled method:: 317 | 318 | from doubles import allow 319 | 320 | from myapp import User 321 | 322 | 323 | def test_verification_of_arguments(): 324 | user = User('Carl') 325 | 326 | # Raises a VerifyingDoubleArgumentError, because set_name accepts only one argument 327 | allow(user).set_name.with_args('Henry', 'Teddy') 328 | 329 | Disabling builtin verification 330 | ++++++++++++++++++++++++++++++ 331 | 332 | Some of the objects in Python's standard library are written in C and do not support the same introspection capabilities that user-created objects do. Because of this, the automatic verification features of **Doubles** may not work when you try to double a standard library function. There are two approaches to work around this: 333 | 334 | *Recommended*: Create a simple object that wraps the standard library you want to use. Use your wrapper object from your production code and double the wrapper in your tests. Test the wrapper itself in integration with the real standard library calls, without using test doubles, to ensure that your wrapper works as expected. Although this may seem heavy handed, it's actually a good approach, since it's a common adage of test doubles never to double objects you don't own. 335 | 336 | Alternatively, use the ``no_builtin_verification`` context manager to disable the automatic verification. This is not a recommended approach, but is available if you must use it:: 337 | 338 | from doubles import allow, InstanceDouble, no_builtin_verification 339 | 340 | with no_builtin_verification(): 341 | date = InstanceDouble('datetime.date') 342 | 343 | allow(date).ctime 344 | 345 | assert date.ctime() is None 346 | 347 | Pure doubles 348 | ------------ 349 | 350 | Often it's useful to have a test double that represents a real object, but does not actually touch the real object. These doubles are called pure doubles, and like partial doubles, stubs and mocks are verified against the real object. In contrast to partial doubles, pure doubles do not implement any methods themselves, so allowances and expectations must be explicitly declared for any method that will be called on them. Calling a method that has not been allowed or expected on a pure double will raise an exception, even if the object the pure double represents has such a method. 351 | 352 | There are three different constructors for creating pure doubles, depending on what type of object you're doubling and how it should be verified: 353 | 354 | InstanceDouble 355 | ++++++++++++++ 356 | 357 | ``InstanceDouble`` creates a pure test double that will ensure its usage matches the API of an instance of the provided class. It's used as follows:: 358 | 359 | from doubles import InstanceDouble, allow 360 | 361 | 362 | def test_verifying_instance_double(): 363 | user = InstanceDouble('myapp.User') 364 | 365 | allow(user).foo 366 | 367 | The argument to ``InstanceDouble`` is the fully qualified module path to the class in question. The double that's created will verify itself against an instance of that class. The example above will fail with a ``VerifyingDoubleError`` exception, assuming ``foo`` is not a real instance method. 368 | 369 | ClassDouble 370 | +++++++++++ 371 | 372 | ``ClassDouble`` is the same as ``InstanceDouble``, except that it verifies against the class itself instead of an instance of the class. The following test will fail, assuming ``find_by_foo`` is not a real class method:: 373 | 374 | from doubles import ClassDouble, allow 375 | 376 | def test_verifying_class_double(): 377 | User = ClassDouble('myapp.User') 378 | 379 | allow(User).find_by_foo 380 | 381 | 382 | ObjectDouble 383 | ++++++++++++ 384 | 385 | ``ObjectDouble`` creates a pure test double that is verified against a specific object. The following test will fail, assuming ``foo`` is not a real method on ``some_object``:: 386 | 387 | from doubles import ObjectDouble, allow 388 | 389 | from myapp import some_object 390 | 391 | 392 | def test_verifying_object_double(): 393 | something = ObjectDouble(some_object) 394 | 395 | allow(something).foo 396 | 397 | There is a subtle distinction between a pure test double created with ``ObjectDouble`` and a partial double created by passing a non-double object to ``allow`` or ``expect``. The former creates an object that does not accept any method calls which are not explicitly allowed, but verifies any that are against the real object. A partial double modifies parts of the real object itself, allowing some methods to be doubled and others to retain their real implementation. 398 | 399 | Clearing Allowances 400 | +++++++++++++++++++ 401 | 402 | If you ever want to to clear all allowances and expectations you have set without verifying them, use ``teardown``:: 403 | 404 | from doubles import teardown, expect 405 | 406 | def test_clearing_allowances(): 407 | expect(some_object).foobar 408 | 409 | teardown() 410 | 411 | If you ever want to to clear all allowances and expectations you have set on an individual object without verifying them, use ``clear``:: 412 | 413 | from doubles import clear, expect 414 | 415 | def test_clearing_allowances(): 416 | expect(some_object).foobar 417 | 418 | clear(some_object) 419 | 420 | Patching 421 | -------- 422 | 423 | ``patch`` is used to replace an existing object:: 424 | 425 | from doubles import patch 426 | import doubles.testing 427 | 428 | def test_patch(): 429 | patch('doubles.testing.User', 'Bob Barker') 430 | 431 | assert doubles.testing.User == 'Bob Barker' 432 | 433 | Patches do not verify against the underlying object, so use them carefully. Patches are automatically restored at the end of the test. 434 | 435 | Patching Classes 436 | ++++++++++++++++ 437 | ``patch_class`` is a wrapper on top of ``patch`` to help you patch a python class with a ``ClassDouble``. ``patch_class`` creates a ``ClassDouble`` of the class specified, patches the original class and returns the ``ClassDouble``:: 438 | 439 | 440 | from doubles import patch_class, ClassDouble 441 | import doubles.testing 442 | 443 | def test_patch_class(): 444 | class_double = patch_class('doubles.testing.User') 445 | 446 | assert doubles.testing.User is class_double 447 | assert isinstance(class_double, ClassDouble) 448 | 449 | Stubbing Constructors 450 | --------------------- 451 | 452 | By default ``ClassDoubles`` cannot create new instances:: 453 | 454 | from doubles import ClassDouble 455 | 456 | def test_unstubbed_constructor(): 457 | User = ClassDouble('doubles.testing.User') 458 | User('Teddy', 1901) # Raises an UnallowedMethodCallError 459 | 460 | Stubbing the constructor of a ``ClassDouble`` is very similar to using ``allow`` or ``expect`` except we use: ``allow_constructor`` or ``expect_constructor``, and don't specify a method:: 461 | 462 | from doubles import allow_constructor, ClassDouble 463 | import doubles.testing 464 | 465 | def test_allow_constructor_with_args(): 466 | User = ClassDouble('doubles.testing.User') 467 | 468 | allow_constructor(User).with_args('Bob', 100).and_return('Bob') 469 | 470 | assert User('Bob', 100) == 'Bob' 471 | 472 | The return value of ``allow_constructor`` and ``expect_constructor`` support all of the same methods as allow/expect. (e.g. ``with_args``, ``once``, ``exactly``, .etc). 473 | 474 | 475 | *NOTE*: Currently you can only stub the constructor of ``ClassDoubles`` 476 | 477 | Stubbing Asynchronous Methods 478 | ----------------------------- 479 | 480 | Stubbing asynchronous methods requires returning futures ``and_return_future`` and ``and_raise_future`` do it for you. 481 | 482 | 483 | Returning Values 484 | ++++++++++++++++ 485 | 486 | Stubbing a method with ``and_return_future`` is similar to using ``and_return``, except the value is wrapped in a ``Future``:: 487 | 488 | from doubles import allow, InstanceDouble 489 | 490 | def test_and_return_future(): 491 | user = InstanceDouble('doubles.testing.User') 492 | allow(user).instance_method.and_return_future('Bob Barker') 493 | 494 | result = user.instance_method() 495 | assert result.result() == 'Bob Barker' 496 | 497 | Raising Exceptions 498 | ++++++++++++++++++ 499 | 500 | Stubbing a method with ``and_raise_future`` is similar to using ``and_raise``, except the exceptions is wrapped in a ``Future``:: 501 | 502 | from doubles import allow, InstanceDouble 503 | from pytest import raises 504 | 505 | def test_and_raise_future(): 506 | user = InstanceDouble('doubles.testing.User') 507 | exception = Exception('Bob Barker') 508 | allow(user).instance_method.and_raise_future(exception) 509 | result = user.instance_method() 510 | 511 | with raises(Exception) as e: 512 | result.result() 513 | 514 | assert e.value == exception 515 | -------------------------------------------------------------------------------- /doubles/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.5.3' 2 | 3 | from doubles.class_double import ClassDouble # noqa 4 | from doubles.instance_double import InstanceDouble # noqa 5 | from doubles.lifecycle import ( # noqa 6 | teardown, 7 | verify, 8 | no_builtin_verification, 9 | clear, 10 | ) 11 | from doubles.object_double import ObjectDouble # noqa 12 | from doubles.targets.allowance_target import allow, allow_constructor # noqa 13 | from doubles.targets.expectation_target import expect, expect_constructor # noqa 14 | from doubles.targets.patch_target import patch, patch_class # noqa 15 | -------------------------------------------------------------------------------- /doubles/allowance.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | 4 | import six 5 | 6 | from doubles.call_count_accumulator import CallCountAccumulator 7 | from doubles.exceptions import MockExpectationError, VerifyingBuiltinDoubleArgumentError 8 | import doubles.lifecycle 9 | from doubles.verification import verify_arguments 10 | 11 | _any = object() 12 | 13 | 14 | def _get_future(): 15 | try: 16 | from concurrent.futures import Future 17 | except ImportError: 18 | try: 19 | from tornado.concurrent import Future 20 | except ImportError: 21 | raise ImportError( 22 | 'Error Importing Future, Could not find concurrent.futures or tornado.concurrent', 23 | ) 24 | return Future() 25 | 26 | 27 | def verify_count_is_non_negative(func): 28 | @functools.wraps(func) 29 | def inner(self, arg): 30 | if arg < 0: 31 | raise TypeError(func.__name__ + ' requires one positive integer argument') 32 | return func(self, arg) 33 | return inner 34 | 35 | 36 | def check_func_takes_args(func): 37 | if six.PY3: 38 | arg_spec = inspect.getfullargspec(func) 39 | return any([arg_spec.args, arg_spec.varargs, arg_spec.varkw, arg_spec.defaults]) 40 | else: 41 | arg_spec = inspect.getargspec(func) 42 | return any([arg_spec.args, arg_spec.varargs, arg_spec.keywords, arg_spec.defaults]) 43 | 44 | 45 | def build_argument_repr_string(args, kwargs): 46 | args = [repr(x) for x in args] 47 | kwargs = ['{}={!r}'.format(k, v) for k, v in kwargs.items()] 48 | return '({})'.format(', '.join(args + kwargs)) 49 | 50 | 51 | class Allowance(object): 52 | """An individual method allowance (stub).""" 53 | 54 | def __init__(self, target, method_name, caller): 55 | """ 56 | :param Target target: The object owning the method to stub. 57 | :param str method_name: The name of the method to stub. 58 | """ 59 | 60 | self._target = target 61 | self._method_name = method_name 62 | self._caller = caller 63 | self.args = _any 64 | self.kwargs = _any 65 | self._custom_matcher = None 66 | self._is_satisfied = True 67 | self._call_counter = CallCountAccumulator() 68 | 69 | self._return_value = lambda *args, **kwargs: None 70 | 71 | def and_raise(self, exception, *args, **kwargs): 72 | """Causes the double to raise the provided exception when called. 73 | 74 | If provided, additional arguments (positional and keyword) passed to 75 | `and_raise` are used in the exception instantiation. 76 | 77 | :param Exception exception: The exception to raise. 78 | """ 79 | def proxy_exception(*proxy_args, **proxy_kwargs): 80 | raise exception 81 | 82 | self._return_value = proxy_exception 83 | return self 84 | 85 | def and_raise_future(self, exception): 86 | """Similar to `and_raise` but the doubled method returns a future. 87 | 88 | :param Exception exception: The exception to raise. 89 | """ 90 | future = _get_future() 91 | future.set_exception(exception) 92 | return self.and_return(future) 93 | 94 | def and_return_future(self, *return_values): 95 | """Similar to `and_return` but the doubled method returns a future. 96 | 97 | :param object return_values: The values the double will return when called, 98 | """ 99 | futures = [] 100 | for value in return_values: 101 | future = _get_future() 102 | future.set_result(value) 103 | futures.append(future) 104 | return self.and_return(*futures) 105 | 106 | def and_return(self, *return_values): 107 | """Set a return value for an allowance 108 | 109 | Causes the double to return the provided values in order. If multiple 110 | values are provided, they are returned one at a time in sequence as the double is called. 111 | If the double is called more times than there are return values, it should continue to 112 | return the last value in the list. 113 | 114 | 115 | :param object return_values: The values the double will return when called, 116 | """ 117 | 118 | if not return_values: 119 | raise TypeError('and_return() expected at least 1 return value') 120 | 121 | return_values = list(return_values) 122 | final_value = return_values.pop() 123 | 124 | self.and_return_result_of( 125 | lambda: return_values.pop(0) if return_values else final_value 126 | ) 127 | return self 128 | 129 | def and_return_result_of(self, return_value): 130 | """ Causes the double to return the result of calling the provided value. 131 | 132 | :param return_value: A callable that will be invoked to determine the double's return value. 133 | :type return_value: any callable object 134 | """ 135 | if not check_func_takes_args(return_value): 136 | self._return_value = lambda *args, **kwargs: return_value() 137 | else: 138 | self._return_value = return_value 139 | 140 | return self 141 | 142 | def is_satisfied(self): 143 | """Returns a boolean indicating whether or not the double has been satisfied. 144 | 145 | Stubs are always satisfied, but mocks are only satisfied if they've been 146 | called as was declared. 147 | 148 | :return: Whether or not the double is satisfied. 149 | :rtype: bool 150 | """ 151 | return self._is_satisfied 152 | 153 | def with_args(self, *args, **kwargs): 154 | """Declares that the double can only be called with the provided arguments. 155 | 156 | :param args: Any positional arguments required for invocation. 157 | :param kwargs: Any keyword arguments required for invocation. 158 | """ 159 | 160 | self.args = args 161 | self.kwargs = kwargs 162 | self.verify_arguments() 163 | return self 164 | 165 | def with_args_validator(self, matching_function): 166 | """Define a custom function for testing arguments 167 | 168 | :param func matching_function: The function used to test arguments passed to the stub. 169 | """ 170 | self.args = None 171 | self.kwargs = None 172 | self._custom_matcher = matching_function 173 | return self 174 | 175 | def __call__(self, *args, **kwargs): 176 | """A short hand syntax for with_args 177 | 178 | Allows callers to do: 179 | allow(module).foo.with_args(1, 2) 180 | With: 181 | allow(module).foo(1, 2) 182 | 183 | :param args: Any positional arguments required for invocation. 184 | :param kwargs: Any keyword arguments required for invocation. 185 | """ 186 | return self.with_args(*args, **kwargs) 187 | 188 | def with_no_args(self): 189 | """Declares that the double can only be called with no arguments.""" 190 | 191 | self.args = () 192 | self.kwargs = {} 193 | self.verify_arguments() 194 | return self 195 | 196 | def satisfy_any_args_match(self): 197 | """Returns a boolean indicating whether or not the stub will accept arbitrary arguments. 198 | 199 | This will be true unless the user has specified otherwise using ``with_args`` or 200 | ``with_no_args``. 201 | 202 | :return: Whether or not the stub accepts arbitrary arguments. 203 | :rtype: bool 204 | """ 205 | 206 | return self.args is _any and self.kwargs is _any 207 | 208 | def satisfy_exact_match(self, args, kwargs): 209 | """Returns a boolean indicating whether or not the stub will accept the provided arguments. 210 | 211 | :return: Whether or not the stub accepts the provided arguments. 212 | :rtype: bool 213 | """ 214 | 215 | if self.args is None and self.kwargs is None: 216 | return False 217 | elif self.args is _any and self.kwargs is _any: 218 | return True 219 | elif args == self.args and kwargs == self.kwargs: 220 | return True 221 | elif len(args) != len(self.args) or len(kwargs) != len(self.kwargs): 222 | return False 223 | 224 | if not all(x == y or y == x for x, y in zip(args, self.args)): 225 | return False 226 | 227 | for key, value in self.kwargs.items(): 228 | if key not in kwargs: 229 | return False 230 | elif not (kwargs[key] == value or value == kwargs[key]): 231 | return False 232 | 233 | return True 234 | 235 | def satisfy_custom_matcher(self, args, kwargs): 236 | """Return a boolean indicating if the args satisfy the stub 237 | 238 | :return: Whether or not the stub accepts the provided arguments. 239 | :rtype: bool 240 | """ 241 | if not self._custom_matcher: 242 | return False 243 | try: 244 | return self._custom_matcher(*args, **kwargs) 245 | except Exception: 246 | return False 247 | 248 | def return_value(self, *args, **kwargs): 249 | """Extracts the real value to be returned from the wrapping callable. 250 | 251 | :return: The value the double should return when called. 252 | """ 253 | 254 | self._called() 255 | return self._return_value(*args, **kwargs) 256 | 257 | def verify_arguments(self, args=None, kwargs=None): 258 | """Ensures that the arguments specified match the signature of the real method. 259 | 260 | :raise: ``VerifyingDoubleError`` if the arguments do not match. 261 | """ 262 | 263 | args = self.args if args is None else args 264 | kwargs = self.kwargs if kwargs is None else kwargs 265 | 266 | try: 267 | verify_arguments(self._target, self._method_name, args, kwargs) 268 | except VerifyingBuiltinDoubleArgumentError: 269 | if doubles.lifecycle.ignore_builtin_verification(): 270 | raise 271 | 272 | @verify_count_is_non_negative 273 | def exactly(self, n): 274 | """Set an exact call count allowance 275 | 276 | :param integer n: 277 | """ 278 | 279 | self._call_counter.set_exact(n) 280 | return self 281 | 282 | @verify_count_is_non_negative 283 | def at_least(self, n): 284 | """Set a minimum call count allowance 285 | 286 | :param integer n: 287 | """ 288 | 289 | self._call_counter.set_minimum(n) 290 | return self 291 | 292 | @verify_count_is_non_negative 293 | def at_most(self, n): 294 | """Set a maximum call count allowance 295 | 296 | :param integer n: 297 | """ 298 | 299 | self._call_counter.set_maximum(n) 300 | return self 301 | 302 | def never(self): 303 | """Set an expected call count allowance of 0""" 304 | 305 | self.exactly(0) 306 | return self 307 | 308 | def once(self): 309 | """Set an expected call count allowance of 1""" 310 | 311 | self.exactly(1) 312 | return self 313 | 314 | def twice(self): 315 | """Set an expected call count allowance of 2""" 316 | 317 | self.exactly(2) 318 | return self 319 | 320 | @property 321 | def times(self): 322 | return self 323 | time = times 324 | 325 | def _called(self): 326 | """Indicate that the allowance was called 327 | 328 | :raise MockExpectationError if the allowance has been called too many times 329 | """ 330 | 331 | if self._call_counter.called().has_too_many_calls(): 332 | self.raise_failure_exception() 333 | 334 | def raise_failure_exception(self, expect_or_allow='Allowed'): 335 | """Raises a ``MockExpectationError`` with a useful message. 336 | 337 | :raise: ``MockExpectationError`` 338 | """ 339 | 340 | raise MockExpectationError( 341 | "{} '{}' to be called {}on {!r} with {}, but was not. ({}:{})".format( 342 | expect_or_allow, 343 | self._method_name, 344 | self._call_counter.error_string(), 345 | self._target.obj, 346 | self._expected_argument_string(), 347 | self._caller.filename, 348 | self._caller.lineno, 349 | ) 350 | ) 351 | 352 | def _expected_argument_string(self): 353 | """Generates a string describing what arguments the double expected. 354 | 355 | :return: A string describing expected arguments. 356 | :rtype: str 357 | """ 358 | 359 | if self.args is _any and self.kwargs is _any: 360 | return 'any args' 361 | elif self._custom_matcher: 362 | return "custom matcher: '{}'".format(self._custom_matcher.__name__) 363 | else: 364 | return build_argument_repr_string(self.args, self.kwargs) 365 | -------------------------------------------------------------------------------- /doubles/call_count_accumulator.py: -------------------------------------------------------------------------------- 1 | def pluralize(word, count): 2 | return word if count == 1 else word + 's' 3 | 4 | 5 | class CallCountAccumulator(object): 6 | def __init__(self): 7 | self._call_count = 0 8 | 9 | def set_exact(self, n): 10 | """Set an exact call count expectation 11 | 12 | :param integer n: 13 | """ 14 | 15 | self._exact = n 16 | 17 | def set_minimum(self, n): 18 | """Set a minimum call count expectation 19 | 20 | :param integer n: 21 | """ 22 | 23 | self._minimum = n 24 | 25 | def set_maximum(self, n): 26 | """Set a maximum call count expectation 27 | 28 | :param integer n: 29 | """ 30 | 31 | self._maximum = n 32 | 33 | def has_too_many_calls(self): 34 | """Test if there have been too many calls 35 | 36 | :rtype boolean 37 | """ 38 | 39 | if self.has_exact and self._call_count > self._exact: 40 | return True 41 | if self.has_maximum and self._call_count > self._maximum: 42 | return True 43 | return False 44 | 45 | def has_too_few_calls(self): 46 | """Test if there have not been enough calls 47 | 48 | :rtype boolean 49 | """ 50 | 51 | if self.has_exact and self._call_count < self._exact: 52 | return True 53 | if self.has_minimum and self._call_count < self._minimum: 54 | return True 55 | return False 56 | 57 | def has_incorrect_call_count(self): 58 | """Test if there have not been a valid number of calls 59 | 60 | :rtype boolean 61 | """ 62 | 63 | return self.has_too_few_calls() or self.has_too_many_calls() 64 | 65 | def has_correct_call_count(self): 66 | """Test if there have been a valid number of calls 67 | 68 | :rtype boolean 69 | """ 70 | 71 | return not self.has_incorrect_call_count() 72 | 73 | def never(self): 74 | """Test if the number of expect is 0 75 | 76 | :rtype: boolean 77 | """ 78 | 79 | return self.has_exact and self._exact == 0 80 | 81 | def called(self): 82 | """Increment the call count""" 83 | 84 | self._call_count += 1 85 | return self 86 | 87 | @property 88 | def count(self): 89 | """Extract the current call count 90 | 91 | :rtype integer 92 | """ 93 | 94 | return self._call_count 95 | 96 | @property 97 | def has_minimum(self): 98 | """Test if self has a minimum call count set 99 | 100 | :rtype boolean 101 | """ 102 | 103 | return getattr(self, '_minimum', None) is not None 104 | 105 | @property 106 | def has_maximum(self): 107 | """Test if self has a maximum call count set 108 | 109 | :rtype boolean 110 | """ 111 | 112 | return getattr(self, '_maximum', None) is not None 113 | 114 | @property 115 | def has_exact(self): 116 | """Test if self has an exact call count set 117 | 118 | :rtype boolean 119 | """ 120 | 121 | return getattr(self, '_exact', None) is not None 122 | 123 | def _restriction_string(self): 124 | """Get a string explaining the expectation currently set 125 | 126 | e.g `at least 5 times`, `at most 1 time`, or `2 times` 127 | 128 | :rtype string 129 | """ 130 | 131 | if self.has_minimum: 132 | string = 'at least ' 133 | value = self._minimum 134 | elif self.has_maximum: 135 | string = 'at most ' 136 | value = self._maximum 137 | elif self.has_exact: 138 | string = '' 139 | value = self._exact 140 | 141 | return (string + '{} {}').format( 142 | value, 143 | pluralize('time', value) 144 | ) 145 | 146 | def error_string(self): 147 | """Returns a well formed error message 148 | 149 | e.g at least 5 times but was called 4 times 150 | 151 | :rtype string 152 | """ 153 | 154 | if self.has_correct_call_count(): 155 | return '' 156 | 157 | return '{} instead of {} {} '.format( 158 | self._restriction_string(), 159 | self.count, 160 | pluralize('time', self.count) 161 | ) 162 | -------------------------------------------------------------------------------- /doubles/class_double.py: -------------------------------------------------------------------------------- 1 | from doubles.exceptions import UnallowedMethodCallError 2 | from doubles.instance_double import InstanceDouble 3 | from doubles.target import Target 4 | from doubles.verification import verify_arguments 5 | 6 | 7 | def patch_class(input_class): 8 | """Create a new class based on the input_class. 9 | 10 | :param class input_class: The class to patch. 11 | :rtype class: 12 | """ 13 | class Instantiator(object): 14 | @classmethod 15 | def _doubles__new__(self, *args, **kwargs): 16 | pass 17 | 18 | new_class = type(input_class.__name__, (input_class, Instantiator), {}) 19 | 20 | return new_class 21 | 22 | 23 | class ClassDouble(InstanceDouble): 24 | """ 25 | A pure double representing the target class. 26 | 27 | :: 28 | 29 | User = ClassDouble('myapp.User') 30 | 31 | :param str path: The absolute module path to the class. 32 | """ 33 | 34 | is_class = True 35 | 36 | def __init__(self, path): 37 | super(ClassDouble, self).__init__(path) 38 | self._doubles_target = patch_class(self._doubles_target) 39 | self._target = Target(self._doubles_target) 40 | 41 | def __call__(self, *args, **kwargs): 42 | """Verify arguments and proxy to _doubles__new__ 43 | 44 | :rtype obj: 45 | :raises VerifyingDoubleArgumentError: If args/kwargs don't match the expected arguments of 46 | __init__ of the underlying class. 47 | """ 48 | verify_arguments(self._target, '_doubles__new__', args, kwargs) 49 | return self._doubles__new__(*args, **kwargs) 50 | 51 | def _doubles__new__(self, *args, **kwargs): 52 | """Raises an UnallowedMethodCallError 53 | 54 | NOTE: This method is here only to raise if it has not been stubbed 55 | """ 56 | raise UnallowedMethodCallError('Cannot call __new__ on a ClassDouble without stubbing it') 57 | -------------------------------------------------------------------------------- /doubles/exceptions.py: -------------------------------------------------------------------------------- 1 | class MockExpectationError(AssertionError): 2 | """An exception raised when a mock fails verification.""" 3 | 4 | pass 5 | 6 | 7 | class UnallowedMethodCallError(AssertionError): 8 | """An exception raised when an unallowed method call is made on a double.""" 9 | 10 | pass 11 | 12 | 13 | class VerifyingDoubleError(AssertionError): 14 | """ 15 | An exception raised when attempting to double a method that does not exist on the real object. 16 | """ 17 | 18 | def __init__(self, method_name, doubled_obj): 19 | """ 20 | :param str method_name: The name of the method to double. 21 | :param object doubled_obj: The real object being doubled. 22 | """ 23 | 24 | self._method_name = method_name 25 | self._doubled_obj = doubled_obj 26 | self.args = (method_name, doubled_obj) 27 | self.message = "Cannot double method '{}' of '{}'." 28 | 29 | def no_matching_method(self): 30 | self.message = "Cannot double method '{}' because {} does not implement it." 31 | 32 | return self 33 | 34 | def not_callable(self): 35 | self.message = "Cannot double method '{}' because it is not a callable attribute on {}." 36 | 37 | return self 38 | 39 | def requires_instance(self): 40 | self.message = "Cannot double method '{}' because it is not callable directly on {}." 41 | 42 | return self 43 | 44 | def __str__(self): 45 | return self.message.format(self._method_name, self._doubled_obj) 46 | 47 | 48 | class VerifyingDoubleArgumentError(AssertionError): 49 | """ 50 | An exception raised when attempting to double a method with arguments that do not match the 51 | signature of the real method. 52 | """ 53 | 54 | pass 55 | 56 | 57 | class VerifyingBuiltinDoubleArgumentError(VerifyingDoubleArgumentError): 58 | """ 59 | An exception raised when attempting to validate arguments of a builtin. 60 | """ 61 | 62 | pass 63 | 64 | 65 | class VerifyingDoubleImportError(AssertionError): 66 | """ 67 | An exception raised when attempting to create a verifying double from an invalid module path. 68 | """ 69 | 70 | 71 | class ConstructorDoubleError(AssertionError): 72 | """ 73 | An exception raised when attempting to double the constructor of a non ClassDouble. 74 | """ 75 | -------------------------------------------------------------------------------- /doubles/expectation.py: -------------------------------------------------------------------------------- 1 | from doubles.allowance import Allowance 2 | 3 | 4 | class Expectation(Allowance): 5 | """An individual method expectation (mock).""" 6 | 7 | def __init__(self, target, method_name, caller): 8 | """ 9 | :param Target target: The object owning the method to mock. 10 | :param str method_name: The name of the method to mock. 11 | :param tuple caller: Details of the stack frame where the expectation was made. 12 | """ 13 | 14 | super(Expectation, self).__init__(target, method_name, caller) 15 | self._is_satisfied = False 16 | 17 | def satisfy_any_args_match(self): 18 | """ 19 | Returns a boolean indicating whether or not the mock will accept arbitrary arguments. 20 | This will be true unless the user has specified otherwise using ``with_args`` or 21 | ``with_no_args``. 22 | 23 | :return: Whether or not the mock accepts arbitrary arguments. 24 | :rtype: bool 25 | """ 26 | 27 | is_match = super(Expectation, self).satisfy_any_args_match() 28 | 29 | if is_match: 30 | self._satisfy() 31 | 32 | return is_match 33 | 34 | def satisfy_exact_match(self, args, kwargs): 35 | """ 36 | Returns a boolean indicating whether or not the mock will accept the provided arguments. 37 | 38 | :return: Whether or not the mock accepts the provided arguments. 39 | :rtype: bool 40 | """ 41 | 42 | is_match = super(Expectation, self).satisfy_exact_match(args, kwargs) 43 | 44 | if is_match: 45 | self._satisfy() 46 | 47 | return is_match 48 | 49 | def satisfy_custom_matcher(self, args, kwargs): 50 | """Returns a boolean indicating whether or not the mock will accept the provided arguments. 51 | 52 | :param tuple args: A tuple of position args 53 | :param dict kwargs: A dictionary of keyword args 54 | :return: Whether or not the mock accepts the provided arguments. 55 | :rtype: bool 56 | """ 57 | 58 | is_match = super(Expectation, self).satisfy_custom_matcher(args, kwargs) 59 | 60 | if is_match: 61 | self._satisfy() 62 | 63 | return is_match 64 | 65 | def _satisfy(self): 66 | """Marks the mock as satisfied.""" 67 | 68 | self._is_satisfied = True 69 | 70 | def raise_failure_exception(self): 71 | """ 72 | Raises a ``MockExpectationError`` with a useful message. 73 | 74 | :raise: ``MockExpectationError`` 75 | """ 76 | 77 | super(Expectation, self).raise_failure_exception('Expected') 78 | 79 | def is_satisfied(self): 80 | """ 81 | Returns a boolean indicating whether or not the double has been satisfied. Stubs are 82 | always satisfied, but mocks are only satisfied if they've been called as was declared, 83 | or if call is expected not to happen. 84 | 85 | :return: Whether or not the double is satisfied. 86 | :rtype: bool 87 | """ 88 | 89 | return self._call_counter.has_correct_call_count() and ( 90 | self._call_counter.never() or self._is_satisfied) 91 | -------------------------------------------------------------------------------- /doubles/instance_double.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass 2 | 3 | from doubles.exceptions import VerifyingDoubleImportError 4 | from doubles.object_double import ObjectDouble 5 | from doubles.utils import get_module, get_path_components 6 | 7 | 8 | def _get_doubles_target(module, class_name, path): 9 | """Validate and return the class to be doubled. 10 | 11 | :param module module: The module that contains the class that will be doubled. 12 | :param str class_name: The name of the class that will be doubled. 13 | :param str path: The full path to the class that will be doubled. 14 | :return: The class that will be doubled. 15 | :rtype: type 16 | :raise: ``VerifyingDoubleImportError`` if the target object doesn't exist or isn't a class. 17 | """ 18 | 19 | try: 20 | doubles_target = getattr(module, class_name) 21 | if isinstance(doubles_target, ObjectDouble): 22 | return doubles_target._doubles_target 23 | 24 | if not isclass(doubles_target): 25 | raise VerifyingDoubleImportError( 26 | 'Path does not point to a class: {}.'.format(path) 27 | ) 28 | 29 | return doubles_target 30 | except AttributeError: 31 | raise VerifyingDoubleImportError( 32 | 'No object at path: {}.'.format(path) 33 | ) 34 | 35 | 36 | class InstanceDouble(ObjectDouble): 37 | """A pure double representing an instance of the target class. 38 | 39 | Any kwargs supplied will be set as attributes on the instance that is 40 | created. 41 | 42 | :: 43 | 44 | user = InstanceDouble('myapp.User', name='Bob Barker') 45 | 46 | :param str path: The absolute module path to the class. 47 | """ 48 | 49 | def __init__(self, path, **kwargs): 50 | module_path, class_name = get_path_components(path) 51 | module = get_module(module_path, path) 52 | self._doubles_target = _get_doubles_target(module, class_name, path) 53 | for k, v in kwargs.items(): 54 | setattr(self, k, v) 55 | -------------------------------------------------------------------------------- /doubles/lifecycle.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from threading import local 3 | 4 | from doubles.space import Space 5 | 6 | _thread_local_data = local() 7 | 8 | 9 | def current_space(): 10 | """An accessor for the current thread's active ``Space``. 11 | 12 | :return: The active ``Space``. 13 | :rtype: Space 14 | """ 15 | 16 | if not hasattr(_thread_local_data, 'current_space'): 17 | _thread_local_data.current_space = Space() 18 | 19 | return _thread_local_data.current_space 20 | 21 | 22 | def teardown(): 23 | """Tears down the current Doubles environment. Must be called after each test case.""" 24 | if hasattr(_thread_local_data, 'current_space'): 25 | _thread_local_data.current_space.teardown() 26 | del _thread_local_data.current_space 27 | 28 | 29 | def clear(*objects_to_clear): 30 | """Clears allowances/expectations on objects 31 | 32 | :param object objects_to_clear: The objects to remove allowances and 33 | expectations from. 34 | """ 35 | if not hasattr(_thread_local_data, 'current_space'): 36 | return 37 | 38 | space = current_space() 39 | for obj in objects_to_clear: 40 | space.clear(obj) 41 | 42 | 43 | def verify(): 44 | """Verify a mock 45 | 46 | Verifies any mocks that have been created during the test run. Must be called after each 47 | test case, but before teardown. 48 | """ 49 | 50 | if hasattr(_thread_local_data, 'current_space'): 51 | _thread_local_data.current_space.verify() 52 | 53 | 54 | @contextmanager 55 | def no_builtin_verification(): 56 | """Turn off validation for builtin methods 57 | 58 | While inside this context we will ignore errors raised while verifying the 59 | arguments of builtins. 60 | 61 | Note: It is impossible to verify the expected arguments of built in functions 62 | """ 63 | current_space().skip_builtin_verification = True 64 | yield 65 | current_space().skip_builtin_verification = False 66 | 67 | 68 | def ignore_builtin_verification(): 69 | """Check if we ignoring builtin argument verification errors. 70 | 71 | :return: True if we are ignoring errors. 72 | :rtype: bool 73 | """ 74 | return not current_space().skip_builtin_verification 75 | -------------------------------------------------------------------------------- /doubles/method_double.py: -------------------------------------------------------------------------------- 1 | from doubles.allowance import Allowance 2 | from doubles.expectation import Expectation 3 | from doubles.proxy_method import ProxyMethod 4 | from doubles.verification import verify_method 5 | 6 | 7 | class MethodDouble(object): 8 | """A double of an individual method.""" 9 | 10 | def __init__(self, method_name, target): 11 | """ 12 | :param str method_name: The name of the method to double. 13 | :param Target target: A ``Target`` object containing the object with the method to double. 14 | """ 15 | 16 | self._method_name = method_name 17 | self._target = target 18 | 19 | self._verify_method() 20 | 21 | self._allowances = [] 22 | self._expectations = [] 23 | 24 | self._proxy_method = ProxyMethod( 25 | target, 26 | method_name, 27 | lambda args, kwargs: self._find_matching_double(args, kwargs) 28 | ) 29 | 30 | def add_allowance(self, caller): 31 | """Adds a new allowance for the method. 32 | 33 | :param: tuple caller: A tuple indicating where the method was called 34 | :return: The new ``Allowance``. 35 | :rtype: Allowance 36 | """ 37 | 38 | allowance = Allowance(self._target, self._method_name, caller) 39 | self._allowances.insert(0, allowance) 40 | return allowance 41 | 42 | def add_expectation(self, caller): 43 | """Adds a new expectation for the method. 44 | 45 | :return: The new ``Expectation``. 46 | :rtype: Expectation 47 | """ 48 | 49 | expectation = Expectation(self._target, self._method_name, caller) 50 | self._expectations.insert(0, expectation) 51 | return expectation 52 | 53 | def restore_original_method(self): 54 | """Removes the proxy method on the target and replaces it with its original value.""" 55 | 56 | self._proxy_method.restore_original_method() 57 | 58 | def verify(self): 59 | """Verifies all expectations on the method. 60 | 61 | :raise: ``MockExpectationError`` on the first expectation that is not satisfied, if any. 62 | """ 63 | 64 | for expectation in self._expectations: 65 | if not expectation.is_satisfied(): 66 | expectation.raise_failure_exception() 67 | 68 | def _find_matching_allowance(self, args, kwargs): 69 | """Return a matching allowance. 70 | 71 | Returns the first allowance that matches the ones declared. Tries one with specific 72 | arguments first, then falls back to an allowance that allows arbitrary arguments. 73 | 74 | :return: The matching ``Allowance``, if one was found. 75 | :rtype: Allowance, None 76 | """ 77 | 78 | for allowance in self._allowances: 79 | if allowance.satisfy_exact_match(args, kwargs): 80 | return allowance 81 | 82 | for allowance in self._allowances: 83 | if allowance.satisfy_custom_matcher(args, kwargs): 84 | return allowance 85 | 86 | for allowance in self._allowances: 87 | if allowance.satisfy_any_args_match(): 88 | return allowance 89 | 90 | def _find_matching_double(self, args, kwargs): 91 | """Returns the first matching expectation or allowance. 92 | 93 | Returns the first allowance or expectation that matches the ones declared. Tries one 94 | with specific arguments first, then falls back to an expectation that allows arbitrary 95 | arguments. 96 | 97 | :return: The matching ``Allowance`` or ``Expectation``, if one was found. 98 | :rtype: Allowance, Expectation, None 99 | """ 100 | 101 | expectation = self._find_matching_expectation(args, kwargs) 102 | 103 | if expectation: 104 | return expectation 105 | 106 | allowance = self._find_matching_allowance(args, kwargs) 107 | 108 | if allowance: 109 | return allowance 110 | 111 | def _find_matching_expectation(self, args, kwargs): 112 | """Return a matching expectation. 113 | 114 | Returns the first expectation that matches the ones declared. Tries one with specific 115 | arguments first, then falls back to an expectation that allows arbitrary arguments. 116 | 117 | :return: The matching ``Expectation``, if one was found. 118 | :rtype: Expectation, None 119 | """ 120 | 121 | for expectation in self._expectations: 122 | if expectation.satisfy_exact_match(args, kwargs): 123 | return expectation 124 | 125 | for expectation in self._expectations: 126 | if expectation.satisfy_custom_matcher(args, kwargs): 127 | return expectation 128 | 129 | for expectation in self._expectations: 130 | if expectation.satisfy_any_args_match(): 131 | return expectation 132 | 133 | def _verify_method(self): 134 | """Verify that a method may be doubled. 135 | 136 | Verifies that the target object has a method matching the name the user is attempting to 137 | double. 138 | 139 | :raise: ``VerifyingDoubleError`` if no matching method is found. 140 | """ 141 | 142 | class_level = self._target.is_class_or_module() 143 | 144 | verify_method(self._target, self._method_name, class_level=class_level) 145 | -------------------------------------------------------------------------------- /doubles/nose.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import sys 4 | 5 | from nose.plugins.base import Plugin 6 | 7 | from doubles.exceptions import MockExpectationError 8 | from doubles.lifecycle import verify, teardown 9 | 10 | 11 | class NoseIntegration(Plugin): 12 | name = 'doubles' 13 | 14 | def afterTest(self, test): 15 | teardown() 16 | 17 | def prepareTestCase(self, test): 18 | def wrapped(result): 19 | test.test(result) 20 | 21 | try: 22 | verify() 23 | except MockExpectationError: 24 | result.addFailure(test.test, sys.exc_info()) 25 | 26 | return wrapped 27 | -------------------------------------------------------------------------------- /doubles/object_double.py: -------------------------------------------------------------------------------- 1 | class ObjectDouble(object): 2 | """ 3 | A pure double representing the target object. 4 | 5 | :: 6 | 7 | dummy_user = ObjectDouble(user) 8 | 9 | :param object target: The object the newly created ObjectDouble will verify against. 10 | """ 11 | is_class = False 12 | 13 | def __init__(self, target): 14 | self._doubles_target = target 15 | 16 | def __repr__(self): 17 | """Provides a string representation of the double. 18 | 19 | NOTE: Includes the memory address and the name of the object being doubled. 20 | 21 | :return: A string representation of the double. 22 | :rtype: str 23 | """ 24 | 25 | address = hex(id(self)) 26 | class_name = self.__class__.__name__ 27 | 28 | return '<{} of {!r} object at {}>'.format( 29 | class_name, 30 | self._doubles_target, 31 | address 32 | ) 33 | -------------------------------------------------------------------------------- /doubles/patch.py: -------------------------------------------------------------------------------- 1 | from doubles.exceptions import VerifyingDoubleError 2 | from doubles.utils import get_module, get_path_components 3 | 4 | 5 | class Patch(object): 6 | """ 7 | A wrapper around an object that has been ``patched`` 8 | """ 9 | def __init__(self, target): 10 | """ 11 | :param str path: The absolute module path to the class. 12 | """ 13 | module_path, self._name = get_path_components(target) 14 | self.target = get_module(module_path, target) 15 | self._capture_original_object() 16 | self.set_value(None) 17 | 18 | def _capture_original_object(self): 19 | """Capture the original python object.""" 20 | try: 21 | self._doubles_target = getattr(self.target, self._name) 22 | except AttributeError: 23 | raise VerifyingDoubleError(self.target, self._name) 24 | 25 | def set_value(self, value): 26 | """Set the value of the target. 27 | 28 | :param obj value: The value to set. 29 | """ 30 | self._value = value 31 | setattr(self.target, self._name, value) 32 | 33 | def restore_original_object(self): 34 | """Restore the target to it's original value.""" 35 | self.set_value(self._doubles_target) 36 | -------------------------------------------------------------------------------- /doubles/proxy.py: -------------------------------------------------------------------------------- 1 | from doubles.method_double import MethodDouble 2 | from doubles.target import Target 3 | 4 | 5 | class Proxy(object): 6 | """ 7 | An intermediate object used to maintain a mapping between target objects and method doubles. 8 | """ 9 | 10 | def __init__(self, obj): 11 | """ 12 | :param object obj: The object that will be doubled. 13 | """ 14 | 15 | self._target = Target(obj) 16 | self._method_doubles = {} 17 | 18 | def add_allowance(self, method_name, caller): 19 | """Adds a new allowance for the given method name. 20 | 21 | :param str method_name: The name of the method to allow. 22 | :param tuple caller: A tuple indicating where the allowance was added 23 | :return: The new ``Allowance``. 24 | :rtype: Allowance 25 | """ 26 | 27 | return self.method_double_for(method_name).add_allowance(caller) 28 | 29 | def add_expectation(self, method_name, caller): 30 | """Adds a new expectation for the given method name. 31 | 32 | :param str method_name: The name of the method to expect. 33 | :return: The new ``Expectation``. 34 | :rtype: Expectation 35 | """ 36 | 37 | return self.method_double_for(method_name).add_expectation(caller) 38 | 39 | def restore_original_object(self): 40 | """Remove all stubs from an object. 41 | 42 | Removes the proxy methods applied to each method double and replaces them with the original 43 | values. 44 | """ 45 | 46 | for method_double in self._method_doubles.values(): 47 | method_double.restore_original_method() 48 | 49 | def verify(self): 50 | """Verifies all expectations on all method doubles. 51 | 52 | :raise: ``MockExpectationError`` on the first expectation that is not satisfied, if any. 53 | """ 54 | 55 | for method_double in self._method_doubles.values(): 56 | method_double.verify() 57 | 58 | def method_double_for(self, method_name): 59 | """Returns the method double for the provided method name, creating one if necessary. 60 | 61 | :param str method_name: The name of the method to retrieve a method double for. 62 | :return: The mapped ``MethodDouble``. 63 | :rtype: MethodDouble 64 | """ 65 | 66 | if method_name not in self._method_doubles: 67 | self._method_doubles[method_name] = MethodDouble(method_name, self._target) 68 | 69 | return self._method_doubles[method_name] 70 | -------------------------------------------------------------------------------- /doubles/proxy_method.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from functools import wraps 3 | from inspect import isbuiltin 4 | 5 | from doubles.allowance import build_argument_repr_string 6 | from doubles.exceptions import UnallowedMethodCallError 7 | from doubles.proxy_property import ProxyProperty 8 | 9 | 10 | def double_name(name): 11 | return 'double_of_' + name 12 | 13 | 14 | def _restore__new__(target, original_method): 15 | """Restore __new__ to original_method on the target 16 | 17 | Python 3 does some magic to verify no arguments are sent to __new__ if it 18 | is the builtin version, to work in python 3 we must handle this: 19 | 20 | 1) If original_method is the builtin version of __new__, wrap the 21 | builtin __new__ to ensure that no arguments are passed in. 22 | 23 | 2) If original_method is a custom method treat it the same as we would 24 | in python2 25 | 26 | :param class target: The class to restore __new__ on 27 | :param func original_method: The method to set __new__ to 28 | """ 29 | if isbuiltin(original_method): 30 | @wraps(original_method) 31 | def _new(cls, *args, **kwargs): 32 | return original_method(cls) 33 | 34 | target.__new__ = _new 35 | else: 36 | target.__new__ = original_method 37 | 38 | 39 | class ProxyMethod(object): 40 | """ 41 | The object that replaces the real value of a doubled method. Responsible for hijacking the 42 | target object, finding matching expectations and returning their values when called, and 43 | restoring the original value to the hijacked object during teardown. 44 | """ 45 | 46 | def __init__(self, target, method_name, find_expectation): 47 | """ 48 | :param Target target: The object to be hijacked. 49 | :param str method_name: The name of the method to replace. 50 | :param function find_expectation: A function to call to look for expectations that match 51 | any provided arguments. 52 | """ 53 | 54 | self._target = target 55 | self._method_name = method_name 56 | self._find_expectation = find_expectation 57 | self._attr = target.get_attr(method_name) 58 | 59 | self._capture_original_method() 60 | self._hijack_target() 61 | 62 | def __call__(self, *args, **kwargs): 63 | """The actual invocation of the doubled method. 64 | 65 | :param tuple args: The arguments the doubled method was called with. 66 | :param dict kwargs: The keyword arguments the doubled method was called with. 67 | :return: The return value the doubled method was declared to return. 68 | :rtype: object 69 | :raise: ``UnallowedMethodCallError`` if no matching doubles were found. 70 | """ 71 | 72 | expectation = self._find_expectation(args, kwargs) 73 | 74 | if not expectation: 75 | self._raise_exception(args, kwargs) 76 | 77 | expectation.verify_arguments(args, kwargs) 78 | 79 | return expectation.return_value(*args, **kwargs) 80 | 81 | def __get__(self, instance, owner): 82 | """Implements the descriptor protocol to allow doubled properties to behave as properties. 83 | 84 | :return: The return value of any matching double in the case of a property, self otherwise. 85 | :rtype: object, ProxyMethod 86 | """ 87 | 88 | if self._attr.kind == 'property': 89 | return self.__call__() 90 | 91 | return self 92 | 93 | @property 94 | def __name__(self): 95 | return self._original_method.__name__ 96 | 97 | @property 98 | def __doc__(self): 99 | return self._original_method.__doc__ 100 | 101 | @property 102 | def __wrapped__(self): 103 | return self._original_method 104 | 105 | def restore_original_method(self): 106 | """Replaces the proxy method on the target object with its original value.""" 107 | 108 | if self._target.is_class_or_module(): 109 | setattr(self._target.obj, self._method_name, self._original_method) 110 | if self._method_name == '__new__' and sys.version_info >= (3, 0): 111 | _restore__new__(self._target.obj, self._original_method) 112 | else: 113 | setattr(self._target.obj, self._method_name, self._original_method) 114 | elif self._attr.kind == 'property': 115 | setattr(self._target.obj.__class__, self._method_name, self._original_method) 116 | del self._target.obj.__dict__[double_name(self._method_name)] 117 | elif self._attr.kind == 'attribute': 118 | self._target.obj.__dict__[self._method_name] = self._original_method 119 | else: 120 | # TODO: Could there ever have been a value here that needs to be restored? 121 | del self._target.obj.__dict__[self._method_name] 122 | 123 | if self._method_name in ['__call__', '__enter__', '__exit__']: 124 | self._target.restore_attr(self._method_name) 125 | 126 | def _capture_original_method(self): 127 | """Saves a reference to the original value of the method to be doubled.""" 128 | 129 | self._original_method = self._attr.object 130 | 131 | def _hijack_target(self): 132 | """Replaces the target method on the target object with the proxy method.""" 133 | 134 | if self._target.is_class_or_module(): 135 | setattr(self._target.obj, self._method_name, self) 136 | elif self._attr.kind == 'property': 137 | proxy_property = ProxyProperty( 138 | double_name(self._method_name), 139 | self._original_method, 140 | ) 141 | setattr(self._target.obj.__class__, self._method_name, proxy_property) 142 | self._target.obj.__dict__[double_name(self._method_name)] = self 143 | else: 144 | self._target.obj.__dict__[self._method_name] = self 145 | 146 | if self._method_name in ['__call__', '__enter__', '__exit__']: 147 | self._target.hijack_attr(self._method_name) 148 | 149 | def _raise_exception(self, args, kwargs): 150 | """ Raises an ``UnallowedMethodCallError`` with a useful message. 151 | 152 | :raise: ``UnallowedMethodCallError`` 153 | """ 154 | 155 | error_message = ( 156 | "Received unexpected call to '{}' on {!r}. The supplied arguments " 157 | "{} do not match any available allowances." 158 | ) 159 | 160 | raise UnallowedMethodCallError( 161 | error_message.format( 162 | self._method_name, 163 | self._target.obj, 164 | build_argument_repr_string(args, kwargs) 165 | ) 166 | ) 167 | -------------------------------------------------------------------------------- /doubles/proxy_property.py: -------------------------------------------------------------------------------- 1 | class ProxyProperty(property): 2 | def __init__(self, name, original): 3 | """ 4 | :param str name: name of the doubled property 5 | :param property original: the original property 6 | """ 7 | self._name = name 8 | self._original = original 9 | 10 | def __get__(self, obj, objtype=None): 11 | if self._name in obj.__dict__: 12 | return obj.__dict__[self._name].__get__(obj, objtype) 13 | return self._original.__get__(obj, objtype) 14 | -------------------------------------------------------------------------------- /doubles/pytest_plugin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from doubles.lifecycle import teardown, verify 4 | 5 | 6 | @pytest.hookimpl(hookwrapper=True) 7 | def pytest_runtest_call(item): 8 | outcome = yield 9 | 10 | try: 11 | outcome.get_result() 12 | verify() 13 | finally: 14 | teardown() 15 | -------------------------------------------------------------------------------- /doubles/space.py: -------------------------------------------------------------------------------- 1 | from doubles.patch import Patch 2 | from doubles.proxy import Proxy 3 | 4 | 5 | class Space(object): 6 | """ 7 | A container object for all the doubles created during the execution of a test case. Maintains 8 | a one-to-one mapping of target objects and ``Proxy`` objects. Maintained by the ``lifecycle`` 9 | module and not intended to be used directly by other objects. 10 | """ 11 | 12 | def __init__(self): 13 | self._proxies = {} 14 | self._patches = {} 15 | self._is_verified = False 16 | self.skip_builtin_verification = False 17 | 18 | def patch_for(self, path): 19 | """Returns the ``Patch`` for the target path, creating it if necessary. 20 | 21 | :param str path: The absolute module path to the target. 22 | :return: The mapped ``Patch``. 23 | :rtype: Patch 24 | """ 25 | 26 | if path not in self._patches: 27 | self._patches[path] = Patch(path) 28 | 29 | return self._patches[path] 30 | 31 | def proxy_for(self, obj): 32 | """Returns the ``Proxy`` for the target object, creating it if necessary. 33 | 34 | :param object obj: The object that will be doubled. 35 | :return: The mapped ``Proxy``. 36 | :rtype: Proxy 37 | """ 38 | 39 | obj_id = id(obj) 40 | 41 | if obj_id not in self._proxies: 42 | self._proxies[obj_id] = Proxy(obj) 43 | 44 | return self._proxies[obj_id] 45 | 46 | def teardown(self): 47 | """Restores all doubled objects to their original state.""" 48 | 49 | for proxy in self._proxies.values(): 50 | proxy.restore_original_object() 51 | 52 | for patch in self._patches.values(): 53 | patch.restore_original_object() 54 | 55 | def clear(self, obj): 56 | """Clear allowances/expectations set on an object. 57 | 58 | :param object obj: The object to clear. 59 | """ 60 | self.proxy_for(obj).restore_original_object() 61 | del self._proxies[id(obj)] 62 | 63 | def verify(self): 64 | """Verifies expectations on all doubled objects. 65 | 66 | :raise: ``MockExpectationError`` on the first expectation that is not satisfied, if any. 67 | """ 68 | 69 | if self._is_verified: 70 | return 71 | 72 | for proxy in self._proxies.values(): 73 | proxy.verify() 74 | 75 | self._is_verified = True 76 | -------------------------------------------------------------------------------- /doubles/target.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from inspect import classify_class_attrs, isclass, ismodule, getmembers 3 | 4 | from doubles.object_double import ObjectDouble 5 | from doubles.verification import is_callable 6 | 7 | Attribute = namedtuple('Attribute', ['object', 'kind', 'defining_class']) 8 | 9 | 10 | def _proxy_class_method_to_instance(original, name): 11 | def func(instance, *args, **kwargs): 12 | if name in instance.__dict__: 13 | return instance.__dict__[name](*args, **kwargs) 14 | return original(instance, *args, **kwargs) 15 | 16 | func._doubles_target_method = original 17 | return func 18 | 19 | 20 | class Target(object): 21 | """ 22 | A wrapper around an object that owns methods to be doubled. Provides additional introspection 23 | such as the class and the kind (method, class, property, etc.) 24 | """ 25 | 26 | def __init__(self, obj): 27 | """ 28 | :param object obj: The real target object. 29 | """ 30 | 31 | self.obj = obj 32 | self.doubled_obj = self._determine_doubled_obj() 33 | self.doubled_obj_type = self._determine_doubled_obj_type() 34 | self.attrs = self._generate_attrs() 35 | 36 | def is_class_or_module(self): 37 | """Determines if the object is a class or a module 38 | 39 | :return: True if the object is a class or a module, False otherwise. 40 | :rtype: bool 41 | """ 42 | 43 | if isinstance(self.obj, ObjectDouble): 44 | return self.obj.is_class 45 | 46 | return isclass(self.doubled_obj) or ismodule(self.doubled_obj) 47 | 48 | def _determine_doubled_obj(self): 49 | """Return the target object. 50 | 51 | Returns the object that should be treated as the target object. For partial doubles, this 52 | will be the same as ``self.obj``, but for pure doubles, it's pulled from the special 53 | ``_doubles_target`` attribute. 54 | 55 | :return: The object to be doubled. 56 | :rtype: object 57 | """ 58 | 59 | if isinstance(self.obj, ObjectDouble): 60 | return self.obj._doubles_target 61 | else: 62 | return self.obj 63 | 64 | def _determine_doubled_obj_type(self): 65 | """Returns the type (class) of the target object. 66 | 67 | :return: The type (class) of the target. 68 | :rtype: type, classobj 69 | """ 70 | 71 | if isclass(self.doubled_obj) or ismodule(self.doubled_obj): 72 | return self.doubled_obj 73 | 74 | return self.doubled_obj.__class__ 75 | 76 | def _generate_attrs(self): 77 | """Get detailed info about target object. 78 | 79 | Uses ``inspect.classify_class_attrs`` to get several important details about each attribute 80 | on the target object. 81 | 82 | :return: The attribute details dict. 83 | :rtype: dict 84 | """ 85 | attrs = {} 86 | 87 | if ismodule(self.doubled_obj): 88 | for name, func in getmembers(self.doubled_obj, is_callable): 89 | attrs[name] = Attribute(func, 'toplevel', self.doubled_obj) 90 | else: 91 | for attr in classify_class_attrs(self.doubled_obj_type): 92 | attrs[attr.name] = attr 93 | 94 | return attrs 95 | 96 | def hijack_attr(self, attr_name): 97 | """Hijack an attribute on the target object. 98 | 99 | Updates the underlying class and delegating the call to the instance. 100 | This allows specially-handled attributes like __call__, __enter__, 101 | and __exit__ to be mocked on a per-instance basis. 102 | 103 | :param str attr_name: the name of the attribute to hijack 104 | """ 105 | if not self._original_attr(attr_name): 106 | setattr( 107 | self.obj.__class__, 108 | attr_name, 109 | _proxy_class_method_to_instance( 110 | getattr(self.obj.__class__, attr_name, None), attr_name 111 | ), 112 | ) 113 | 114 | def restore_attr(self, attr_name): 115 | """Restore an attribute back onto the target object. 116 | 117 | :param str attr_name: the name of the attribute to restore 118 | """ 119 | original_attr = self._original_attr(attr_name) 120 | if self._original_attr(attr_name): 121 | setattr(self.obj.__class__, attr_name, original_attr) 122 | 123 | def _original_attr(self, attr_name): 124 | """Return the original attribute off of the proxy on the target object. 125 | 126 | :param str attr_name: the name of the original attribute to return 127 | :return: Func or None. 128 | :rtype: func 129 | """ 130 | try: 131 | return getattr( 132 | getattr(self.obj.__class__, attr_name), '_doubles_target_method', None 133 | ) 134 | except AttributeError: 135 | return None 136 | 137 | def get_callable_attr(self, attr_name): 138 | """Used to double methods added to an object after creation 139 | 140 | :param str attr_name: the name of the original attribute to return 141 | :return: Attribute or None. 142 | :rtype: func 143 | """ 144 | 145 | if not hasattr(self.doubled_obj, attr_name): 146 | return None 147 | 148 | func = getattr(self.doubled_obj, attr_name) 149 | if not is_callable(func): 150 | return None 151 | 152 | attr = Attribute( 153 | func, 154 | 'attribute', 155 | self.doubled_obj if self.is_class_or_module() else self.doubled_obj_type, 156 | ) 157 | self.attrs[attr_name] = attr 158 | return attr 159 | 160 | def get_attr(self, method_name): 161 | """Get attribute from the target object""" 162 | return self.attrs.get(method_name) or self.get_callable_attr(method_name) 163 | -------------------------------------------------------------------------------- /doubles/targets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/doubles/15e68dcf98f709b19a581915fa6af5ef49ebdd8a/doubles/targets/__init__.py -------------------------------------------------------------------------------- /doubles/targets/allowance_target.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from doubles.class_double import ClassDouble 4 | from doubles.exceptions import ConstructorDoubleError 5 | from doubles.lifecycle import current_space 6 | 7 | 8 | def allow(target): 9 | """ 10 | Prepares a target object for a method call allowance (stub). The name of the method to allow 11 | should be called as a method on the return value of this function:: 12 | 13 | allow(foo).bar 14 | 15 | Accessing the ``bar`` attribute will return an ``Allowance`` which provides additional methods 16 | to configure the stub. 17 | 18 | :param object target: The object that will be stubbed. 19 | :return: An ``AllowanceTarget`` for the target object. 20 | """ 21 | 22 | return AllowanceTarget(target) 23 | 24 | 25 | def allow_constructor(target): 26 | """ 27 | Set an allowance on a ``ClassDouble`` constructor 28 | 29 | This allows the caller to control what a ClassDouble returns when a new instance is created. 30 | 31 | :param ClassDouble target: The ClassDouble to set the allowance on. 32 | :return: an ``Allowance`` for the __new__ method. 33 | :raise: ``ConstructorDoubleError`` if target is not a ClassDouble. 34 | """ 35 | if not isinstance(target, ClassDouble): 36 | raise ConstructorDoubleError( 37 | 'Cannot allow_constructor of {} since it is not a ClassDouble.'.format(target), 38 | ) 39 | 40 | return allow(target)._doubles__new__ 41 | 42 | 43 | class AllowanceTarget(object): 44 | """A wrapper around a target object that creates new allowances on attribute access.""" 45 | 46 | def __init__(self, target): 47 | """ 48 | :param object target: The object to wrap. 49 | """ 50 | 51 | self._proxy = current_space().proxy_for(target) 52 | 53 | def __getattribute__(self, attr_name): 54 | """ 55 | Returns the value of existing attributes, and returns a new allowance for any attribute 56 | that doesn't yet exist. 57 | 58 | :param str attr_name: The name of the attribute to look up. 59 | :return: The existing value or a new ``Allowance``. 60 | :rtype: object, Allowance 61 | """ 62 | 63 | __dict__ = object.__getattribute__(self, '__dict__') 64 | 65 | if __dict__ and attr_name in __dict__: 66 | return __dict__[attr_name] 67 | 68 | caller = inspect.getframeinfo(inspect.currentframe().f_back) 69 | return self._proxy.add_allowance(attr_name, caller) 70 | -------------------------------------------------------------------------------- /doubles/targets/expectation_target.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from doubles.class_double import ClassDouble 4 | from doubles.exceptions import ConstructorDoubleError 5 | from doubles.lifecycle import current_space 6 | 7 | 8 | def expect(target): 9 | """ 10 | Prepares a target object for a method call expectation (mock). The name of the method to expect 11 | should be called as a method on the return value of this function:: 12 | 13 | expect(foo).bar 14 | 15 | Accessing the ``bar`` attribute will return an ``Expectation`` which provides additional methods 16 | to configure the mock. 17 | 18 | :param object target: The object that will be mocked. 19 | :return: An ``ExpectationTarget`` for the target object. 20 | """ 21 | 22 | return ExpectationTarget(target) 23 | 24 | 25 | def expect_constructor(target): 26 | """ 27 | Set an expectation on a ``ClassDouble`` constructor 28 | 29 | :param ClassDouble target: The ClassDouble to set the expectation on. 30 | :return: an ``Expectation`` for the __new__ method. 31 | :raise: ``ConstructorDoubleError`` if target is not a ClassDouble. 32 | """ 33 | if not isinstance(target, ClassDouble): 34 | raise ConstructorDoubleError( 35 | 'Cannot allow_constructor of {} since it is not a ClassDouble.'.format(target), 36 | ) 37 | 38 | return expect(target)._doubles__new__ 39 | 40 | 41 | class ExpectationTarget(object): 42 | """A wrapper around a target object that creates new expectations on attribute access.""" 43 | 44 | def __init__(self, target): 45 | """ 46 | :param object target: The object to wrap. 47 | """ 48 | 49 | self._proxy = current_space().proxy_for(target) 50 | 51 | def __getattribute__(self, attr_name): 52 | """ 53 | Returns the value of existing attributes, and returns a new expectation for any attribute 54 | that doesn't yet exist. 55 | 56 | :param str attr_name: The name of the attribute to look up. 57 | :return: The existing value or a new ``Expectation``. 58 | :rtype: object, Expectation 59 | """ 60 | 61 | __dict__ = object.__getattribute__(self, '__dict__') 62 | 63 | if __dict__ and attr_name in __dict__: 64 | return __dict__[attr_name] 65 | 66 | caller = inspect.getframeinfo(inspect.currentframe().f_back) 67 | return self._proxy.add_expectation(attr_name, caller) 68 | -------------------------------------------------------------------------------- /doubles/targets/patch_target.py: -------------------------------------------------------------------------------- 1 | from doubles.class_double import ClassDouble 2 | from doubles.lifecycle import current_space 3 | 4 | 5 | def patch_class(target): 6 | """ 7 | Replace the specified class with a ClassDouble 8 | 9 | :param str target: A string pointing to the target to patch. 10 | :param obj values: Values to return when new instances are created. 11 | :return: A ``ClassDouble`` object. 12 | """ 13 | class_double = ClassDouble(target) 14 | patch(target, class_double) 15 | return class_double 16 | 17 | 18 | def patch(target, value): 19 | """ 20 | Replace the specified object 21 | 22 | :param str target: A string pointing to the target to patch. 23 | :param object value: The value to replace the target with. 24 | :return: A ``Patch`` object. 25 | """ 26 | patch = current_space().patch_for(target) 27 | patch.set_value(value) 28 | return patch 29 | -------------------------------------------------------------------------------- /doubles/testing.py: -------------------------------------------------------------------------------- 1 | class OldStyleEmptyClass(): 2 | pass 3 | 4 | 5 | class EmptyClass(OldStyleEmptyClass, object): 6 | pass 7 | 8 | 9 | class ArbitraryCallable(object): 10 | def __init__(self, value): 11 | self.value = value 12 | 13 | def __call__(self): 14 | return self.value 15 | 16 | 17 | class OldStyleUser(): 18 | """An importable dummy class used for testing purposes.""" 19 | 20 | class_attribute = 'foo' 21 | callable_class_attribute = classmethod(lambda cls: 'dummy result') 22 | arbitrary_callable = ArbitraryCallable('ArbitraryCallable Value') 23 | 24 | def __init__(self, name, age): 25 | self.name = name 26 | self.age = age 27 | self.callable_instance_attribute = lambda: 'dummy result' 28 | 29 | @staticmethod 30 | def static_method(arg): 31 | return 'static_method return value: {}'.format(arg) 32 | 33 | @classmethod 34 | def class_method(cls, arg): 35 | return 'class_method return value: {}'.format(arg) 36 | 37 | def get_name(self): 38 | return self.name 39 | 40 | def instance_method(self): 41 | return 'instance_method return value' 42 | 43 | def method_with_varargs(self, *args): 44 | return 'method_with_varargs return value' 45 | 46 | def method_with_default_args(self, foo, bar='baz'): 47 | return 'method_with_default_args return value' 48 | 49 | def method_with_varkwargs(self, **kwargs): 50 | return 'method_with_varkwargs return value' 51 | 52 | def method_with_positional_arguments(self, foo): 53 | return 'method_with_positional_arguments return value' 54 | 55 | def method_with_doc(self): 56 | """A basic method of OldStyleUser to illustrate existence of a docstring""" 57 | return 58 | 59 | @property 60 | def some_property(self): 61 | return 'some_property return value' 62 | 63 | def __call__(self, *args): 64 | return 'user was called' 65 | 66 | def __enter__(self): 67 | return self 68 | 69 | def __exit__(self, exc_type, exc_value, traceback): 70 | pass 71 | 72 | 73 | class User(OldStyleUser, object): 74 | pass 75 | 76 | 77 | class UserWithCustomNew(User): 78 | def __new__(cls, name, age): 79 | instance = User.__new__(cls) 80 | instance.name_set_in__new__ = name 81 | return instance 82 | 83 | 84 | def top_level_function(arg1, arg2='default'): 85 | return "{arg1} -- {arg2}".format( 86 | arg1=arg1, 87 | arg2=arg2 88 | ) 89 | 90 | 91 | def top_level_function_that_creates_an_instance(): 92 | return User('Bob Barker', 100), OldStyleUser('Bob Barker', 100) 93 | 94 | 95 | class ClassWithGetAttr(object): 96 | def __init__(self): 97 | self.attr = 'attr' 98 | 99 | def method(self): 100 | return 'method' 101 | 102 | def __getattr__(self, name): 103 | return 'attr {name}'.format(name=name) 104 | 105 | 106 | class Callable(object): 107 | def __call__(self, arg1): 108 | return arg1 109 | 110 | 111 | def return_callable(func): 112 | return Callable() 113 | 114 | 115 | def decorate_me(func): 116 | def decorated(arg1): 117 | return '{arg1} decorated'.format(arg1=arg1) 118 | 119 | return decorated 120 | 121 | 122 | @return_callable 123 | def decorated_function_callable(arg1): 124 | return arg1 125 | 126 | 127 | @decorate_me 128 | def decorated_function(arg1): 129 | return arg1 130 | 131 | callable_variable = Callable() 132 | 133 | class_method = User.class_method 134 | instance_method = User('Bob', 25).get_name 135 | 136 | 137 | class NeverEquals(object): 138 | def __eq__(self, other): 139 | return False 140 | 141 | 142 | class AlwaysEquals(object): 143 | def __eq__(self, other): 144 | return True 145 | 146 | 147 | class DictSubClass(dict): 148 | pass 149 | 150 | 151 | class ListSubClass(list): 152 | pass 153 | 154 | 155 | class SetSubClass(set): 156 | pass 157 | 158 | 159 | class TupleSubClass(tuple): 160 | pass 161 | -------------------------------------------------------------------------------- /doubles/unittest.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import unittest 4 | 5 | from doubles.lifecycle import teardown, verify 6 | 7 | 8 | def wrap_test(test_func): 9 | def wrapper(): 10 | test_func() 11 | verify() 12 | 13 | return wrapper 14 | 15 | 16 | class TestCase(unittest.TestCase): 17 | def __init__(self, methodName='runTest'): 18 | super(TestCase, self).__init__(methodName) 19 | 20 | setattr(self, self._testMethodName, wrap_test(getattr(self, self._testMethodName))) 21 | 22 | def setUp(self): 23 | self.addCleanup(teardown) 24 | -------------------------------------------------------------------------------- /doubles/utils.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from doubles.exceptions import VerifyingDoubleImportError 4 | 5 | 6 | def get_module(module_path, full_path): 7 | """Return the module given its path. 8 | 9 | :param str module_path: The path to the module to import. 10 | :param str full_path: The full path to the class that will be doubled. 11 | :return: The module object. 12 | :rtype: module 13 | :raise: ``VerifyingDoubleImportError`` if the module can't be imported. 14 | """ 15 | 16 | try: 17 | return import_module(module_path) 18 | except ImportError: 19 | raise VerifyingDoubleImportError('Cannot import object from path: {}.'.format(full_path)) 20 | 21 | 22 | def get_path_components(path): 23 | """Extract the module name and class name out of the fully qualified path to the class. 24 | 25 | :param str path: The full path to the class. 26 | :return: The module path and the class name. 27 | :rtype: str, str 28 | :raise: ``VerifyingDoubleImportError`` if the path is to a top-level module. 29 | """ 30 | 31 | path_segments = path.split('.') 32 | module_path = '.'.join(path_segments[:-1]) 33 | 34 | if module_path == '': 35 | raise VerifyingDoubleImportError('Invalid import path: {}.'.format(path)) 36 | 37 | class_name = path_segments[-1] 38 | 39 | return module_path, class_name 40 | -------------------------------------------------------------------------------- /doubles/verification.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from inspect import isbuiltin, getcallargs, isfunction, ismethod 3 | 4 | from doubles.exceptions import ( 5 | VerifyingDoubleArgumentError, 6 | VerifyingBuiltinDoubleArgumentError, 7 | VerifyingDoubleError, 8 | ) 9 | 10 | ACCEPTS_ARGS = (list, tuple, set) 11 | ACCEPTS_KWARGS = (dict,) 12 | 13 | 14 | if sys.version_info >= (3, 0): 15 | def _get_func_object(func): 16 | return func.__func__ 17 | else: 18 | def _get_func_object(func): 19 | return func.im_func 20 | 21 | 22 | def _is_python_function(func): 23 | if ismethod(func): 24 | func = _get_func_object(func) 25 | return isfunction(func) 26 | 27 | 28 | def is_callable(obj): 29 | if isfunction(obj): 30 | return True 31 | return hasattr(obj, '__call__') 32 | 33 | 34 | def _is_python_33(): 35 | v = sys.version_info 36 | return v[0] == 3 and v[1] == 3 37 | 38 | 39 | def _verify_arguments_of_doubles__new__(target, args, kwargs): 40 | """Verify arg/kwargs against a class's __init__ 41 | 42 | :param class target: The class to verify against. 43 | :param tuple args: Positional arguments. 44 | :params dict kwargs: Keyword arguments. 45 | """ 46 | if not _is_python_function(target.doubled_obj.__init__): 47 | class_ = target.doubled_obj 48 | if args and not kwargs and issubclass(class_, ACCEPTS_ARGS): 49 | return True 50 | elif kwargs and not args and issubclass(class_, ACCEPTS_KWARGS): 51 | return True 52 | elif args or kwargs: 53 | given_args_count = 1 + len(args) + len(kwargs) 54 | raise VerifyingDoubleArgumentError( 55 | '__init__() takes exactly 1 arguments ({} given)'.format( 56 | given_args_count, 57 | ) 58 | ) 59 | return 60 | 61 | return _verify_arguments( 62 | target.doubled_obj.__init__, 63 | '__init__', 64 | ['self'] + list(args), 65 | kwargs, 66 | ) 67 | 68 | 69 | def _raise_doubles_error_from_index_error(method_name): 70 | # Work Around for http://bugs.python.org/issue20817 71 | raise VerifyingDoubleArgumentError( 72 | "{method}() missing 3 or more arguments.".format(method=method_name) 73 | ) 74 | 75 | 76 | def verify_method(target, method_name, class_level=False): 77 | """Verifies that the provided method exists on the target object. 78 | 79 | :param Target target: A ``Target`` object containing the object with the method to double. 80 | :param str method_name: The name of the method to double. 81 | :raise: ``VerifyingDoubleError`` if the attribute doesn't exist, if it's not a callable object, 82 | and in the case where the target is a class, that the attribute isn't an instance method. 83 | """ 84 | 85 | attr = target.get_attr(method_name) 86 | 87 | if not attr: 88 | raise VerifyingDoubleError(method_name, target.doubled_obj).no_matching_method() 89 | 90 | if attr.kind == 'data' and not isbuiltin(attr.object) and not is_callable(attr.object): 91 | raise VerifyingDoubleError(method_name, target.doubled_obj).not_callable() 92 | 93 | if class_level and attr.kind == 'method' and method_name != '__new__': 94 | raise VerifyingDoubleError(method_name, target.doubled_obj).requires_instance() 95 | 96 | 97 | def verify_arguments(target, method_name, args, kwargs): 98 | """Verifies that the provided arguments match the signature of the provided method. 99 | 100 | :param Target target: A ``Target`` object containing the object with the method to double. 101 | :param str method_name: The name of the method to double. 102 | :param tuple args: The positional arguments the method should be called with. 103 | :param dict kwargs: The keyword arguments the method should be called with. 104 | :raise: ``VerifyingDoubleError`` if the provided arguments do not match the signature. 105 | """ 106 | 107 | if method_name == '_doubles__new__': 108 | return _verify_arguments_of_doubles__new__(target, args, kwargs) 109 | 110 | attr = target.get_attr(method_name) 111 | method = attr.object 112 | 113 | if attr.kind in ('data', 'attribute', 'toplevel', 'class method', 'static method'): 114 | try: 115 | method = method.__get__(None, attr.defining_class) 116 | except AttributeError: 117 | method = method.__call__ 118 | elif attr.kind == 'property': 119 | if args or kwargs: 120 | raise VerifyingDoubleArgumentError("Properties do not accept arguments.") 121 | return 122 | else: 123 | args = ['self_or_cls'] + list(args) 124 | 125 | _verify_arguments(method, method_name, args, kwargs) 126 | 127 | 128 | def _verify_arguments(method, method_name, args, kwargs): 129 | try: 130 | getcallargs(method, *args, **kwargs) 131 | except TypeError as e: 132 | if not _is_python_function(method): 133 | raise VerifyingBuiltinDoubleArgumentError(str(e)) 134 | raise VerifyingDoubleArgumentError(str(e)) 135 | except IndexError as e: 136 | if _is_python_33(): 137 | _raise_doubles_error_from_index_error(method_name) 138 | else: 139 | raise e 140 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Code coverage 2 | coverage==3.7.1 3 | coveralls==0.4.2 4 | # Linting 5 | flake8==2.1.0 6 | # Validate package 7 | pyroma==1.6 8 | # Test runner 9 | pytest==3.5.0 10 | nose==1.3.3 11 | # Documentation 12 | Sphinx==1.2.2 13 | sphinx-rtd-theme==0.1.6 14 | futures==3.0.3 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | max-complexity = 10 4 | 5 | [tool:pytest] 6 | addopts = --tb native --assert plain 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | from setuptools import setup 5 | from setuptools.command.test import test as TestCommand 6 | 7 | 8 | with open('README.rst') as f: 9 | long_description = f.read() 10 | 11 | with open('doubles/__init__.py') as f: 12 | version = re.search(r'__version__ = \'(.*?)\'', f.read()).group(1) 13 | 14 | 15 | class PyTest(TestCommand): 16 | def finalize_options(self): 17 | TestCommand.finalize_options(self) 18 | self.test_args = [] 19 | self.test_suite = True 20 | 21 | def run_tests(self): 22 | import pytest 23 | errcode = pytest.main(self.test_args) 24 | sys.exit(errcode) 25 | 26 | setup( 27 | name='doubles', 28 | version=version, 29 | description='Test doubles for Python.', 30 | long_description=long_description, 31 | author='Jimmy Cuadra', 32 | author_email='jimmy@uber.com', 33 | url='https://github.com/uber/doubles', 34 | license='MIT', 35 | packages=['doubles', 'doubles.targets'], 36 | install_requires=['six'], 37 | tests_require=['pytest'], 38 | cmdclass={'test': PyTest}, 39 | entry_points = { 40 | 'pytest11': ['doubles = doubles.pytest_plugin'], 41 | 'nose.plugins.0.10': ['doubles = doubles.nose:NoseIntegration'], 42 | }, 43 | zip_safe=True, 44 | keywords=['testing', 'test doubles', 'mocks', 'mocking', 'stubs', 'stubbing'], 45 | classifiers=[ 46 | 'Development Status :: 1 - Planning', 47 | 'Programming Language :: Python', 48 | 'Programming Language :: Python :: 2.7', 49 | 'Programming Language :: Python :: 3.4', 50 | 'Programming Language :: Python :: 3.5', 51 | 'Programming Language :: Python :: 3.6', 52 | 'Programming Language :: Python :: 3.7', 53 | 'Intended Audience :: Developers', 54 | 'License :: OSI Approved :: MIT License', 55 | 'Topic :: Software Development :: Testing', 56 | ] 57 | ) 58 | -------------------------------------------------------------------------------- /test/allow_test.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import re 3 | import sys 4 | 5 | import pytest 6 | from pytest import raises 7 | 8 | from doubles.exceptions import ( 9 | UnallowedMethodCallError, 10 | MockExpectationError, 11 | VerifyingDoubleArgumentError 12 | ) 13 | from doubles.instance_double import InstanceDouble 14 | from doubles import allow, no_builtin_verification 15 | from doubles.lifecycle import teardown 16 | from doubles.testing import AlwaysEquals, NeverEquals 17 | 18 | 19 | class UserDefinedException(Exception): 20 | pass 21 | 22 | 23 | class TestBasicAllowance(object): 24 | def test_allows_method_calls_on_doubles(self): 25 | subject = InstanceDouble('doubles.testing.User') 26 | 27 | allow(subject).instance_method 28 | 29 | assert subject.instance_method() is None 30 | 31 | def test_raises_on_undefined_attribute_access(self): 32 | subject = InstanceDouble('doubles.testing.User') 33 | 34 | with raises(AttributeError): 35 | subject.instance_method 36 | 37 | def test_raises_if_called_with_args_that_do_not_match_signature(self): 38 | subject = InstanceDouble('doubles.testing.User') 39 | allow(subject).instance_method 40 | 41 | with raises(VerifyingDoubleArgumentError): 42 | subject.instance_method('bar') 43 | 44 | def test_skip_builtin_verification_does_not_affect_non_builtins(self): 45 | with no_builtin_verification(): 46 | subject = InstanceDouble('doubles.testing.User') 47 | allow(subject).instance_method 48 | 49 | with raises(VerifyingDoubleArgumentError): 50 | subject.instance_method('bar') 51 | 52 | def test_objects_with_custom__eq__method_one(self): 53 | subject = InstanceDouble('doubles.testing.User') 54 | allow(subject).method_with_positional_arguments.with_args(NeverEquals()) 55 | 56 | subject.method_with_positional_arguments(AlwaysEquals()) 57 | 58 | def test_objects_with_custom__eq__method_two(self): 59 | subject = InstanceDouble('doubles.testing.User') 60 | allow(subject).method_with_positional_arguments.with_args(AlwaysEquals()) 61 | 62 | subject.method_with_positional_arguments(NeverEquals()) 63 | 64 | def test_proxies_docstring(self): 65 | subject = InstanceDouble('doubles.testing.User') 66 | 67 | allow(subject).method_with_doc 68 | 69 | assert subject.method_with_doc.__doc__ == ( 70 | """A basic method of OldStyleUser to illustrate existence of a docstring""" 71 | ) 72 | 73 | def test_proxies_name(self): 74 | subject = InstanceDouble('doubles.testing.User') 75 | 76 | allow(subject).method_with_doc 77 | 78 | assert subject.method_with_doc.__name__ == "method_with_doc" 79 | 80 | @pytest.mark.skipif(sys.version_info <= (3, 3), reason="requires Python 3.3 or higher") 81 | def test_proxies_can_be_inspected(self): 82 | subject = InstanceDouble("doubles.testing.User") 83 | allow(subject).instance_method 84 | parameters = inspect.signature(subject.instance_method).parameters 85 | assert len(parameters) == 1 86 | 87 | 88 | class TestWithArgs(object): 89 | def test__call__is_short_hand_for_with_args(self): 90 | subject = InstanceDouble('doubles.testing.User') 91 | 92 | allow(subject).method_with_positional_arguments('Bob').and_return('Barker') 93 | assert subject.method_with_positional_arguments('Bob') == 'Barker' 94 | 95 | def test_allows_any_arguments_if_none_are_specified(self): 96 | subject = InstanceDouble('doubles.testing.User') 97 | 98 | allow(subject).method_with_positional_arguments.and_return('bar') 99 | 100 | assert subject.method_with_positional_arguments('unspecified argument') == 'bar' 101 | 102 | def test_allows_specification_of_arguments(self): 103 | subject = InstanceDouble('doubles.testing.User') 104 | 105 | allow(subject).method_with_positional_arguments.with_args('foo') 106 | 107 | assert subject.method_with_positional_arguments('foo') is None 108 | 109 | def test_raises_if_arguments_were_specified_but_not_provided_when_called(self): 110 | subject = InstanceDouble('doubles.testing.User') 111 | 112 | allow(subject).method_with_default_args.with_args('one', bar='two') 113 | 114 | with raises(UnallowedMethodCallError) as e: 115 | subject.method_with_default_args() 116 | 117 | assert re.match( 118 | r"Received unexpected call to 'method_with_default_args' on " 119 | r" object at .+>\." 121 | r" The supplied arguments \(\)" 122 | r" do not match any available allowances.", 123 | str(e.value) 124 | ) 125 | 126 | def test_raises_if_arguments_were_specified_but_wrong_kwarg_used_when_called(self): 127 | subject = InstanceDouble('doubles.testing.User') 128 | 129 | allow(subject).method_with_default_args.with_args('one', bar='two') 130 | 131 | with raises(UnallowedMethodCallError) as e: 132 | subject.method_with_default_args('one', bob='barker') 133 | 134 | assert re.match( 135 | r"Received unexpected call to 'method_with_default_args' on " 136 | r" object at .+>\." 138 | r" The supplied arguments \('one', bob='barker'\)" 139 | r" do not match any available allowances.", 140 | str(e.value) 141 | ) 142 | 143 | def test_raises_if_method_is_called_with_wrong_arguments(self): 144 | subject = InstanceDouble('doubles.testing.User') 145 | 146 | allow(subject).method_with_varargs.with_args('bar') 147 | 148 | with raises(UnallowedMethodCallError) as e: 149 | subject.method_with_varargs('baz') 150 | 151 | assert re.match( 152 | r"Received unexpected call to 'method_with_varargs' on " 153 | r" object at .+>\." 155 | r" The supplied arguments \('baz'\)" 156 | r" do not match any available allowances.", 157 | str(e.value) 158 | ) 159 | 160 | def test_matches_most_specific_allowance(self): 161 | subject = InstanceDouble('doubles.testing.User') 162 | 163 | allow(subject).method_with_varargs.and_return('bar') 164 | allow(subject).method_with_varargs.with_args('baz').and_return('blah') 165 | 166 | assert subject.method_with_varargs('baz') == 'blah' 167 | 168 | 169 | class TestWithNoArgs(object): 170 | def test_allows_call_with_no_arguments(self): 171 | subject = InstanceDouble('doubles.testing.User') 172 | 173 | allow(subject).instance_method.with_no_args() 174 | 175 | assert subject.instance_method() is None 176 | 177 | def test_raises_if_called_with_args(self): 178 | subject = InstanceDouble('doubles.testing.User') 179 | 180 | allow(subject).instance_method.with_no_args() 181 | 182 | with raises(UnallowedMethodCallError) as e: 183 | subject.instance_method('bar') 184 | 185 | assert re.match( 186 | r"Received unexpected call to 'instance_method' on " 187 | r" object at .+>\." 189 | r" The supplied arguments \('bar'\)" 190 | r" do not match any available allowances.", 191 | str(e.value) 192 | ) 193 | 194 | def test_chains_with_return_values(self): 195 | subject = InstanceDouble('doubles.testing.User') 196 | 197 | allow(subject).instance_method.with_no_args().and_return('bar') 198 | 199 | assert subject.instance_method() == 'bar' 200 | 201 | 202 | class TestTwice(object): 203 | def test_passes_when_called_twice(self): 204 | subject = InstanceDouble('doubles.testing.User') 205 | 206 | allow(subject).instance_method.twice() 207 | 208 | subject.instance_method() 209 | subject.instance_method() 210 | 211 | def test_passes_when_called_once(self): 212 | subject = InstanceDouble('doubles.testing.User') 213 | 214 | allow(subject).instance_method.twice() 215 | 216 | subject.instance_method() 217 | 218 | def test_fails_when_called_three_times(self): 219 | subject = InstanceDouble('doubles.testing.User') 220 | 221 | allow(subject).instance_method.twice() 222 | 223 | subject.instance_method() 224 | subject.instance_method() 225 | with raises(MockExpectationError) as e: 226 | subject.instance_method() 227 | teardown() 228 | 229 | assert re.match( 230 | r"Allowed 'instance_method' to be called 2 times instead of 3 times on " 231 | r" object at .+> " 232 | r"with any args, but was not." 233 | r" \(.*doubles/test/allow_test.py:\d+\)", 234 | str(e.value) 235 | ) 236 | 237 | 238 | class TestOnce(object): 239 | def test_passes_when_called_once(self): 240 | subject = InstanceDouble('doubles.testing.User') 241 | 242 | allow(subject).instance_method.once() 243 | 244 | subject.instance_method() 245 | 246 | def test_fails_when_called_two_times(self): 247 | subject = InstanceDouble('doubles.testing.User') 248 | 249 | allow(subject).instance_method.once() 250 | 251 | subject.instance_method() 252 | with raises(MockExpectationError) as e: 253 | subject.instance_method() 254 | teardown() 255 | 256 | assert re.match( 257 | r"Allowed 'instance_method' to be called 1 time instead of 2 times on " 258 | r" object at .+> " 259 | r"with any args, but was not." 260 | r" \(.*doubles/test/allow_test.py:\d+\)", 261 | str(e.value) 262 | ) 263 | 264 | 265 | class TestZeroTimes(object): 266 | def test_passes_when_called_never(self): 267 | subject = InstanceDouble('doubles.testing.User') 268 | 269 | allow(subject).instance_method.never() 270 | 271 | def test_fails_when_called_once_times(self): 272 | subject = InstanceDouble('doubles.testing.User') 273 | 274 | allow(subject).instance_method.never() 275 | 276 | with raises(MockExpectationError) as e: 277 | subject.instance_method() 278 | teardown() 279 | 280 | assert re.match( 281 | r"Allowed 'instance_method' to be called 0 times instead of 1 " 282 | r"time on " 283 | r"object at .+> with any args, but was not." 284 | r" \(.*doubles/test/allow_test.py:\d+\)", 285 | str(e.value) 286 | ) 287 | 288 | 289 | class TestExactly(object): 290 | def test_raises_if_called_with_negative_value(self): 291 | subject = InstanceDouble('doubles.testing.User') 292 | 293 | with raises(TypeError) as e: 294 | allow(subject).instance_method.exactly(-1).times 295 | teardown() 296 | 297 | assert re.match( 298 | r"exactly requires one positive integer argument", 299 | str(e.value) 300 | ) 301 | 302 | def test_called_with_zero(self): 303 | subject = InstanceDouble('doubles.testing.User') 304 | 305 | allow(subject).instance_method.exactly(0).times 306 | 307 | with raises(MockExpectationError) as e: 308 | subject.instance_method() 309 | teardown() 310 | 311 | assert re.match( 312 | r"Allowed 'instance_method' to be called 0 times instead of 1 " 313 | r"time on " 314 | r"object at .+> with any args, but was not." 315 | r" \(.*doubles/test/allow_test.py:\d+\)", 316 | str(e.value) 317 | ) 318 | 319 | def test_calls_are_chainable(self): 320 | subject = InstanceDouble('doubles.testing.User') 321 | 322 | allow(subject).instance_method.exactly(1).time.exactly(2).times 323 | 324 | subject.instance_method() 325 | subject.instance_method() 326 | 327 | def test_passes_when_called_less_than_expected_times(self): 328 | subject = InstanceDouble('doubles.testing.User') 329 | 330 | allow(subject).instance_method.exactly(2).times 331 | 332 | subject.instance_method() 333 | 334 | def test_passes_when_called_exactly_expected_times(self): 335 | subject = InstanceDouble('doubles.testing.User') 336 | 337 | allow(subject).instance_method.exactly(1).times 338 | 339 | subject.instance_method() 340 | 341 | def test_fails_when_called_more_than_expected_times(self): 342 | subject = InstanceDouble('doubles.testing.User') 343 | 344 | allow(subject).instance_method.exactly(1).times 345 | 346 | subject.instance_method() 347 | with raises(MockExpectationError) as e: 348 | subject.instance_method() 349 | teardown() 350 | 351 | assert re.match( 352 | r"Allowed 'instance_method' to be called 1 time instead of 2 times on " 353 | r" object at .+> " 354 | r"with any args, but was not." 355 | r" \(.*doubles/test/allow_test.py:\d+\)", 356 | str(e.value) 357 | ) 358 | 359 | 360 | class TestAtLeast(object): 361 | def test_raises_if_called_with_negative_value(self): 362 | subject = InstanceDouble('doubles.testing.User') 363 | 364 | with raises(TypeError) as e: 365 | allow(subject).instance_method.at_least(-1).times 366 | teardown() 367 | 368 | assert re.match( 369 | r"at_least requires one positive integer argument", 370 | str(e.value) 371 | ) 372 | 373 | def test_if_called_with_zero(self): 374 | subject = InstanceDouble('doubles.testing.User') 375 | 376 | allow(subject).instance_method.at_least(0).times 377 | 378 | def test_calls_are_chainable(self): 379 | subject = InstanceDouble('doubles.testing.User') 380 | 381 | allow(subject).instance_method.at_least(2).times.at_least(1).times 382 | 383 | subject.instance_method() 384 | 385 | def test_passes_when_called_less_than_at_least_times(self): 386 | subject = InstanceDouble('doubles.testing.User') 387 | 388 | allow(subject).instance_method.at_least(2).times 389 | 390 | subject.instance_method() 391 | 392 | def test_passes_when_called_exactly_at_least_times(self): 393 | subject = InstanceDouble('doubles.testing.User') 394 | 395 | allow(subject).instance_method.at_least(1).times 396 | 397 | subject.instance_method() 398 | subject.instance_method() 399 | 400 | def test_passes_when_called_more_than_at_least_times(self): 401 | subject = InstanceDouble('doubles.testing.User') 402 | 403 | allow(subject).instance_method.at_least(1).times 404 | 405 | subject.instance_method() 406 | subject.instance_method() 407 | 408 | 409 | class TestAtMost(object): 410 | def test_raises_if_called_with_negative_value(self): 411 | subject = InstanceDouble('doubles.testing.User') 412 | 413 | with raises(TypeError) as e: 414 | allow(subject).instance_method.at_most(-1).times 415 | teardown() 416 | 417 | assert re.match( 418 | r"at_most requires one positive integer argument", 419 | str(e.value) 420 | ) 421 | 422 | def test_called_with_zero(self): 423 | subject = InstanceDouble('doubles.testing.User') 424 | 425 | allow(subject).instance_method.at_most(0).times 426 | 427 | def test_calls_are_chainable(self): 428 | subject = InstanceDouble('doubles.testing.User') 429 | 430 | allow(subject).instance_method.at_most(1).times.at_most(2).times 431 | 432 | subject.instance_method() 433 | subject.instance_method() 434 | 435 | def test_passes_when_called_exactly_at_most_times(self): 436 | subject = InstanceDouble('doubles.testing.User') 437 | 438 | allow(subject).instance_method.at_most(1).times 439 | 440 | subject.instance_method() 441 | 442 | def test_passes_when_called_less_than_at_most_times(self): 443 | subject = InstanceDouble('doubles.testing.User') 444 | 445 | allow(subject).instance_method.at_most(2).times 446 | 447 | subject.instance_method() 448 | 449 | def test_fails_when_called_more_than_at_most_times(self): 450 | subject = InstanceDouble('doubles.testing.User') 451 | 452 | allow(subject).instance_method.at_most(1).times 453 | 454 | subject.instance_method() 455 | with raises(MockExpectationError) as e: 456 | subject.instance_method() 457 | teardown() 458 | 459 | assert re.match( 460 | r"Allowed 'instance_method' to be called at most 1 time instead of 2 times on " 461 | r" object at .+> " 462 | r"with any args, but was not." 463 | r" \(.*doubles/test/allow_test.py:\d+\)", 464 | str(e.value) 465 | ) 466 | 467 | 468 | class TestCustomMatcher(object): 469 | def setup(self): 470 | self.subject = InstanceDouble('doubles.testing.User') 471 | 472 | def test_matcher_raises_an_exception(self): 473 | def func(): 474 | raise Exception('Bob Barker') 475 | 476 | allow(self.subject).instance_method.with_args_validator(func) 477 | with raises(UnallowedMethodCallError): 478 | self.subject.instance_method() 479 | 480 | def test_matcher_with_no_args_returns_true(self): 481 | allow(self.subject).instance_method.with_args_validator(lambda: True).and_return('Bob') 482 | self.subject.instance_method() == 'Bob' 483 | 484 | def test_matcher_with_no_args_returns_false(self): 485 | allow(self.subject).instance_method.with_args_validator(lambda: False) 486 | with raises(UnallowedMethodCallError): 487 | self.subject.instance_method() 488 | 489 | def test_matcher_with_positional_args_returns_true(self): 490 | (allow(self.subject) 491 | .method_with_positional_arguments 492 | .with_args_validator(lambda x: True) 493 | .and_return('Bob')) 494 | self.subject.method_with_positional_arguments('Bob Barker') == 'Bob' 495 | 496 | def test_matcher_with_positional_args_returns_false(self): 497 | allow(self.subject).method_with_positional_arguments.with_args_validator(lambda x: False) 498 | with raises(UnallowedMethodCallError): 499 | self.subject.method_with_positional_arguments('Bob Barker') 500 | 501 | def test_matcher_with_kwargs_args_returns_false(self): 502 | def func(bar=None): 503 | return False 504 | allow(self.subject).instance_method.with_args_validator(func) 505 | with raises(UnallowedMethodCallError): 506 | self.subject.instance_method() 507 | 508 | def test_matcher_with_kwargs_args_returns_true(self): 509 | def func(bar=None): 510 | return True 511 | allow(self.subject).instance_method.with_args_validator(func).and_return('Bob') 512 | self.subject.instance_method() == 'Bob' 513 | 514 | def test_matcher_with_positional_and_kwargs_returns_true(self): 515 | def func(foo, bar=None): 516 | return True 517 | allow(self.subject).method_with_default_args.with_args_validator(func).and_return('Bob') 518 | self.subject.method_with_default_args('bob', bar='Barker') == 'Bob' 519 | 520 | def test_matcher_with_positional_and_kwargs_returns_false(self): 521 | def func(foo, bar=None): 522 | return False 523 | allow(self.subject).method_with_default_args.with_args_validator(func).and_return('Bob') 524 | with raises(UnallowedMethodCallError): 525 | self.subject.method_with_default_args('bob', bar='Barker') 526 | 527 | def test_matcher_returns_true_but_args_do_not_match_call_signature(self): 528 | allow(self.subject).instance_method.with_args_validator(lambda x: True) 529 | with raises(VerifyingDoubleArgumentError): 530 | self.subject.instance_method('bob') 531 | 532 | 533 | class TestAsync(object): 534 | def setup(self): 535 | self.subject = InstanceDouble('doubles.testing.User') 536 | 537 | def test_and_return_future(self): 538 | allow(self.subject).instance_method.and_return_future('Bob Barker') 539 | 540 | result = self.subject.instance_method() 541 | assert result.result() == 'Bob Barker' 542 | 543 | def test_and_return_future_multiple_values(self): 544 | allow(self.subject).instance_method.and_return_future('Bob Barker', 'Drew Carey') 545 | 546 | result1 = self.subject.instance_method() 547 | result2 = self.subject.instance_method() 548 | assert result1.result() == 'Bob Barker' 549 | assert result2.result() == 'Drew Carey' 550 | 551 | def test_and_raise_future(self): 552 | exception = Exception('Bob Barker') 553 | allow(self.subject).instance_method.and_raise_future(exception) 554 | 555 | result = self.subject.instance_method() 556 | with raises(Exception) as e: 557 | result.result() 558 | 559 | assert e.value == exception 560 | -------------------------------------------------------------------------------- /test/class_double_test.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pytest import raises, mark 4 | 5 | from doubles import allow, allow_constructor, expect_constructor, ClassDouble 6 | from doubles.lifecycle import teardown, verify 7 | from doubles.exceptions import ( 8 | MockExpectationError, 9 | UnallowedMethodCallError, 10 | VerifyingDoubleError, 11 | VerifyingDoubleArgumentError, 12 | ConstructorDoubleError, 13 | ) 14 | from doubles.testing import ( 15 | User, 16 | OldStyleUser, 17 | EmptyClass, 18 | OldStyleEmptyClass, 19 | ) 20 | 21 | TEST_CLASSES = ( 22 | 'doubles.testing.User', 23 | 'doubles.testing.OldStyleUser', 24 | 'doubles.testing.EmptyClass', 25 | 'doubles.testing.OldStyleEmptyClass', 26 | ) 27 | 28 | VALID_ARGS = { 29 | 'doubles.testing.User': ('Bob', 100), 30 | 'doubles.testing.OldStyleUser': ('Bob', 100), 31 | 'doubles.testing.EmptyClass': tuple(), 32 | 'doubles.testing.OldStyleEmptyClass': tuple(), 33 | } 34 | 35 | 36 | class TestClassDouble(object): 37 | def test_allows_stubs_on_existing_class_methods(self): 38 | User = ClassDouble('doubles.testing.User') 39 | 40 | allow(User).class_method 41 | 42 | assert User.class_method('arg') is None 43 | 44 | def test_raises_when_stubbing_nonexistent_methods(self): 45 | User = ClassDouble('doubles.testing.User') 46 | 47 | with raises(VerifyingDoubleError): 48 | allow(User).non_existent_method 49 | 50 | def test_allows_stubs_on_existing_static_methods(self): 51 | User = ClassDouble('doubles.testing.User') 52 | 53 | allow(User).static_method 54 | 55 | assert User.static_method('arg') is None 56 | 57 | def test_raises_when_stubbing_noncallable_class_attributes(self): 58 | User = ClassDouble('doubles.testing.User') 59 | 60 | with raises(VerifyingDoubleError): 61 | allow(User).class_attribute 62 | 63 | def test_raises_when_argspec_does_not_match(self): 64 | User = ClassDouble('doubles.testing.User') 65 | 66 | with raises(VerifyingDoubleArgumentError): 67 | allow(User).class_method.with_no_args() 68 | 69 | def test_raises_when_stubbing_instance_methods(self): 70 | User = ClassDouble('doubles.testing.User') 71 | 72 | with raises(VerifyingDoubleError) as e: 73 | allow(User).instance_method 74 | 75 | assert re.search(r"not callable directly on", str(e)) 76 | 77 | 78 | class TestUsingStubbingConstructor(object): 79 | @mark.parametrize('test_class', TEST_CLASSES) 80 | class TestUserAndEmptyClass(object): 81 | """Run test against User and EmptyClass""" 82 | 83 | def test_allowing_with_args(self, test_class): 84 | TestClass = ClassDouble(test_class) 85 | 86 | allow_constructor(TestClass).with_args(*VALID_ARGS[test_class]).and_return('Bob Barker') 87 | 88 | assert TestClass(*VALID_ARGS[test_class]) == 'Bob Barker' 89 | 90 | def test_with_no_allowances(self, test_class): 91 | TestClass = ClassDouble(test_class) 92 | 93 | with raises(UnallowedMethodCallError): 94 | TestClass(*VALID_ARGS[test_class]) 95 | 96 | def test_unsatisfied_expectation(self, test_class): 97 | TestClass = ClassDouble(test_class) 98 | 99 | expect_constructor(TestClass) 100 | with raises(MockExpectationError): 101 | verify() 102 | teardown() 103 | 104 | def test_satisfied_exception(self, test_class): 105 | TestClass = ClassDouble(test_class) 106 | 107 | expect_constructor(TestClass) 108 | 109 | TestClass(*VALID_ARGS[test_class]) 110 | 111 | @mark.parametrize('test_class', TEST_CLASSES[0:2]) 112 | class TestUserOnly(object): 113 | """Test using the constructor on a class with a custom __init__""" 114 | 115 | def test_called_with_wrong_args(self, test_class): 116 | TestClass = ClassDouble(test_class) 117 | 118 | allow_constructor(TestClass).with_args(*VALID_ARGS[test_class]).and_return('Bob Barker') 119 | 120 | with raises(UnallowedMethodCallError): 121 | TestClass('Bob', 101) 122 | 123 | 124 | class TestStubbingConstructor(object): 125 | @mark.parametrize('test_class', TEST_CLASSES) 126 | class TestAllow(object): 127 | def test_with_no_args(self, test_class): 128 | TestClass = ClassDouble(test_class) 129 | 130 | allow_constructor(TestClass) 131 | assert TestClass(*VALID_ARGS[test_class]) is None 132 | 133 | def test_with_invalid_args(self, test_class): 134 | TestClass = ClassDouble(test_class) 135 | 136 | with raises(VerifyingDoubleArgumentError): 137 | allow_constructor(TestClass).with_args(10) 138 | 139 | def test_with_valid_args(self, test_class): 140 | TestClass = ClassDouble(test_class) 141 | 142 | allow_constructor(TestClass).with_args(*VALID_ARGS[test_class]).and_return('Bob') 143 | 144 | assert TestClass(*VALID_ARGS[test_class]) == 'Bob' 145 | 146 | @mark.parametrize('test_class', TEST_CLASSES) 147 | class TestExpect(object): 148 | def test_with_no_args(self, test_class): 149 | TestClass = ClassDouble(test_class) 150 | 151 | expect_constructor(TestClass) 152 | assert TestClass(*VALID_ARGS[test_class]) is None 153 | 154 | def test_with_invalid_args(self, test_class): 155 | TestClass = ClassDouble(test_class) 156 | 157 | with raises(VerifyingDoubleArgumentError): 158 | expect_constructor(TestClass).with_args(10) 159 | teardown() 160 | 161 | def test_with_valid_args(self, test_class): 162 | TestClass = ClassDouble(test_class) 163 | 164 | expect_constructor(TestClass).with_args(*VALID_ARGS[test_class]) 165 | 166 | assert TestClass(*VALID_ARGS[test_class]) is None 167 | 168 | 169 | @mark.parametrize('test_class', [User, OldStyleUser, EmptyClass, OldStyleEmptyClass]) 170 | class TestingStubbingNonClassDoubleConstructors(object): 171 | def test_raises_if_you_allow_constructor(self, test_class): 172 | with raises(ConstructorDoubleError): 173 | allow_constructor(test_class) 174 | 175 | def test_raises_if_you_expect_constructor(self, test_class): 176 | with raises(ConstructorDoubleError): 177 | expect_constructor(test_class) 178 | 179 | 180 | class TestStubbingConstructorOfBuiltinSubClass(object): 181 | @mark.parametrize('type_', ['Dict']) 182 | class TestAcceptsKwargs(object): 183 | def test_fails_with_positional_args(self, type_): 184 | double = ClassDouble('doubles.testing.{}SubClass'.format(type_)) 185 | with raises(VerifyingDoubleArgumentError): 186 | allow_constructor(double).with_args(1, 2) 187 | 188 | def test_fails_with_positional_args_and_kwargs(self, type_): 189 | double = ClassDouble('doubles.testing.{}SubClass'.format(type_)) 190 | with raises(VerifyingDoubleArgumentError): 191 | allow_constructor(double).with_args(1, 2, foo=1) 192 | 193 | def test_passes_with_kwargs(self, type_): 194 | double = ClassDouble('doubles.testing.{}SubClass'.format(type_)) 195 | allow_constructor(double).with_args(bob='Barker') 196 | 197 | @mark.parametrize('type_', ['List', 'Set', 'Tuple']) 198 | class TestAccpectArgs(object): 199 | def test_passes_with_positional_args(self, type_): 200 | double = ClassDouble('doubles.testing.{}SubClass'.format(type_)) 201 | allow_constructor(double).with_args(1, 2) 202 | 203 | def test_fails_with_kwargs(self, type_): 204 | double = ClassDouble('doubles.testing.{}SubClass'.format(type_)) 205 | with raises(VerifyingDoubleArgumentError): 206 | allow_constructor(double).with_args(bob='Barker') 207 | 208 | def test_fails_with_positional_args_and_kwargs(self, type_): 209 | double = ClassDouble('doubles.testing.{}SubClass'.format(type_)) 210 | with raises(VerifyingDoubleArgumentError): 211 | allow_constructor(double).with_args(1, 2, foo=1) 212 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from coverage import coverage 4 | 5 | cov = coverage(source=('doubles',)) 6 | cov.start() 7 | 8 | pytest_plugins = ['doubles.pytest_plugin'] 9 | 10 | 11 | def pytest_sessionfinish(session, exitstatus): 12 | cov.stop() 13 | cov.save() 14 | 15 | 16 | def pytest_terminal_summary(terminalreporter): 17 | print("\nCoverage report:\n") 18 | cov.report(show_missing=True, ignore_errors=True, file=terminalreporter._tw) 19 | cov.html_report() 20 | -------------------------------------------------------------------------------- /test/expect_test.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pytest import raises 4 | 5 | from doubles.exceptions import MockExpectationError 6 | from doubles.instance_double import InstanceDouble 7 | from doubles.lifecycle import verify, teardown 8 | from doubles.targets.allowance_target import allow 9 | from doubles.targets.expectation_target import expect 10 | 11 | 12 | class TestExpect(object): 13 | def test_with_args_validator_not_called(self): 14 | subject = InstanceDouble('doubles.testing.User') 15 | 16 | def arg_matcher(*args): 17 | return True 18 | expect(subject).method_with_varargs.with_args_validator(arg_matcher) 19 | with raises(MockExpectationError) as e: 20 | verify() 21 | teardown() 22 | 23 | assert re.match( 24 | r"Expected 'method_with_varargs' to be called on " 25 | r" object at .+> " 27 | r"with custom matcher: 'arg_matcher', but was not." 28 | r" \(.*doubles/test/expect_test.py:\d+\)", 29 | str(e.value) 30 | ) 31 | 32 | def test_with_args_validator(self): 33 | subject = InstanceDouble('doubles.testing.User') 34 | 35 | expect(subject).method_with_varargs.with_args_validator( 36 | lambda *args: args[0] == 'Bob Barker' 37 | ) 38 | subject.method_with_varargs('Bob Barker', 'Drew Carey') 39 | 40 | def test_raises_if_an_expected_method_call_without_args_is_not_made(self): 41 | subject = InstanceDouble('doubles.testing.User') 42 | 43 | expect(subject).instance_method 44 | 45 | with raises(MockExpectationError) as e: 46 | verify() 47 | teardown() 48 | 49 | assert re.match( 50 | r"Expected 'instance_method' to be called on " 51 | r" object at .+> " 53 | r"with any args, but was not." 54 | r" \(.*doubles/test/expect_test.py:\d+\)", 55 | str(e.value) 56 | ) 57 | 58 | def test_raises_if_an_expected_method_call_with_args_is_not_made(self): 59 | subject = InstanceDouble('doubles.testing.User') 60 | 61 | expect(subject).method_with_varargs.with_args('bar') 62 | 63 | with raises(MockExpectationError) as e: 64 | verify() 65 | teardown() 66 | 67 | assert re.match( 68 | r"Expected 'method_with_varargs' to be called on " 69 | r" object at .+> " 71 | r"with \('bar'\), but was not." 72 | r" \(.*doubles/test/expect_test.py:\d+\)", 73 | str(e.value) 74 | ) 75 | 76 | def test_raises_if_an_expected_method_call_with_default_args_is_not_made(self): 77 | subject = InstanceDouble('doubles.testing.User') 78 | 79 | expect(subject).method_with_default_args.with_args('bar', bar='barker') 80 | 81 | with raises(MockExpectationError) as e: 82 | verify() 83 | teardown() 84 | 85 | assert re.match( 86 | r"Expected 'method_with_default_args' to be called on " 87 | r" object at .+> " 89 | r"with \('bar', bar='barker'\), but was not." 90 | r" \(.*doubles/test/expect_test.py:\d+\)", 91 | str(e.value) 92 | ) 93 | 94 | def test_passes_if_an_expected_method_call_is_made(self): 95 | subject = InstanceDouble('doubles.testing.User') 96 | 97 | expect(subject).instance_method 98 | 99 | subject.instance_method() 100 | 101 | def test_passes_if_method_is_called_with_specified_arguments(self): 102 | subject = InstanceDouble('doubles.testing.User') 103 | 104 | expect(subject).method_with_default_args.with_args('one', bar='two') 105 | 106 | assert subject.method_with_default_args('one', bar='two') is None 107 | 108 | def test_takes_precendence_over_previous_allowance(self): 109 | subject = InstanceDouble('doubles.testing.User') 110 | 111 | allow(subject).instance_method.and_return('foo') 112 | expect(subject).instance_method 113 | 114 | assert subject.instance_method() is None 115 | 116 | def test_takes_precedence_over_subsequent_allowances(self): 117 | subject = InstanceDouble('doubles.testing.User') 118 | 119 | expect(subject).instance_method 120 | allow(subject).instance_method.and_return('foo') 121 | 122 | with raises(MockExpectationError): 123 | verify() 124 | 125 | teardown() 126 | 127 | 128 | class TestTwice(object): 129 | def test_passes_when_called_twice(self): 130 | subject = InstanceDouble('doubles.testing.User') 131 | 132 | expect(subject).instance_method.twice() 133 | 134 | subject.instance_method() 135 | subject.instance_method() 136 | 137 | def test_fails_when_called_once(self): 138 | subject = InstanceDouble('doubles.testing.User') 139 | 140 | expect(subject).instance_method.twice() 141 | 142 | subject.instance_method() 143 | with raises(MockExpectationError) as e: 144 | verify() 145 | teardown() 146 | 147 | assert re.match( 148 | r"Expected 'instance_method' to be called 2 times instead of 1 time on " 149 | r" object at .+> " 150 | r"with any args, but was not." 151 | r" \(.*doubles/test/expect_test.py:\d+\)", 152 | str(e.value) 153 | ) 154 | 155 | def test_fails_when_called_three_times(self): 156 | subject = InstanceDouble('doubles.testing.User') 157 | 158 | expect(subject).instance_method.twice() 159 | 160 | subject.instance_method() 161 | subject.instance_method() 162 | with raises(MockExpectationError) as e: 163 | subject.instance_method() 164 | teardown() 165 | 166 | assert re.match( 167 | r"Expected 'instance_method' to be called 2 times instead of 3 times on " 168 | r" object at .+> " 169 | r"with any args, but was not." 170 | r" \(.*doubles/test/expect_test.py:\d+\)", 171 | str(e.value) 172 | ) 173 | 174 | 175 | class TestOnce(object): 176 | def test_passes_when_called_once(self): 177 | subject = InstanceDouble('doubles.testing.User') 178 | 179 | expect(subject).instance_method.once() 180 | 181 | subject.instance_method() 182 | 183 | def test_fails_when_called_two_times(self): 184 | subject = InstanceDouble('doubles.testing.User') 185 | 186 | expect(subject).instance_method.once() 187 | 188 | subject.instance_method() 189 | with raises(MockExpectationError) as e: 190 | subject.instance_method() 191 | teardown() 192 | 193 | assert re.match( 194 | r"Expected 'instance_method' to be called 1 time instead of 2 times on " 195 | r" object at .+> " 196 | r"with any args, but was not." 197 | r" \(.*doubles/test/expect_test.py:\d+\)", 198 | str(e.value) 199 | ) 200 | 201 | 202 | class TestNever(object): 203 | def test_passes_when_called_never(self): 204 | subject = InstanceDouble('doubles.testing.User') 205 | 206 | expect(subject).instance_method.never() 207 | 208 | 209 | class TestExactly(object): 210 | def test_calls_are_chainable(self): 211 | subject = InstanceDouble('doubles.testing.User') 212 | 213 | expect(subject).instance_method.exactly(1).times.exactly(2).times 214 | 215 | subject.instance_method() 216 | subject.instance_method() 217 | 218 | def test_fails_when_called_less_than_expected_times(self): 219 | subject = InstanceDouble('doubles.testing.User') 220 | 221 | expect(subject).instance_method.exactly(2).times 222 | 223 | subject.instance_method() 224 | with raises(MockExpectationError) as e: 225 | verify() 226 | teardown() 227 | 228 | assert re.match( 229 | r"Expected 'instance_method' to be called 2 times instead of 1 time on " 230 | r" object at .+> " 231 | r"with any args, but was not." 232 | r" \(.*doubles/test/expect_test.py:\d+\)", 233 | str(e.value) 234 | ) 235 | 236 | def test_passes_when_called_exactly_expected_times(self): 237 | subject = InstanceDouble('doubles.testing.User') 238 | 239 | expect(subject).instance_method.exactly(1).times 240 | 241 | subject.instance_method() 242 | 243 | def test_fails_when_called_more_than_expected_times(self): 244 | subject = InstanceDouble('doubles.testing.User') 245 | 246 | expect(subject).instance_method.exactly(1).times 247 | 248 | subject.instance_method() 249 | with raises(MockExpectationError) as e: 250 | subject.instance_method() 251 | teardown() 252 | 253 | assert re.match( 254 | r"Expected 'instance_method' to be called 1 time instead of 2 times on " 255 | r" object at .+> " 256 | r"with any args, but was not." 257 | r" \(.*doubles/test/expect_test.py:\d+\)", 258 | str(e.value) 259 | ) 260 | 261 | 262 | class TestAtLeast(object): 263 | def test_calls_are_chainable(self): 264 | subject = InstanceDouble('doubles.testing.User') 265 | 266 | expect(subject).instance_method.at_least(2).times.at_least(1).times 267 | 268 | subject.instance_method() 269 | 270 | def test_fails_when_called_less_than_at_least_times(self): 271 | subject = InstanceDouble('doubles.testing.User') 272 | 273 | expect(subject).instance_method.at_least(2).times 274 | 275 | subject.instance_method() 276 | with raises(MockExpectationError) as e: 277 | verify() 278 | teardown() 279 | 280 | assert re.match( 281 | r"Expected 'instance_method' to be called at least 2 times instead of 1 time on " 282 | r" object at .+> " 283 | r"with any args, but was not." 284 | r" \(.*doubles/test/expect_test.py:\d+\)", 285 | str(e.value) 286 | ) 287 | 288 | def test_passes_when_called_exactly_at_least_times(self): 289 | subject = InstanceDouble('doubles.testing.User') 290 | 291 | expect(subject).instance_method.at_least(1).times 292 | 293 | subject.instance_method() 294 | subject.instance_method() 295 | 296 | def test_passes_when_called_more_than_at_least_times(self): 297 | subject = InstanceDouble('doubles.testing.User') 298 | 299 | expect(subject).instance_method.at_least(1).times 300 | 301 | subject.instance_method() 302 | subject.instance_method() 303 | 304 | 305 | class TestAtMost(object): 306 | def test_calls_are_chainable(self): 307 | subject = InstanceDouble('doubles.testing.User') 308 | 309 | expect(subject).instance_method.at_most(1).times.at_most(2).times 310 | 311 | subject.instance_method() 312 | subject.instance_method() 313 | 314 | def test_passes_when_called_exactly_at_most_times(self): 315 | subject = InstanceDouble('doubles.testing.User') 316 | 317 | expect(subject).instance_method.at_most(1).times 318 | 319 | subject.instance_method() 320 | 321 | def test_passes_when_called_less_than_at_most_times(self): 322 | subject = InstanceDouble('doubles.testing.User') 323 | 324 | expect(subject).instance_method.at_most(2).times 325 | 326 | subject.instance_method() 327 | 328 | def test_fails_when_called_more_than_at_most_times(self): 329 | subject = InstanceDouble('doubles.testing.User') 330 | 331 | expect(subject).instance_method.at_most(1).times 332 | 333 | subject.instance_method() 334 | with raises(MockExpectationError) as e: 335 | subject.instance_method() 336 | teardown() 337 | 338 | assert re.match( 339 | r"Expected 'instance_method' to be called at most 1 time instead of 2 times on " 340 | r" object at .+> " 341 | r"with any args, but was not." 342 | r" \(.*doubles/test/expect_test.py:\d+\)", 343 | str(e.value) 344 | ) 345 | 346 | 347 | class Test__call__(object): 348 | def test_satisfied_expectation(self): 349 | subject = InstanceDouble('doubles.testing.User') 350 | 351 | expect(subject).__call__.once() 352 | 353 | subject() 354 | 355 | def test_unsatisfied_expectation(self): 356 | subject = InstanceDouble('doubles.testing.User') 357 | 358 | expect(subject).__call__.once() 359 | 360 | with raises(MockExpectationError) as e: 361 | verify() 362 | teardown() 363 | 364 | assert re.match( 365 | r"Expected '__call__' to be called 1 time instead of 0 times on " 366 | r" object at .+> " 367 | r"with any args, but was not." 368 | r" \(.*doubles/test/expect_test.py:\d+\)", 369 | str(e.value) 370 | ) 371 | 372 | 373 | class Test__enter__(object): 374 | def test_satisfied_expectation(self): 375 | subject = InstanceDouble('doubles.testing.User') 376 | 377 | expect(subject).__enter__.once() 378 | allow(subject).__exit__ 379 | 380 | with subject: 381 | pass 382 | 383 | def test_unsatisfied_expectation(self): 384 | subject = InstanceDouble('doubles.testing.User') 385 | 386 | expect(subject).__enter__.once() 387 | 388 | with raises(MockExpectationError) as e: 389 | verify() 390 | teardown() 391 | 392 | assert re.match( 393 | r"Expected '__enter__' to be called 1 time instead of 0 times on " 394 | r" object at .+> " 395 | r"with any args, but was not." 396 | r" \(.*doubles/test/expect_test.py:\d+\)", 397 | str(e.value) 398 | ) 399 | 400 | 401 | class Test__exit__(object): 402 | def test_satisfied_expectation(self): 403 | subject = InstanceDouble('doubles.testing.User') 404 | 405 | allow(subject).__enter__ 406 | expect(subject).__exit__.once() 407 | 408 | with subject: 409 | pass 410 | 411 | def test_unsatisfied_expectation(self): 412 | subject = InstanceDouble('doubles.testing.User') 413 | 414 | expect(subject).__exit__.once() 415 | 416 | with raises(MockExpectationError) as e: 417 | verify() 418 | teardown() 419 | 420 | assert re.match( 421 | r"Expected '__exit__' to be called 1 time instead of 0 times on " 422 | r" object at .+> " 423 | r"with any args, but was not." 424 | r" \(.*doubles/test/expect_test.py:\d+\)", 425 | str(e.value) 426 | ) 427 | -------------------------------------------------------------------------------- /test/instance_double_test.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from doubles import allow, InstanceDouble, no_builtin_verification 4 | from doubles.exceptions import ( 5 | VerifyingDoubleArgumentError, 6 | VerifyingDoubleError, 7 | VerifyingDoubleImportError 8 | ) 9 | 10 | 11 | class TestInstanceDouble(object): 12 | def test_allows_stubs_on_existing_methods(self): 13 | with no_builtin_verification(): 14 | date = InstanceDouble('datetime.date') 15 | 16 | allow(date).ctime 17 | 18 | assert date.ctime() is None 19 | 20 | def test_raises_when_stubbing_nonexistent_methods(self): 21 | date = InstanceDouble('datetime.date') 22 | 23 | with raises(VerifyingDoubleError): 24 | allow(date).nonexistent_method 25 | 26 | def test_raises_when_stubbing_noncallable_attributes(self): 27 | date = InstanceDouble('datetime.date') 28 | 29 | with raises(VerifyingDoubleError): 30 | allow(date).year 31 | 32 | def test_raises_when_argspec_does_not_match(self): 33 | date = InstanceDouble('datetime.date') 34 | 35 | with raises(VerifyingDoubleArgumentError): 36 | allow(date).ctime.with_args('foo') 37 | 38 | def test_allows_stubs_on_existing_class_methods(self): 39 | with no_builtin_verification(): 40 | date = InstanceDouble('datetime.date') 41 | 42 | allow(date).today.with_args() 43 | 44 | assert date.today() is None 45 | 46 | def test_raises_when_target_is_a_top_level_module(self): 47 | with raises(VerifyingDoubleImportError) as e: 48 | InstanceDouble('foo') 49 | 50 | assert str(e.value) == 'Invalid import path: foo.' 51 | 52 | def test_raises_on_import_error(self): 53 | with raises(VerifyingDoubleImportError) as e: 54 | InstanceDouble('foo.bar') 55 | 56 | assert str(e.value) == 'Cannot import object from path: foo.bar.' 57 | 58 | def test_raises_when_importing_a_non_class_object(self): 59 | with raises(VerifyingDoubleImportError) as e: 60 | InstanceDouble('unittest.signals') 61 | 62 | assert str(e.value) == 'Path does not point to a class: unittest.signals.' 63 | 64 | def test_raises_when_class_does_not_exist_in_module(self): 65 | with raises(VerifyingDoubleImportError) as e: 66 | InstanceDouble('unittest.foo') 67 | 68 | assert str(e.value) == 'No object at path: unittest.foo.' 69 | 70 | def test_mocking__call__works(self): 71 | user = InstanceDouble('doubles.testing.User') 72 | allow(user).__call__.and_return('bob barker') 73 | assert user() == 'bob barker' 74 | 75 | def test_mocking__enter__and__exit__works(self): 76 | user = InstanceDouble('doubles.testing.User') 77 | allow(user).__enter__.and_return('bob barker') 78 | allow(user).__exit__ 79 | 80 | with user as u: 81 | assert u == 'bob barker' 82 | 83 | def test_passing_kwargs_assings_them_as_attrs(self): 84 | user = InstanceDouble('doubles.testing.User', name='Bob Barker') 85 | 86 | assert user.name == 'Bob Barker' 87 | 88 | def test_class_with__getattr__(self): 89 | test_obj = InstanceDouble('doubles.testing.ClassWithGetAttr') 90 | allow(test_obj).method.and_return('bob barker') 91 | assert test_obj.method() == 'bob barker' 92 | -------------------------------------------------------------------------------- /test/lifecycle_test.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | try: 3 | from Queue import Queue 4 | except ImportError: 5 | from queue import Queue 6 | 7 | from doubles import lifecycle, expect, allow, clear 8 | import doubles.testing 9 | 10 | 11 | class TestLifecycle(object): 12 | def test_stores_a_space_per_thread(self): 13 | queue = Queue() 14 | 15 | def push_thread_space_to_queue(queue): 16 | queue.put(lifecycle.current_space()) 17 | 18 | push_thread_space_to_queue(queue) 19 | 20 | thread = Thread(target=push_thread_space_to_queue, args=(queue,)) 21 | thread.start() 22 | thread.join() 23 | 24 | main_space = queue.get() 25 | thread_space = queue.get() 26 | 27 | assert main_space is not thread_space 28 | 29 | 30 | class TestClear(object): 31 | 32 | def test_does_not_raise_expectation_errors(self): 33 | expect(doubles.testing).top_level_function 34 | clear(doubles.testing) 35 | 36 | def test_new_allowances_can_be_set(self): 37 | (allow(doubles.testing). 38 | top_level_function. 39 | with_args('Bob Barker'). 40 | and_return('Drew Carey')) 41 | 42 | clear(doubles.testing) 43 | allow(doubles.testing).top_level_function.and_return('Bob Barker') 44 | 45 | assert doubles.testing.top_level_function('bar') == 'Bob Barker' 46 | 47 | def test_clearing_an_instance(self): 48 | user = doubles.testing.User('Bob Barker', 25) 49 | (allow(user). 50 | method_with_positional_arguments. 51 | with_args(25). 52 | and_return('The price is right')) 53 | 54 | clear(user) 55 | allow(user).method_with_positional_arguments.and_return('The price is wrong') 56 | 57 | assert user.method_with_positional_arguments(10) == 'The price is wrong' 58 | 59 | def test_calling_twice(self): 60 | expect(doubles.testing).top_level_function 61 | clear(doubles.testing) 62 | clear(doubles.testing) 63 | 64 | def test_calling_on_an_undoubled_object(self): 65 | clear(doubles.testing) 66 | 67 | result = doubles.testing.top_level_function('bob') 68 | assert result == 'bob -- default' 69 | -------------------------------------------------------------------------------- /test/nose_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from nose.plugins import PluginTester 4 | 5 | from doubles.nose import NoseIntegration 6 | from doubles.instance_double import InstanceDouble 7 | from doubles.targets.expectation_target import expect 8 | 9 | 10 | def test_nose_plugin(): 11 | class TestNosePlugin(PluginTester, unittest.TestCase): 12 | activate = '--with-doubles' 13 | plugins = [NoseIntegration()] 14 | 15 | def test_expect(self): 16 | assert 'MockExpectationError' in self.output 17 | assert 'FAILED (failures=1)' in self.output 18 | assert 'Ran 2 tests' in self.output 19 | 20 | def makeSuite(self): 21 | class TestCase(unittest.TestCase): 22 | def runTest(self): 23 | subject = InstanceDouble('doubles.testing.User') 24 | 25 | expect(subject).instance_method 26 | 27 | def test2(self): 28 | pass 29 | 30 | return [TestCase('runTest'), TestCase('test2')] 31 | 32 | result = unittest.TestResult() 33 | TestNosePlugin('test_expect')(result) 34 | assert result.wasSuccessful() 35 | -------------------------------------------------------------------------------- /test/object_double_test.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pytest import raises, mark 4 | 5 | from doubles.exceptions import VerifyingDoubleArgumentError, VerifyingDoubleError 6 | from doubles.object_double import ObjectDouble 7 | from doubles.targets.allowance_target import allow 8 | from doubles.testing import User, OldStyleUser 9 | 10 | user = User('Alice', 25) 11 | old_style_user = OldStyleUser('Alice', 25) 12 | 13 | 14 | @mark.parametrize('test_object', [user, old_style_user]) 15 | class TestRepr(object): 16 | def test_displays_correct_class_name(self, test_object): 17 | subject = ObjectDouble(test_object) 18 | 19 | assert re.match( 20 | r" object " 22 | r"at 0x[0-9a-f]+>", 23 | repr(subject) 24 | ) 25 | 26 | 27 | @mark.parametrize('test_object', [user, old_style_user]) 28 | class TestObjectDouble(object): 29 | def test_allows_stubs_on_existing_methods(self, test_object): 30 | doubled_user = ObjectDouble(test_object) 31 | 32 | allow(doubled_user).get_name.and_return('Bob') 33 | 34 | assert doubled_user.get_name() == 'Bob' 35 | 36 | def test_raises_when_stubbing_nonexistent_methods(self, test_object): 37 | doubled_user = ObjectDouble(test_object) 38 | 39 | with raises(VerifyingDoubleError) as e: 40 | allow(doubled_user).foo 41 | 42 | assert re.search(r"does not implement it", str(e)) 43 | 44 | def test_raises_when_stubbing_noncallable_attributes(self, test_object): 45 | doubled_user = ObjectDouble(test_object) 46 | 47 | with raises(VerifyingDoubleError) as e: 48 | allow(doubled_user).class_attribute 49 | 50 | assert re.search(r"not a callable attribute", str(e)) 51 | 52 | def test_raises_when_specifying_different_arity(self, test_object): 53 | doubled_user = ObjectDouble(test_object) 54 | 55 | with raises(VerifyingDoubleArgumentError): 56 | allow(doubled_user).get_name.with_args('foo', 'bar') 57 | 58 | def test_allows_varargs_if_specified(self, test_object): 59 | doubled_user = ObjectDouble(test_object) 60 | 61 | allow(doubled_user).method_with_varargs.with_args('foo', 'bar', 'baz') 62 | 63 | assert doubled_user.method_with_varargs('foo', 'bar', 'baz') is None 64 | 65 | def test_allows_missing_default_arguments(self, test_object): 66 | doubled_user = ObjectDouble(test_object) 67 | 68 | allow(doubled_user).method_with_default_args.with_args('blah') 69 | 70 | assert doubled_user.method_with_default_args('blah') is None 71 | 72 | def test_allows_default_arguments_specified_positionally(self, test_object): 73 | doubled_user = ObjectDouble(test_object) 74 | 75 | allow(doubled_user).method_with_default_args.with_args('blah', 'blam') 76 | 77 | assert doubled_user.method_with_default_args('blah', 'blam') is None 78 | 79 | def test_allows_default_arguments_specified_with_keywords(self, test_object): 80 | doubled_user = ObjectDouble(test_object) 81 | 82 | allow(doubled_user).method_with_default_args.with_args('blah', bar='blam') 83 | 84 | assert doubled_user.method_with_default_args('blah', bar='blam') is None 85 | 86 | def test_raises_when_specifying_higher_arity_to_method_with_default_arguments(self, test_object): # noqa 87 | doubled_user = ObjectDouble(test_object) 88 | 89 | with raises(VerifyingDoubleArgumentError): 90 | allow(doubled_user).method_with_default_args.with_args(1, 2, 3) 91 | 92 | def test_raises_when_specifying_extra_keyword_arguments(self, test_object): 93 | doubled_user = ObjectDouble(test_object) 94 | 95 | with raises(VerifyingDoubleArgumentError): 96 | allow(doubled_user).method_with_default_args.with_args(1, moo='woof') 97 | 98 | def test_allows_varkwargs_if_specified(self, test_object): 99 | doubled_user = ObjectDouble(test_object) 100 | 101 | allow(doubled_user).method_with_varkwargs.with_args(foo='bar') 102 | 103 | assert doubled_user.method_with_varkwargs(foo='bar') is None 104 | 105 | def test_mocking__call__works(self, test_object): 106 | doubled_user = ObjectDouble(test_object) 107 | 108 | allow(doubled_user).__call__.and_return('bob barker') 109 | 110 | assert doubled_user() == 'bob barker' 111 | 112 | def test_mocking__enter__and__exit__works(self, test_object): 113 | doubled_user = ObjectDouble(test_object) 114 | 115 | allow(doubled_user).__enter__.and_return('bob barker') 116 | # Not ideal to do both in the same test case, 117 | # but the with block won't execute at all unless both methods are defined. 118 | allow(doubled_user).__exit__ 119 | 120 | with doubled_user as u: 121 | assert u == 'bob barker' 122 | -------------------------------------------------------------------------------- /test/partial_double_test.py: -------------------------------------------------------------------------------- 1 | from pytest import raises, mark 2 | 3 | from doubles.exceptions import ( 4 | VerifyingDoubleError, 5 | VerifyingDoubleArgumentError, 6 | UnallowedMethodCallError, 7 | ) 8 | from doubles.lifecycle import teardown 9 | from doubles import allow, no_builtin_verification 10 | from doubles.testing import User, OldStyleUser, UserWithCustomNew 11 | import doubles.testing 12 | 13 | 14 | @mark.parametrize('test_class', [User, OldStyleUser]) 15 | class TestInstanceMethods(object): 16 | def test_arbitrary_callable_on_instance(self, test_class): 17 | instance = test_class('Bob', 10) 18 | allow(instance).arbitrary_callable.and_return('Bob Barker') 19 | assert instance.arbitrary_callable() == 'Bob Barker' 20 | teardown() 21 | assert instance.arbitrary_callable() == 'ArbitraryCallable Value' 22 | 23 | def test_arbitrary_callable_on_class(self, test_class): 24 | allow(test_class).arbitrary_callable.and_return('Bob Barker') 25 | assert test_class.arbitrary_callable() == 'Bob Barker' 26 | teardown() 27 | assert test_class.arbitrary_callable() == 'ArbitraryCallable Value' 28 | 29 | def test_callable_class_attribute(self, test_class): 30 | allow(test_class).callable_class_attribute.and_return('Bob Barker') 31 | assert test_class.callable_class_attribute() == 'Bob Barker' 32 | teardown() 33 | assert test_class.callable_class_attribute() == 'dummy result' 34 | 35 | def test_callable_instance_attribute(self, test_class): 36 | user = test_class('Alice', 25) 37 | allow(user).callable_instance_attribute.and_return('Bob Barker') 38 | 39 | assert user.callable_instance_attribute() == 'Bob Barker' 40 | teardown() 41 | assert user.callable_instance_attribute() == 'dummy result' 42 | 43 | def test_stubs_instance_methods(self, test_class): 44 | user = test_class('Alice', 25) 45 | 46 | allow(user).get_name.and_return('Bob') 47 | 48 | assert user.get_name() == 'Bob' 49 | 50 | def test_restores_instance_methods_on_teardown(self, test_class): 51 | user = test_class('Alice', 25) 52 | allow(user).get_name.and_return('Bob') 53 | 54 | teardown() 55 | 56 | assert user.get_name() == 'Alice' 57 | 58 | def test_only_affects_stubbed_method(self, test_class): 59 | user = test_class('Alice', 25) 60 | 61 | allow(user).get_name.and_return('Bob') 62 | 63 | assert user.age == 25 64 | 65 | def test_raises_when_stubbing_nonexistent_methods(self, test_class): 66 | user = test_class('Alice', 25) 67 | 68 | with raises(VerifyingDoubleError): 69 | allow(user).gender 70 | 71 | def test_stubs_properties(self, test_class): 72 | user = test_class('Alice', 25) 73 | 74 | allow(user).some_property.and_return('foo') 75 | 76 | assert user.some_property == 'foo' 77 | 78 | def test_stubing_property_with_args_raises(self, test_class): 79 | user = test_class('Alice', 25) 80 | 81 | with raises(VerifyingDoubleArgumentError) as e: 82 | allow(user).some_property.with_args(1) 83 | 84 | assert str(e.value) == 'Properties do not accept arguments.' 85 | 86 | def test_calling_stubbed_property_with_args_works(self, test_class): 87 | user = test_class('Alice', 25) 88 | allow(user).some_property.and_return(lambda x: x) 89 | 90 | assert user.some_property('bob') == 'bob' 91 | 92 | def test_stubbing_properties_on_multiple_instances(self, test_class): 93 | user_1 = test_class('Bob', 25) 94 | user_2 = test_class('Drew', 25) 95 | 96 | allow(user_1).some_property.and_return('Barker') 97 | allow(user_2).some_property.and_return('Carey') 98 | 99 | assert user_1.some_property == 'Barker' 100 | assert user_2.some_property == 'Carey' 101 | 102 | def test_stubbing_property_does_not_affect_other_instances(self, test_class): 103 | user_1 = test_class('Bob', 25) 104 | user_2 = test_class('Drew', 25) 105 | 106 | allow(user_1).some_property.and_return('Barker') 107 | 108 | assert user_1.some_property == 'Barker' 109 | assert user_2.some_property == 'some_property return value' 110 | 111 | def test_teardown_restores_properties(self, test_class): 112 | user_1 = test_class('Bob', 25) 113 | user_2 = test_class('Drew', 25) 114 | 115 | allow(user_1).some_property.and_return('Barker') 116 | allow(user_2).some_property.and_return('Carey') 117 | 118 | teardown() 119 | 120 | assert user_1.some_property == 'some_property return value' 121 | assert user_2.some_property == 'some_property return value' 122 | 123 | 124 | @mark.parametrize('test_class', [User, OldStyleUser]) 125 | class Test__call__(object): 126 | def test_basic_usage(self, test_class): 127 | user = test_class('Alice', 25) 128 | allow(user).__call__.and_return('bob barker') 129 | 130 | assert user() == 'bob barker' 131 | 132 | def test_stubbing_two_objects_does_not_interfere(self, test_class): 133 | alice = test_class('Alice', 25) 134 | peter = test_class('Peter', 25) 135 | 136 | allow(alice).__call__.and_return('alice') 137 | allow(peter).__call__.and_return('peter') 138 | 139 | assert alice() == 'alice' 140 | assert peter() == 'peter' 141 | 142 | def test_does_not_intefere_with_unstubbed_objects(self, test_class): 143 | alice = test_class('Alice', 25) 144 | peter = test_class('Peter', 25) 145 | 146 | allow(alice).__call__.and_return('alice') 147 | 148 | assert alice() == 'alice' 149 | assert peter() == 'user was called' 150 | 151 | def test_teardown_restores_previous_functionality(self, test_class): 152 | user = test_class('Alice', 25) 153 | allow(user).__call__.and_return('bob barker') 154 | 155 | assert user() == 'bob barker' 156 | 157 | teardown() 158 | 159 | assert user() == 'user was called' 160 | 161 | def test_works_with_arguments(self, test_class): 162 | user = test_class('Alice', 25) 163 | allow(user).__call__.with_args(1, 2).and_return('bob barker') 164 | 165 | assert user(1, 2) == 'bob barker' 166 | 167 | def test_raises_when_called_with_invalid_args(self, test_class): 168 | user = test_class('Alice', 25) 169 | allow(user).__call__.with_args(1, 2).and_return('bob barker') 170 | 171 | with raises(UnallowedMethodCallError): 172 | user(1, 2, 3) 173 | 174 | def test_raises_when_mocked_with_invalid_call_signature(self, test_class): 175 | user = test_class('Alice', 25) 176 | with raises(VerifyingDoubleArgumentError): 177 | allow(user).__call__.with_args(1, 2, bob='barker') 178 | 179 | 180 | @mark.parametrize('test_class', [User, OldStyleUser]) 181 | class Test__enter__(object): 182 | def test_basic_usage(self, test_class): 183 | user = test_class('Alice', 25) 184 | allow(user).__enter__.and_return(user) 185 | 186 | with user as u: 187 | assert user == u 188 | 189 | def test_stubbing_two_objects_does_not_interfere(self, test_class): 190 | alice = test_class('Alice', 25) 191 | bob = test_class('Bob', 25) 192 | 193 | allow(alice).__enter__.and_return('alice') 194 | allow(bob).__enter__.and_return('bob') 195 | 196 | with alice as a: 197 | assert a == 'alice' 198 | 199 | with bob as b: 200 | assert b == 'bob' 201 | 202 | def test_does_not_intefere_with_unstubbed_objects(self, test_class): 203 | alice = test_class('Alice', 25) 204 | bob = test_class('Bob', 25) 205 | 206 | allow(alice).__enter__.and_return('user') 207 | 208 | with alice as a: 209 | assert a == 'user' 210 | 211 | with bob as b: 212 | assert b == bob 213 | 214 | def test_teardown_restores_previous_functionality(self, test_class): 215 | user = test_class('Alice', 25) 216 | allow(user).__enter__.and_return('bob barker') 217 | 218 | with user as u: 219 | assert u == 'bob barker' 220 | 221 | teardown() 222 | 223 | with user as u: 224 | assert u == user 225 | 226 | def test_raises_when_mocked_with_invalid_call_signature(self, test_class): 227 | user = test_class('Alice', 25) 228 | with raises(VerifyingDoubleArgumentError): 229 | allow(user).__enter__.with_args(1) 230 | 231 | 232 | @mark.parametrize('test_class', [User, OldStyleUser]) 233 | class Test__exit__(object): 234 | def test_basic_usage(self, test_class): 235 | user = test_class('Alice', 25) 236 | allow(user).__exit__.with_args(None, None, None) 237 | 238 | with user: 239 | pass 240 | 241 | def test_stubbing_two_objects_does_not_interfere(self, test_class): 242 | alice = test_class('Alice', 25) 243 | bob = test_class('Bob', 25) 244 | 245 | allow(alice).__exit__.and_return('alice') 246 | allow(bob).__exit__.and_return('bob') 247 | 248 | assert alice.__exit__(None, None, None) == 'alice' 249 | assert bob.__exit__(None, None, None) == 'bob' 250 | 251 | def test_does_not_intefere_with_unstubbed_objects(self, test_class): 252 | alice = test_class('Alice', 25) 253 | bob = test_class('Bob', 25) 254 | 255 | allow(alice).__exit__.and_return('user') 256 | 257 | assert alice.__exit__(None, None, None) == 'user' 258 | assert bob.__exit__(None, None, None) is None 259 | 260 | def test_teardown_restores_previous_functionality(self, test_class): 261 | user = test_class('Alice', 25) 262 | allow(user).__exit__.and_return('bob barker') 263 | 264 | assert user.__exit__(None, None, None) == 'bob barker' 265 | 266 | teardown() 267 | 268 | assert user.__exit__(None, None, None) is None 269 | 270 | def test_raises_when_mocked_with_invalid_call_signature(self, test_class): 271 | user = test_class('Alice', 25) 272 | with raises(VerifyingDoubleArgumentError): 273 | allow(user).__exit__.with_no_args() 274 | 275 | 276 | @mark.parametrize('test_class', [User, OldStyleUser]) 277 | class TestClassMethods(object): 278 | def test_stubs_class_methods(self, test_class): 279 | allow(test_class).class_method.with_args('foo').and_return('overridden value') 280 | 281 | assert test_class.class_method('foo') == 'overridden value' 282 | 283 | def test_restores_class_methods_on_teardown(self, test_class): 284 | allow(test_class).class_method.and_return('overridden value') 285 | 286 | teardown() 287 | 288 | assert test_class.class_method('foo') == 'class_method return value: foo' 289 | 290 | def test_raises_when_stubbing_noncallable_attributes(self, test_class): 291 | with raises(VerifyingDoubleError): 292 | allow(test_class).class_attribute 293 | 294 | def test_raises_when_stubbing_nonexistent_class_methods(self, test_class): 295 | with raises(VerifyingDoubleError): 296 | allow(test_class).nonexistent_method 297 | 298 | 299 | class TestBuiltInConstructorMethods(object): 300 | def test_stubs_constructors(self): 301 | with no_builtin_verification(): 302 | user = object() 303 | 304 | allow(User).__new__.and_return(user) 305 | 306 | assert User('Alice', 25) is user 307 | 308 | def test_restores_constructor_on_teardown(self): 309 | user = object() 310 | allow(User).__new__.and_return(user) 311 | 312 | teardown() 313 | 314 | result = User('Alice', 25) 315 | 316 | assert result is not user 317 | assert result.name == 'Alice' 318 | 319 | 320 | class TestCustomConstructorMethods(object): 321 | def test_stubs_constructors(self): 322 | with no_builtin_verification(): 323 | user = object() 324 | 325 | allow(UserWithCustomNew).__new__.and_return(user) 326 | 327 | assert UserWithCustomNew('Alice', 25) is user 328 | 329 | def test_restores_constructor_on_teardown(self): 330 | user = object() 331 | allow(UserWithCustomNew).__new__.and_return(user) 332 | 333 | teardown() 334 | 335 | result = UserWithCustomNew('Alice', 25) 336 | 337 | assert result is not user 338 | assert result.name_set_in__new__ == 'Alice' 339 | assert result.name == 'Alice' 340 | 341 | 342 | class TestTopLevelFunctions(object): 343 | def test_stubs_method(self): 344 | allow(doubles.testing).top_level_function.and_return('foo') 345 | 346 | assert doubles.testing.top_level_function('bob barker') == 'foo' 347 | 348 | def test_restores_the_orignal_method(self): 349 | allow(doubles.testing).top_level_function.and_return('foo') 350 | teardown() 351 | assert doubles.testing.top_level_function('foo', 'bar') == 'foo -- bar' 352 | 353 | def test_raises_if_incorrect_call_signature_used(self): 354 | with raises(VerifyingDoubleArgumentError): 355 | allow(doubles.testing).top_level_function.with_args( 356 | 'bob', 357 | 'barker', 358 | 'is_great' 359 | ) 360 | 361 | def test_allows_correct_call_signature(self): 362 | allow(doubles.testing).top_level_function.with_args( 363 | 'bob', 364 | 'barker', 365 | ).and_return('bar') 366 | assert doubles.testing.top_level_function('bob', 'barker') == 'bar' 367 | 368 | def test_verifies_the_function_exists(self): 369 | with raises(VerifyingDoubleError): 370 | allow(doubles.testing).fake_function 371 | 372 | def test_callable_top_level_variable(self): 373 | allow(doubles.testing).callable_variable.and_return('foo') 374 | 375 | assert doubles.testing.callable_variable('bob barker') == 'foo' 376 | 377 | def test_decorated_function(self): 378 | allow(doubles.testing).decorated_function_callable.and_return('foo') 379 | 380 | assert doubles.testing.decorated_function_callable('bob barker') == 'foo' 381 | 382 | def test_decorated_function_that_returns_a_callable(self): 383 | allow(doubles.testing).decorated_function.and_return('foo') 384 | 385 | assert doubles.testing.decorated_function('bob barker') == 'foo' 386 | 387 | def test_variable_that_points_to_class_method(self): 388 | allow(doubles.testing).class_method.and_return('foo') 389 | 390 | assert doubles.testing.class_method('bob barker') == 'foo' 391 | 392 | def test_variable_that_points_to_instance_method(self): 393 | allow(doubles.testing).instance_method.and_return('foo') 394 | 395 | assert doubles.testing.instance_method() == 'foo' 396 | 397 | 398 | class TestClassWithGetAttr(object): 399 | def test_can_allow_an_existing_method(self): 400 | test_obj = doubles.testing.ClassWithGetAttr() 401 | allow(test_obj).method.and_return('foobar') 402 | 403 | assert test_obj.method() == 'foobar' 404 | 405 | def test_raises_if_method_does_not_exist(self): 406 | test_obj = doubles.testing.ClassWithGetAttr() 407 | 408 | with raises(VerifyingDoubleError): 409 | allow(test_obj).fake_function 410 | -------------------------------------------------------------------------------- /test/patch_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from doubles import ( 4 | patch, 5 | patch_class, 6 | InstanceDouble, 7 | ClassDouble, 8 | allow_constructor, 9 | ) 10 | from doubles.exceptions import ( 11 | VerifyingDoubleImportError, 12 | VerifyingDoubleError, 13 | ) 14 | from doubles.lifecycle import teardown 15 | import doubles.testing 16 | 17 | 18 | class TestPatch(object): 19 | def test_patch_wth_new_object_supplied(self): 20 | patch('doubles.testing.User', 'Bob Barker') 21 | 22 | assert doubles.testing.User == 'Bob Barker' 23 | 24 | def test_restores_original_value(self): 25 | original_value = doubles.testing.User 26 | patch('doubles.testing.User', 'Bob Barker') 27 | 28 | teardown() 29 | 30 | assert original_value == doubles.testing.User 31 | 32 | def test_creating_instance_double_after_patching(self): 33 | patch('doubles.testing.User', InstanceDouble('doubles.testing.User')) 34 | 35 | assert InstanceDouble('doubles.testing.User') 36 | 37 | def test_patch_non_existent_object(self): 38 | with pytest.raises(VerifyingDoubleError): 39 | patch('doubles.testing.NotReal', True) 40 | 41 | 42 | class TestPatchClass(object): 43 | def test_raises_an_error_trying_to_patch_a_function(self): 44 | with pytest.raises(VerifyingDoubleImportError): 45 | patch_class('doubles.test.top_level_function') 46 | 47 | def test_using_the_patched_class(self): 48 | allow_constructor(patch_class('doubles.testing.User')).and_return('result_1') 49 | allow_constructor(patch_class('doubles.testing.OldStyleUser')).and_return('result_2') 50 | 51 | result_1, result_2 = doubles.testing.top_level_function_that_creates_an_instance() 52 | 53 | assert result_1 == 'result_1' 54 | assert result_2 == 'result_2' 55 | 56 | def test_restores_original_value(self): 57 | original_value = doubles.testing.User 58 | patch_class('doubles.testing.User') 59 | 60 | teardown() 61 | 62 | assert original_value == doubles.testing.User 63 | 64 | def test_non_existent_class(self): 65 | with pytest.raises(VerifyingDoubleImportError): 66 | patch_class('doubles.testing.NotReal') 67 | 68 | def test_class_double_after_patching(self): 69 | patch_class('doubles.testing.User') 70 | assert ClassDouble('doubles.testing.User') 71 | -------------------------------------------------------------------------------- /test/pytest_test.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = "pytester" 2 | 3 | 4 | def test_exceptions_dont_cause_leaking_between_tests(testdir, capsys): 5 | testdir.makepyfile(""" 6 | 7 | from doubles.targets.expectation_target import expect 8 | from doubles.testing import User 9 | 10 | def test_that_sets_expectation_then_raises(): 11 | expect(User).class_method.with_args(1).once() 12 | raise Exception('Bob') 13 | 14 | def test_that_should_pass(): 15 | assert True 16 | 17 | """) 18 | result = testdir.runpytest() 19 | outcomes = result.parseoutcomes() 20 | assert outcomes['failed'] == 1 21 | assert outcomes['passed'] == 1 22 | 23 | 24 | def test_failed_expections_do_not_leak_between_tests(testdir, capsys): 25 | testdir.makepyfile(""" 26 | 27 | from doubles.targets.expectation_target import expect 28 | from doubles.testing import User 29 | 30 | def test_that_fails_for_not_satisfying_expectation(): 31 | expect(User).class_method.with_args('test_one').once() 32 | 33 | def test_that_should_fail_for_not_satisfying_expection(): 34 | expect(User).class_method.with_args('test_two').once() 35 | 36 | """) 37 | result = testdir.runpytest() 38 | outcomes = result.parseoutcomes() 39 | assert outcomes['failed'] == 2 40 | expected_error = ( 41 | "*Expected 'class_method' to be called 1 time instead of 0 times on" 42 | " with ('{arg_value}')*" 43 | ) 44 | result.stdout.fnmatch_lines([expected_error.format(arg_value="test_one")]) 45 | result.stdout.fnmatch_lines([expected_error.format(arg_value="test_two")]) 46 | -------------------------------------------------------------------------------- /test/return_values_test.py: -------------------------------------------------------------------------------- 1 | from pytest import raises, mark 2 | 3 | from doubles.instance_double import InstanceDouble 4 | from doubles import allow, expect 5 | from doubles.lifecycle import teardown 6 | 7 | 8 | class UserDefinedException(Exception): 9 | pass 10 | 11 | 12 | class UserDefinedExceptionWithArgs(Exception): 13 | def __init__(self, msg, arg1, arg2=None): 14 | pass 15 | 16 | 17 | @mark.parametrize('stubber', [allow, expect]) 18 | class TestReturnValues(object): 19 | def test_returns_result_of_a_callable(self, stubber): 20 | subject = InstanceDouble('doubles.testing.User') 21 | 22 | stubber(subject).instance_method.and_return_result_of(lambda: 'bar') 23 | 24 | assert subject.instance_method() == 'bar' 25 | 26 | def test_returns_result_of_a_callable_with_positional_arg(self, stubber): 27 | subject = InstanceDouble('doubles.testing.User') 28 | 29 | stubber(subject).method_with_positional_arguments.and_return_result_of(lambda x: x) 30 | 31 | assert subject.method_with_positional_arguments('bar') == 'bar' 32 | 33 | def test_returns_result_of_a_callable_with_positional_vargs(self, stubber): 34 | subject = InstanceDouble('doubles.testing.User') 35 | 36 | stubber(subject).method_with_varargs.and_return_result_of(lambda *x: x) 37 | 38 | result = subject.method_with_varargs('bob', 'barker') 39 | assert result == ('bob', 'barker') 40 | 41 | def test_returns_result_of_a_callable_with_varkwargs(self, stubber): 42 | subject = InstanceDouble('doubles.testing.User') 43 | 44 | stubber(subject).method_with_varkwargs.and_return_result_of(lambda **kwargs: kwargs['bob']) 45 | 46 | assert subject.method_with_varkwargs(bob='barker') == 'barker' 47 | 48 | def test_raises_provided_exception(self, stubber): 49 | subject = InstanceDouble('doubles.testing.User') 50 | 51 | stubber(subject).instance_method.and_raise(UserDefinedException) 52 | 53 | with raises(UserDefinedException): 54 | subject.instance_method() 55 | 56 | def test_raises_provided_exception_with_complex_signature(self, stubber): 57 | subject = InstanceDouble('doubles.testing.User') 58 | 59 | stubber(subject).instance_method.and_raise( 60 | UserDefinedExceptionWithArgs('msg', 'arg1', arg2='arg2'), 61 | ) 62 | 63 | with raises(UserDefinedExceptionWithArgs): 64 | subject.instance_method() 65 | 66 | def test_chaining_result_methods_gives_the_last_one_precedence(self, stubber): 67 | subject = InstanceDouble('doubles.testing.User') 68 | 69 | stubber(subject).instance_method.and_return('bar').and_return_result_of( 70 | lambda: 'baz' 71 | ).and_raise(UserDefinedException).and_return('final') 72 | 73 | assert subject.instance_method() == 'final' 74 | 75 | 76 | @mark.parametrize('stubber', [allow, expect]) 77 | class TestAndReturn(object): 78 | def test_raises_if_no_arguments_supplied(self, stubber): 79 | subject = InstanceDouble('doubles.testing.User') 80 | 81 | with raises(TypeError) as e: 82 | stubber(subject).instance_method.and_return() 83 | 84 | assert str(e.value) == 'and_return() expected at least 1 return value' 85 | teardown() 86 | 87 | def test_returns_specified_value(self, stubber): 88 | subject = InstanceDouble('doubles.testing.User') 89 | 90 | stubber(subject).instance_method.and_return('bar') 91 | 92 | assert subject.instance_method() == 'bar' 93 | 94 | def test_returns_specified_values_in_order(self, stubber): 95 | subject = InstanceDouble('doubles.testing.User') 96 | 97 | stubber(subject).instance_method.and_return('bar', 'bazz') 98 | 99 | assert subject.instance_method() == 'bar' 100 | assert subject.instance_method() == 'bazz' 101 | 102 | def test_returns_the_last_specified_value_multiple_times(self, stubber): 103 | subject = InstanceDouble('doubles.testing.User') 104 | 105 | stubber(subject).instance_method.and_return('bar', 'bazz') 106 | 107 | assert subject.instance_method() == 'bar' 108 | assert subject.instance_method() == 'bazz' 109 | assert subject.instance_method() == 'bazz' 110 | 111 | def test_subsequent_allowances_override_previous_ones(self, stubber): 112 | subject = InstanceDouble('doubles.testing.User') 113 | 114 | stubber(subject).instance_method.never().and_return('bar') 115 | stubber(subject).instance_method.and_return('baz') 116 | 117 | assert subject.instance_method() == 'baz' 118 | -------------------------------------------------------------------------------- /test/unittest_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | try: 4 | from StringIO import StringIO 5 | except ImportError: 6 | from io import StringIO 7 | 8 | from doubles.instance_double import InstanceDouble 9 | from doubles.targets.expectation_target import expect 10 | from doubles.unittest import TestCase 11 | 12 | 13 | def test_unittest_integration(): 14 | class UnittestIntegration(TestCase): 15 | def runTest(self): 16 | subject = InstanceDouble('doubles.testing.User') 17 | 18 | expect(subject).instance_method 19 | 20 | test_loader = unittest.TestLoader() 21 | suite = test_loader.loadTestsFromTestCase(UnittestIntegration) 22 | stream = StringIO() 23 | runner = unittest.TextTestRunner(stream=stream) 24 | result = runner.run(suite) 25 | 26 | assert len(result.failures) == 1 27 | assert 'MockExpectationError' in stream.getvalue() 28 | --------------------------------------------------------------------------------