├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .ian ├── BUGS ├── CHANGES.rst ├── LICENSE ├── Makefile ├── README.rst ├── ToDo ├── debian ├── changelog ├── compat ├── control ├── copyright ├── python-doublex.install ├── python3-doublex.install ├── rules ├── source │ └── format └── watch ├── docs ├── Makefile ├── README.rst.inc ├── README.rst.inc.old ├── api.rst ├── async-spies.rst ├── async-spies.rst.test ├── calls.rst ├── calls.rst.test ├── conf.py ├── contents.rst.inc ├── delegates.rst ├── delegates.rst.test ├── doubles.rst ├── doubles.rst.test ├── index.rst ├── inline-setup.rst ├── install.rst ├── methods.rst ├── methods.rst.test ├── mimics.rst ├── mimics.rst.test ├── observers.rst ├── observers.rst.test ├── properties.rst ├── properties.rst.test ├── pyDoubles.rst ├── reference.rst ├── reference.rst.test └── release-notes.rst ├── doctests ├── Makefile ├── conf.py ├── index.rst └── test.rst ├── doublex.svg ├── doublex ├── __init__.py ├── doubles.py ├── internal.py ├── matchers.py ├── proxy.py ├── test │ ├── README │ ├── __init__.py │ ├── any_arg_tests.py │ ├── async_race_condition_tests.py │ ├── chain_tests.py │ ├── issue_14_tests.py │ ├── namedtuple_stub_tests.py │ ├── report_tests.py │ └── unit_tests.py └── tracer.py ├── pydoubles-site ├── doublex-documentation ├── downloads ├── overview ├── pydoubles-documentation ├── release-notes └── support ├── pyproject.toml ├── slides ├── Makefile ├── beamer │ ├── Makefile │ ├── custom.sty │ └── slides.tex ├── index.html └── sample.py ├── tox.ini └── unittest.cfg /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | name: Python ${{ matrix.python-version }} 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] 15 | 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install tox tox-gh-actions 26 | - name: Test with tox 27 | run: tox 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | _build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | __pycache__ 22 | doublex/_version.py 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | 32 | doctests/*.inc 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | .vscode/ 42 | 43 | debian/files 44 | debian/.debhelper/ 45 | debian/python-doublex* 46 | debian/python3-doublex* 47 | debian/tmp 48 | 49 | .idea -------------------------------------------------------------------------------- /.ian: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: shell-script; tab-width: 4 -*- 2 | 3 | function ian-release-hook() { 4 | local version=$(upstream-version) 5 | log-info "setting version to $version" 6 | sc-assert-files-exist version.py 7 | echo "__version__ = '$version'" > version.py 8 | git tag -f v"$version" 9 | } 10 | 11 | function ian-clean-hook { 12 | rm -rf .tox .eggs 13 | rm -rf dist 14 | } 15 | -------------------------------------------------------------------------------- /BUGS: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spy stubbed method avoid collaborator method invocation 4 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 20230214 2 | ======== 3 | 4 | - Release 1.9.6 5 | - Drop support for Python 3.5 and earlier 6 | - Allow using called().with_some_args() with type-hinted arguments 7 | - Fix deprecation warnings 8 | - Mock support for classmethods on instances 9 | - Fix namedtuple support for python 3.8 and newer 10 | - Fix issue with property stubbing using when() leaving stubs in setting up state 11 | - Allow making Mimics of Generic sub-classes 12 | 13 | 20190405 14 | ======== 15 | 16 | - Release 1.9.2 17 | - async() matcher is available as async_mode() for python3.7 and newer 18 | 19 | 20 | 20141126 21 | ======== 22 | 23 | - Release 1.8.2 24 | - Bug fixed: https://bitbucket.org/DavidVilla/python-doublex/issue/22 25 | - returns_input() may return several parameters, fixes 26 | - delegates() accepts dictionaries. 27 | - method_returning() and method_raising() are now spies: 28 | https://bitbucket.org/DavidVilla/python-doublex/issue/21 29 | 30 | 31 | 20140107 32 | ======== 33 | - Release 1.8.1 34 | - PyHamcrest must be a requirement. Thanks to Javier Santacruz and Guillermo Pascual 35 | https://bitbucket.org/DavidVilla/python-doublex/pull-request/7 36 | https://bitbucket.org/DavidVilla/python-doublex/issue/18 37 | 38 | 20140101 39 | ======== 40 | 41 | - Release 1.8a 42 | - [NEW] inline stubbing and mocking functions: when, expect_call (merge with feature-inline-stubbing) 43 | - [NEW] Testing Python 2.6, 2.7, 3.2 and 3.3 using tox 44 | - [NEW] Add AttributeFactory type: wrapper_descriptor for builtin method (such as list.__setitem__) 45 | 46 | 20131227 47 | ======== 48 | 49 | - [NEW] Double methods copy original __name__ attribute 50 | - [NEW] Mock support for properties 51 | 52 | 20131107 53 | ======== 54 | 55 | - Release 1.7.2 56 | - [NEW] support for varargs (*args, **kargs) methods 57 | - [NEW] tracer for doubles, methods and properties 58 | 59 | 20130712 60 | ======== 61 | 62 | - Release 1.6.8 63 | - [NEW] with_some_args matcher 64 | - [NEW] set_default_behavior() module function to define behavior for non stubbed methods. 65 | 66 | 20130513 67 | ======== 68 | 69 | - ANY_ARG is not allowed as keyword value 70 | - ANY_ARG must be the last positional argument value 71 | 72 | 20130427 73 | ======== 74 | 75 | - Release 1.6.6 76 | - [FIXED] stub/empty_stub were missing in pyDoubles wrapper 77 | 78 | 20130215 79 | ======== 80 | 81 | - Release 1.6.3 82 | - [FIXED] async race condition bug 83 | 84 | 20130211 85 | ======== 86 | 87 | - [NEW] Access to spy invocations with _method_.calls 88 | 89 | 20130110 90 | ======== 91 | 92 | - Release 1.6 93 | - [NEW] Ad-hoc stub attributes 94 | - [NEW] AttributeFactory callable types: function, method (Closes: #bitbucket:issue/7) 95 | - [NEW] BuiltingSignature for non Python functions 96 | 97 | 20121118 98 | ======== 99 | 100 | - [NEW] ProxySpy propagates stubbed invocations too 101 | 102 | 20121025 103 | ======== 104 | 105 | - Merge feature-async branch: Spy async checking 106 | 107 | 20121008 108 | ======== 109 | 110 | - Release 1.5 to replace pyDoubles 111 | 112 | 20120928 113 | ======== 114 | 115 | - ANY_ARG must be different to any other thing. 116 | 117 | 20120911 118 | ======== 119 | 120 | - API CHANGE: called_with() is now called().with_args() (magmax suggestion) 121 | 122 | 123 | .. Local Variables: 124 | .. coding: utf-8 125 | .. mode: rst 126 | .. mode: flyspell 127 | .. ispell-local-dictionary: "american" 128 | .. End: 129 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- mode:makefile -*- 3 | 4 | URL_AUTH=svn+ssh://${ALIOTH_USER}@svn.debian.org/svn/python-modules/packages/doublex/trunk 5 | URL_ANON=svn://svn.debian.org/svn/python-modules/packages/doublex/trunk 6 | BITBUCKET=git@bitbucket.org:DavidVilla/python-doublex.git 7 | 8 | debian: 9 | if [ ! -z "$${ALIOTH_USER}" ]; then \ 10 | svn co ${URL_AUTH} -N; \ 11 | else \ 12 | svn co ${URL_ANON} -N; \ 13 | fi 14 | 15 | mv trunk/.svn . 16 | rmdir trunk 17 | svn up debian 18 | 19 | 20 | .PHONY: docs doctests 21 | docs: 22 | $(MAKE) -C docs 23 | 24 | doctests: 25 | $(MAKE) -C doctests 26 | 27 | push: 28 | git push 29 | git push --tags 30 | git push ${BITBUCKET} 31 | git push --tags ${BITBUCKET} 32 | 33 | pypi-release: 34 | $(RM) -f dist 35 | python3 setup.py sdist 36 | twine upload dist/* 37 | 38 | clean: 39 | find . -name *.pyc -delete 40 | find . -name *.pyo -delete 41 | find . -name *~ -delete 42 | $(RM) -r *.egg-info MANIFEST 43 | $(RM) -r dist build *.egg-info .tox 44 | $(RM) -r slides/reveal.js 45 | $(MAKE) -C docs clean 46 | $(MAKE) -C doctests clean 47 | 48 | vclean: clean 49 | $(RM) -r .svn debian 50 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/doublex.png 2 | :target: http://pypi.python.org/pypi/doublex 3 | :alt: Latest PyPI version 4 | 5 | .. image:: https://img.shields.io/pypi/l/doublex.png?maxAge=2592000 6 | :alt: License 7 | 8 | .. image:: https://img.shields.io/pypi/pyversions/doublex.png?maxAge=2592000 9 | :target: http://pypi.python.org/pypi/doublex 10 | :alt: Supported Python Versions 11 | 12 | .. image:: https://github.com/DavidVilla/python-doublex/actions/workflows/tests.yml/badge.svg 13 | :target: https://github.com/DavidVilla/python-doublex 14 | :alt: GitHub Actions CI status 15 | 16 | Powerful test doubles framework for Python 17 | 18 | 19 | [ 20 | `install `_ | 21 | `docs `_ | 22 | `changelog `_ | 23 | `sources `_ | 24 | `issues `_ | 25 | `PyPI `_ | 26 | `github `_ 27 | ] 28 | 29 | 30 | a trivial example 31 | ----------------- 32 | 33 | .. sourcecode:: python 34 | 35 | import unittest 36 | from doublex import Spy, assert_that, called 37 | 38 | class SpyUseExample(unittest.TestCase): 39 | def test_spy_example(self): 40 | # given 41 | spy = Spy(SomeCollaboratorClass) 42 | cut = YourClassUnderTest(spy) 43 | 44 | # when 45 | cut.a_method_that_call_the_collaborator() 46 | 47 | # then 48 | assert_that(spy.some_method, called()) 49 | 50 | See more about `doublex doubles `_. 51 | 52 | 53 | Features 54 | -------- 55 | 56 | * doubles have not public API framework methods. It could cause silent misspelling. 57 | * doubles do not require collaborator instances, just classes, and it never instantiate them. 58 | * ``assert_that()`` is used for ALL assertions. 59 | * mock invocation order is relevant by default. 60 | * supports old and new style classes. 61 | * **supports Python versions: 3.6, 3.7, 3.8, 3.9, 3.10** 62 | 63 | 64 | Debian 65 | ^^^^^^ 66 | 67 | * amateur repository: ``deb https://uclm-arco.github.io/debian sid main`` (always updated) 68 | * `official package `_ (may be outdated) 69 | * `official ubuntu package `_ 70 | * debian dir: ``svn://svn.debian.org/svn/python-modules/packages/doublex/trunk`` 71 | 72 | 73 | related 74 | ------- 75 | 76 | * `pyDoubles `_ 77 | * `doublex-expects `_ 78 | * `crate `_ 79 | * `other doubles `_ 80 | * `ludibrio `_ 81 | * `doubles `_ 82 | 83 | 84 | .. Local Variables: 85 | .. coding: utf-8 86 | .. mode: rst 87 | .. mode: flyspell 88 | .. ispell-local-dictionary: "american" 89 | .. fill-columnd: 90 90 | .. End: 91 | -------------------------------------------------------------------------------- /ToDo: -------------------------------------------------------------------------------- 1 | - function doubles (supporting __call__ method) 2 | - orphan spy methods 3 | - double chains 4 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | doublex (1.9.5.1-1) unstable; urgency=low 2 | 3 | * New release 4 | 5 | -- David Villa Alises Thu, 03 Feb 2022 11:23:04 +0100 6 | 7 | doublex (1.9.4-1) unstable; urgency=low 8 | 9 | * New release 10 | 11 | -- David Villa Alises Thu, 03 Feb 2022 11:11:21 +0100 12 | 13 | doublex (1.9.3-2) unstable; urgency=low 14 | 15 | * New release 16 | 17 | -- David Villa Alises Tue, 15 Dec 2020 22:44:19 +0100 18 | 19 | doublex (1.9.3-1) unstable; urgency=low 20 | 21 | * New release 22 | 23 | -- David Villa Alises Tue, 15 Dec 2020 11:35:46 +0100 24 | 25 | doublex (1.9.2-1) unstable; urgency=low 26 | 27 | * New release 28 | 29 | -- David Villa Alises Fri, 05 Apr 2019 11:24:14 +0200 30 | 31 | doublex (1.9.1-2) unstable; urgency=low 32 | 33 | * New release 34 | * lintian fixes 35 | 36 | -- David Villa Alises Thu, 01 Nov 2018 20:41:59 +0100 37 | 38 | doublex (1.9.1-1) unstable; urgency=low 39 | 40 | * New release 41 | * Python3.7 support 42 | 43 | -- David Villa Alises Tue, 24 Jul 2018 10:47:21 +0200 44 | 45 | doublex (1.9.0-1) unstable; urgency=low 46 | 47 | * New release 48 | * pyDoubles support removed 49 | 50 | -- David Villa Alises Thu, 19 Jul 2018 11:22:02 +0200 51 | 52 | doublex (1.8.4-1) unstable; urgency=low 53 | 54 | * New release 55 | * setup.packages avoids test packages 56 | 57 | -- David Villa Alises Wed, 09 Nov 2016 23:55:36 +0000 58 | 59 | doublex (1.8.3-1) unstable; urgency=medium 60 | 61 | * New release 62 | 63 | -- David Villa Alises Tue, 27 Sep 2016 11:59:35 +0100 64 | 65 | doublex (1.8.2-1) unstable; urgency=low 66 | 67 | * New release 68 | 69 | -- David Villa Alises Wed, 26 Nov 2014 10:57:37 +0100 70 | 71 | doublex (1.8.1-1) unstable; urgency=low 72 | 73 | * New release 74 | 75 | -- David Villa Alises Tue, 07 Jan 2014 16:42:56 +0100 76 | 77 | doublex (1.8a-1) UNRELEASED; urgency=low 78 | 79 | * New release 80 | 81 | -- David Villa Alises Wed, 01 Jan 2014 18:15:11 +0100 82 | 83 | doublex (1.7.2-1) unstable; urgency=low 84 | 85 | * New release 86 | * [/control] using 3.9.4 standards-version 87 | 88 | -- David Villa Alises Wed, 06 Nov 2013 23:40:56 +0100 89 | 90 | doublex (1.7.1-1) UNRELEASED; urgency=low 91 | 92 | * New release 93 | 94 | -- David Villa Alises Mon, 28 Oct 2013 15:19:12 +0100 95 | 96 | doublex (1.6.6-4) UNRELEASED; urgency=low 97 | 98 | [ Jakub Wilk ] 99 | * Use canonical URIs for Vcs-* fields. 100 | 101 | -- David Villa Alises Mon, 28 Oct 2013 15:18:09 +0100 102 | 103 | doublex (1.6.6-2) unstable; urgency=low 104 | 105 | * [/control] python3:Depends for python3-doublex 106 | 107 | -- David Villa Alises Sat, 27 Apr 2013 21:05:27 +0200 108 | 109 | doublex (1.6.6-1) UNRELEASED; urgency=low 110 | 111 | * New release 112 | * [/control] python3 support 113 | 114 | -- David Villa Alises Sat, 27 Apr 2013 19:58:24 +0200 115 | 116 | doublex (1.6.5-1) UNRELEASED; urgency=low 117 | 118 | * New release 119 | 120 | -- David Villa Alises Thu, 25 Apr 2013 09:28:52 +0200 121 | 122 | doublex (1.6.3-2) UNRELEASED; urgency=low 123 | 124 | * New release 125 | 126 | -- David Villa Alises Mon, 18 Feb 2013 13:44:35 +0100 127 | 128 | doublex (1.6.1-1) UNRELEASED; urgency=low 129 | 130 | * New release 131 | 132 | -- David Villa Alises Thu, 07 Feb 2013 14:10:36 +0100 133 | 134 | doublex (1.6-1) UNRELEASED; urgency=low 135 | 136 | * New release 137 | * [/control] python3 support 138 | 139 | -- David Villa Alises Thu, 10 Jan 2013 13:34:34 +0100 140 | 141 | doublex (1.5.1-1) unstable; urgency=low 142 | 143 | * New release 144 | * First official release: (Closes: #688979) 145 | * debian/rules: clone specific version tag 146 | * debian/watch added 147 | 148 | -- David Villa Alises Thu, 25 Oct 2012 16:03:44 +0200 149 | 150 | doublex (1.5-1) UNRELEASED; urgency=low 151 | 152 | * Release to substitute pyDoubles 153 | * [/control] New description 154 | 155 | -- David Villa Alises Tue, 09 Oct 2012 12:00:05 +0200 156 | 157 | doublex (0.6.3-1) UNRELEASED; urgency=low 158 | 159 | * New release 160 | 161 | -- David Villa Alises Wed, 03 Oct 2012 15:31:42 +0200 162 | 163 | doublex (0.6.2-1) UNRELEASED; urgency=low 164 | 165 | * Initial release 166 | 167 | -- David Villa Alises Fri, 28 Sep 2012 14:04:37 +0200 168 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: doublex 2 | Section: python 3 | Priority: optional 4 | Maintainer: David Villa Alises 5 | Uploaders: Debian Python Modules Team 6 | Build-Depends: 7 | debhelper (>= 7.0.50), 8 | python-all (>= 2.6.6-3), 9 | python-setuptools (>= 0.6b3), 10 | python3-all, 11 | python3-setuptools, 12 | dh-python 13 | Standards-Version: 4.2.1 14 | Homepage: https://bitbucket.org/DavidVilla/python-doublex 15 | 16 | Package: python-doublex 17 | Architecture: all 18 | Depends: ${misc:Depends}, ${python:Depends}, python, python-hamcrest, python-six 19 | Replaces: python-pydoubles 20 | Description: test doubles framework for Python 2 21 | doublex is a test doubles framework for the Python unittest module. It may be 22 | used as a effective tool to perform Test Driven Development. 23 | . 24 | It provides stubs, spies, proxy-spies, mocks, individual methods, properties, 25 | etc. Methods support observer attachment or delegate return value generation 26 | to iterables, generators or even third party functions. A special double 27 | factory (called Mimic) allows one to create doubles inheriting original class 28 | superclasses. This provides replacements for the original instances even for 29 | code performing explicit type checking. 30 | . 31 | doublex supersedes pyDoubles and it provides its legacy API as a wrapper. 32 | 33 | 34 | Package: python3-doublex 35 | Architecture: all 36 | Depends: ${misc:Depends}, ${python3:Depends}, python3, python3-hamcrest, python3-six 37 | Description: test doubles framework for Python 3 38 | doublex is a test doubles framework for the Python unittest module. It may be 39 | used as a effective tool to perform Test Driven Development. 40 | . 41 | It provides stubs, spies, proxy-spies, mocks, individual methods, properties, 42 | etc. Methods support observer attachment or delegate return value generation 43 | to iterables, generators or even third party functions. A special double 44 | factory (called Mimic) allows one to create doubles inheriting original class 45 | superclasses. This provides replacements for the original instances even for 46 | code performing explicit type checking. 47 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | This work was packaged for Debian by: 2 | 3 | David Villa Alises on Mon, 3 Sep 2012 18:00:00 +0100 4 | 5 | It was downloaded from: 6 | 7 | https://bitbucket.org/DavidVilla/python-doublex 8 | 9 | Upstream Author: 10 | 11 | David Villa Alises 12 | 13 | 14 | Copyright: 15 | 16 | Copyright (C) 2012 David Villa Alises 17 | 18 | 19 | License: 20 | 21 | This program is free software: you can redistribute it and/or modify 22 | it under the terms of the GNU General Public License as published by 23 | the Free Software Foundation, either version 3 of the License, or 24 | (at your option) any later version. 25 | 26 | This package is distributed in the hope that it will be useful, 27 | but WITHOUT ANY WARRANTY; without even the implied warranty of 28 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 29 | GNU General Public License for more details. 30 | 31 | You should have received a copy of the GNU General Public License 32 | along with this program. If not, see . 33 | 34 | On Debian systems, the complete text of the GNU General 35 | Public License version 3 can be found in `/usr/share/common-licenses/GPL-3'. 36 | 37 | 38 | Legacy pyDoubles tests (test/pyDoubles/*) copyright by: 39 | 40 | Carlos Ble Jurado 41 | www.iExpertos.com 42 | 43 | License: 44 | 45 | Apache 2.0 46 | 47 | On Debian systems, the complete text of the GNU General 48 | Public License version 3 can be found in `/usr/share/common-licenses/Apache-2.0'. 49 | 50 | 51 | The Debian packaging is: 52 | 53 | Copyright (C) 2012 David Villa Alises 54 | 55 | and is licensed under the GPL version 3, see above. 56 | -------------------------------------------------------------------------------- /debian/python-doublex.install: -------------------------------------------------------------------------------- 1 | usr/lib/python2* 2 | -------------------------------------------------------------------------------- /debian/python3-doublex.install: -------------------------------------------------------------------------------- 1 | usr/lib/python3* 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | APP=doublex 4 | VERSION=$(shell head -n 1 debian/changelog | cut -f2 -d " " | tr -d "()" | cut -f1 -d "-") 5 | 6 | ORIG_REPO=git clone --branch v$(VERSION) https://github.com/davidvilla/python-doublex 7 | ORIG_DIR=$(APP)-$(VERSION) 8 | EXCLUDE=--exclude=debian --exclude=\*~ --exclude=.hg --exclude=.svn --exclude=\*.pyc 9 | 10 | # http://wiki.debian.org/Python/LibraryStyleGuide 11 | 12 | PYTHON2=$(shell pyversions -vr) 13 | PYTHON3=$(shell py3versions -vr) 14 | 15 | %: 16 | dh $@ --with python3 17 | 18 | 19 | build-python%: 20 | python$* setup.py build 21 | 22 | override_dh_auto_build: $(PYTHON3:%=build-python%) 23 | python setup.py build --force 24 | 25 | install-python%: 26 | python$* setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb 27 | 28 | override_dh_auto_install: $(PYTHON3:%=install-python%) 29 | python setup.py install --force --root=./debian/tmp --no-compile -O0 --install-layout=deb 30 | 31 | override_dh_auto_clean: 32 | python setup.py clean -a 33 | rm -rf build 34 | rm -rf *.egg-info 35 | 36 | get-orig-source: 37 | $(ORIG_REPO) $(ORIG_DIR) 38 | tar $(EXCLUDE) -czf $(APP)_$(VERSION).orig.tar.gz $(ORIG_DIR) 39 | $(RM) -r $(ORIG_DIR) 40 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | version=3 2 | 3 | https://bitbucket.org/DavidVilla/python-doublex/downloads/doublex-(.*)\.tar\.gz 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -W 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 dont 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) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | all: html 25 | 26 | help: 27 | @echo "Please use \`make ' where is one of" 28 | @echo " html to make standalone HTML files" 29 | @echo " dirhtml to make HTML files named index.html in directories" 30 | @echo " singlehtml to make a single large HTML file" 31 | @echo " pickle to make pickle files" 32 | @echo " json to make JSON files" 33 | @echo " htmlhelp to make HTML files and a HTML help project" 34 | @echo " qthelp to make HTML files and a qthelp project" 35 | @echo " devhelp to make HTML files and a Devhelp project" 36 | @echo " epub to make an epub" 37 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 38 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 39 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 40 | @echo " text to make text files" 41 | @echo " man to make manual pages" 42 | @echo " texinfo to make Texinfo files" 43 | @echo " info to make Texinfo files and run them through makeinfo" 44 | @echo " gettext to make PO message catalogs" 45 | @echo " changes to make an overview of all changed/added/deprecated items" 46 | @echo " xml to make Docutils-native XML files" 47 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 48 | @echo " linkcheck to check all external links for integrity" 49 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 50 | 51 | GENERATED=doubles.rst reference.rst methods.rst delegates.rst observers.rst mimics.rst async-spies.rst calls.rst properties.rst 52 | 53 | clean: 54 | rm -rf $(BUILDDIR)/* *~ 55 | # $(RM) $(GENERATED) 56 | 57 | html: index.rst release-notes.rst $(GENERATED) 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | %.rst: %.rst.test 63 | sed 's/^.. doctest::.*$$//g' $< > $@ 64 | sed -i 's/^.. testcode::.*$$//g' $@ 65 | sed -i 's/^.. testoutput::$$//g' $@ 66 | 67 | 68 | dirhtml: 69 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 70 | @echo 71 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 72 | 73 | singlehtml: 74 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 75 | @echo 76 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 77 | 78 | pickle: 79 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 80 | @echo 81 | @echo "Build finished; now you can process the pickle files." 82 | 83 | json: 84 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 85 | @echo 86 | @echo "Build finished; now you can process the JSON files." 87 | 88 | htmlhelp: 89 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 90 | @echo 91 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 92 | ".hhp project file in $(BUILDDIR)/htmlhelp." 93 | 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/doublex.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/doublex.qhc" 102 | 103 | devhelp: 104 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 105 | @echo 106 | @echo "Build finished." 107 | @echo "To view the help file:" 108 | @echo "# mkdir -p $$HOME/.local/share/devhelp/doublex" 109 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/doublex" 110 | @echo "# devhelp" 111 | 112 | epub: 113 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 114 | @echo 115 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 116 | 117 | latex: 118 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 119 | @echo 120 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 121 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 122 | "(use \`make latexpdf' here to do that automatically)." 123 | 124 | latexpdf: 125 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 126 | @echo "Running LaTeX files through pdflatex..." 127 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 128 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 129 | 130 | latexpdfja: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo "Running LaTeX files through platex and dvipdfmx..." 133 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 134 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 135 | 136 | text: 137 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 138 | @echo 139 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 140 | 141 | man: 142 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 143 | @echo 144 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 145 | 146 | texinfo: 147 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 148 | @echo 149 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 150 | @echo "Run \`make' in that directory to run these through makeinfo" \ 151 | "(use \`make info' here to do that automatically)." 152 | 153 | info: 154 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 155 | @echo "Running Texinfo files through makeinfo..." 156 | make -C $(BUILDDIR)/texinfo info 157 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 158 | 159 | gettext: 160 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 161 | @echo 162 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 163 | 164 | changes: 165 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 166 | @echo 167 | @echo "The overview file is in $(BUILDDIR)/changes." 168 | 169 | linkcheck: 170 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 171 | @echo 172 | @echo "Link check complete; look for any errors in the above output " \ 173 | "or in $(BUILDDIR)/linkcheck/output.txt." 174 | 175 | doctest: 176 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 177 | @echo "Testing of doctests in the sources finished, look at the " \ 178 | "results in $(BUILDDIR)/doctest/output.txt." 179 | 180 | xml: 181 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 182 | @echo 183 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 184 | 185 | pseudoxml: 186 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 187 | @echo 188 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 189 | -------------------------------------------------------------------------------- /docs/README.rst.inc: -------------------------------------------------------------------------------- 1 | ../README.rst -------------------------------------------------------------------------------- /docs/README.rst.inc.old: -------------------------------------------------------------------------------- 1 | .. image:: https://pypip.in/v/doublex/badge.png 2 | :target: https://crate.io/packages/doublex/ 3 | :alt: Latest PyPI version 4 | 5 | .. image:: https://pypip.in/d/doublex/badge.png 6 | :target: https://crate.io/packages/doublex/ 7 | :alt: Number of PyPI downloads 8 | 9 | 10 | Powerful test doubles framework for Python. 11 | 12 | design principles 13 | ----------------- 14 | 15 | * doubles should not have public API framework methods. It avoids silent misspelling. 16 | * doubles do not require collaborator instances, just classes, and it never instantiate them. 17 | * ``assert_that()`` is used for ALL assertions. 18 | * invocation order for mocks is relevant by default. 19 | * supports old and new style classes. 20 | 21 | 22 | a trivial example 23 | ----------------- 24 | 25 | .. sourcecode:: python 26 | 27 | import unittest 28 | from doublex import Spy, assert_that, called 29 | 30 | class SpyUseExample(unittest.TestCase): 31 | def test_spy_example(self): 32 | # given 33 | spy = Spy(SomeCollaboratorClass) 34 | cut = YourClassUnderTest(spy) 35 | 36 | # when 37 | cut.a_method_that_call_the_collaborator() 38 | 39 | # then 40 | assert_that(spy.some_method, called()) 41 | 42 | See more about `doublex doubles `_. 43 | 44 | 45 | relevant links 46 | -------------- 47 | 48 | * `install `_ 49 | * `documentation `_ 50 | * `release notes `_ 51 | * `slides `_ 52 | * `sources `_ 53 | * `PyPI project `_ 54 | * `pyDoubles `_ 55 | * `crate `_ 56 | * `buildbot job `_ 57 | * `other doubles `_ 58 | 59 | 60 | Debian 61 | ^^^^^^ 62 | 63 | * `official package `_ (may be outdated) 64 | * amateur repository: ``deb http://babel.esi.uclm.es/arco/ sid main`` (always updated) 65 | * `official ubuntu package `_ 66 | * debian dir: ``svn://svn.debian.org/svn/python-modules/packages/doublex/trunk`` 67 | 68 | 69 | .. Local Variables: 70 | .. coding: utf-8 71 | .. mode: rst 72 | .. mode: flyspell 73 | .. ispell-local-dictionary: "american" 74 | .. fill-columnd: 90 75 | .. End: 76 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | === 2 | API 3 | === 4 | 5 | Double classes 6 | ============== 7 | 8 | .. py:class:: Stub([collaborator]) 9 | .. py:class:: Spy([collaborator]) 10 | .. py:class:: ProxySpy([collaborator]) 11 | .. py:class:: Mock([collaborator]) 12 | 13 | .. py:class:: Mimic(double, collaborator) 14 | 15 | 16 | Stubbing 17 | ======== 18 | 19 | .. py:method:: Method.raises(exception) 20 | 21 | Stub method will raise the given `exception` when invoked. Method parameters are 22 | relevant, and they may be literal values or hamcrest matchers:: 23 | 24 | with Stub() as stub: 25 | stub.method().raises(ValueError) 26 | 27 | See :ref:`raises`. 28 | 29 | .. py:method:: Method.returns(value) 30 | 31 | Stub method will return the given value when invoked:: 32 | 33 | with Stub() as stub: 34 | stub.method().returns(100) 35 | 36 | See :ref:`stub`. 37 | 38 | .. py:method:: Method.returns_input() 39 | 40 | Stub method will return input parameters when invoked:: 41 | 42 | with Stub() as stub: 43 | stub.method().returns_input() 44 | 45 | See :ref:`returns_input`. 46 | 47 | 48 | .. py:method:: Method.delegates(delegate) 49 | 50 | Stub method will return values generated by the `delegate`, that may be a function, 51 | generator or iterable object:: 52 | 53 | with Stub() as stub: 54 | stub.method().delegates([1, 2, 4, 8]) 55 | 56 | See :ref:`delegates`. 57 | 58 | 59 | .. py:method:: Method.attach(callable) 60 | 61 | Stub methods are observable. You may attach arbitrary callable that will be invoked any 62 | time the stub method does:: 63 | 64 | counter = itertools.count() 65 | stub.method.attach(counter.next) 66 | 67 | See :ref:`observers`. 68 | 69 | 70 | Matchers 71 | ======== 72 | 73 | .. py:class:: never(matcher) 74 | 75 | Just a cosmetic alias to the hamcrest matcher :py:func:`is_not`. See :ref:`never`. 76 | 77 | 78 | for Spy methods 79 | --------------- 80 | 81 | .. py:class:: called 82 | 83 | Asserts a spy method was called:: 84 | 85 | assert_that(spy.method, called()) 86 | 87 | See :ref:`called`. 88 | 89 | .. py:method:: called.async_mode(timeout) 90 | 91 | The ``called`` assertion waits the corresponding invocation a maximum of `timeout` 92 | seconds. 93 | 94 | :param int timeout: how many second wait before assume assertion fails. 95 | 96 | :: 97 | 98 | assert_that(spy.method, called().async_mode(1)) 99 | 100 | See :ref:`async_mode`. 101 | 102 | 103 | .. py:method:: called.times(value) 104 | 105 | The spy method must be invoked `value` times to consider the assertion right. The 106 | `value` parameter may an integer or hamcrest matcher as well. 107 | 108 | :param value: how many times the method should be called. 109 | :type value: int or hamcrest Matcher 110 | 111 | :: 112 | 113 | assert_that(spy.method, called().times(less_that(3))) 114 | 115 | See :ref:`times`. 116 | 117 | .. py:method:: called.with_args(*args, **kargs) 118 | 119 | The spy method must be invoked with the given positional or/and named parameters. All 120 | of them may be literal values and hamcrest matchers. 121 | 122 | :: 123 | 124 | assert_that(spy.method, called().with_args("mary", greater_that(4))) 125 | 126 | See :ref:`with_args`. 127 | 128 | 129 | .. py:method:: called.with_some_args(**kargs) 130 | 131 | The spy method must be invoked with AT LEAST the given parameter values. It supports 132 | literal values and hamcrest matchers. 133 | 134 | :: 135 | 136 | assert_that(spy.method, called().with_some_args(name="mary")) 137 | 138 | See :ref:`with_some_args`. 139 | 140 | 141 | for properties 142 | -------------- 143 | 144 | .. py:class:: property_got() 145 | .. py:class:: property_set() 146 | .. py:method:: property_set.to(value) 147 | 148 | See :ref:`properties`. 149 | 150 | 151 | for mocks 152 | --------- 153 | 154 | .. py:class:: verify() 155 | 156 | Checks the given mock meets the given expectations. 157 | 158 | :: 159 | 160 | assert_that(mock, verify()) 161 | 162 | See :ref:`verify`. 163 | 164 | .. py:class:: any_order_verify() 165 | 166 | Checks the given mock meets the given expectations even when the invocation sequence 167 | has a different order to the expectations. 168 | 169 | :: 170 | 171 | assert_that(mock, any_order_verify()) 172 | 173 | See :ref:`verify`. 174 | 175 | 176 | Module level functions 177 | ====================== 178 | 179 | .. py:function:: assert_that(item, matcher) 180 | 181 | A convenient replace for the hamcrest `assert_that` method. See :ref:`sec assert_that`. 182 | 183 | 184 | .. py:function:: wait_that(item, matcher, reason='', delta=1, timeout=5) 185 | 186 | It test the `matcher` over `item` until it matches or fails after `timemout` seconds, 187 | polling the matcher each `delta` seconds. 188 | 189 | 190 | .. py:function:: method_returning(value) 191 | 192 | Creates an independent Stub method that returns the given value. It may be added to any 193 | object:: 194 | 195 | some.method = method_returning(20) 196 | 197 | See :ref:`ad-hoc methods`. 198 | 199 | 200 | .. py:function:: method_raising() 201 | 202 | Creates an independent Stub method that raises the given exception. It may be added to 203 | any object:: 204 | 205 | some.method = method_raising(ValueError) 206 | 207 | See :ref:`ad-hoc methods`. 208 | 209 | 210 | .. py:function:: set_default_behavior() 211 | 212 | Set the default behavior for undefined Stub methods. The built-in behavior is to return 213 | **None**. See :ref:`set_default_behavior`. 214 | 215 | 216 | .. Local Variables: 217 | .. coding: utf-8 218 | .. mode: rst 219 | .. mode: flyspell 220 | .. ispell-local-dictionary: "american" 221 | .. fill-column: 90 222 | .. End: 223 | -------------------------------------------------------------------------------- /docs/async-spies.rst: -------------------------------------------------------------------------------- 1 | .. _async_mode: 2 | 3 | Asynchronous spies 4 | ================== 5 | 6 | .. versionadded:: 1.5.1 7 | 8 | Sometimes interaction among your SUT class and their collaborators does not meet a 9 | synchronous behavior. That may happen when the SUT performs collaborator invocations in a 10 | different thread, or when the invocation pass across a message queue, publish/subscribe 11 | service, etc. 12 | 13 | Something like that: 14 | 15 | 16 | .. sourcecode:: python 17 | 18 | class Collaborator(object): 19 | def write(self, data): 20 | print("your code here") 21 | 22 | class SUT(object): 23 | def __init__(self, collaborator): 24 | self.collaborator = collaborator 25 | 26 | def some_method(self): 27 | thread.start_new_thread(self.collaborator.write, ("something",)) 28 | 29 | 30 | If you try to test your collaborator is called using a Spy, you will get a wrong behavior: 31 | 32 | 33 | .. sourcecode:: python 34 | 35 | # THE WRONG WAY 36 | class AsyncTests(unittest.TestCase): 37 | def test_wrong_try_to_test_an_async_invocation(self): 38 | # given 39 | spy = Spy(Collaborator) 40 | sut = SUT(spy) 41 | 42 | # when 43 | sut.some_method() 44 | 45 | # then 46 | assert_that(spy.write, called()) 47 | 48 | 49 | due to the ``called()`` assertion may happen before the ``write()`` invocation, although 50 | not always... 51 | 52 | You may be tempted to put a sleep before the assertion, but this is a bad solution. A 53 | right way to solve that issue is to use something like a barrier. The `threading.Event`__ 54 | may be used as a barrier. See this new test version: 55 | 56 | __ http://docs.python.org/2/library/threading.html#event-objects 57 | 58 | 59 | .. sourcecode:: python 60 | 61 | # THE DIRTY WAY 62 | class AsyncTests(unittest.TestCase): 63 | def test_an_async_invocation_with_barrier(self): 64 | # given 65 | barrier = threading.Event() 66 | with Spy(Collaborator) as spy: 67 | spy.write.attach(lambda *args: barrier.set) 68 | 69 | sut = SUT(spy) 70 | 71 | # when 72 | sut.some_method() 73 | barrier.wait(1) 74 | 75 | # then 76 | assert_that(spy.write, called()) 77 | 78 | 79 | The ``spy.write.attach()`` is part of the doublex stub-observer `mechanism`__, a 80 | way to run arbitrary code when stubbed methods are called. 81 | 82 | __ http://python-doublex.readthedocs.org/en/latest/reference.html#stub-observers 83 | 84 | That works because the ``called()`` assertion is performed only when the spy releases the 85 | barrier. If the ``write()`` invocation never happens, the ``barrier.wait()`` continues 86 | after 1 second but the test fails, as must do. When all is right, the barrier waits just 87 | the required time. 88 | 89 | Well, this mechanism is a doublex builtin (the ``async_mode`` matcher) since release 1.5.1 90 | providing the same behavior in a clearer way. The next is functionally equivalent to the 91 | listing just above: 92 | 93 | 94 | .. sourcecode:: python 95 | 96 | # THE DOUBLEX WAY 97 | class AsyncTests(unittest.TestCase): 98 | def test_test_an_async_invocation_with_doublex_async(self): 99 | # given 100 | spy = Spy(Collaborator) 101 | sut = SUT(spy) 102 | 103 | # when 104 | sut.some_method() 105 | 106 | # then 107 | assert_that(spy.write, called().async_mode(timeout=1)) 108 | 109 | 110 | .. Local Variables: 111 | .. coding: utf-8 112 | .. mode: rst 113 | .. mode: flyspell 114 | .. ispell-local-dictionary: "american" 115 | .. fill-column: 90 116 | .. End: 117 | -------------------------------------------------------------------------------- /docs/async-spies.rst.test: -------------------------------------------------------------------------------- 1 | .. _async_mode: 2 | 3 | Asynchronous spies 4 | ================== 5 | 6 | .. versionadded:: 1.5.1 7 | 8 | Sometimes interaction among your SUT class and their collaborators does not meet a 9 | synchronous behavior. That may happen when the SUT performs collaborator invocations in a 10 | different thread, or when the invocation pass across a message queue, publish/subscribe 11 | service, etc. 12 | 13 | Something like that: 14 | 15 | .. testcode:: 16 | .. sourcecode:: python 17 | 18 | class Collaborator(object): 19 | def write(self, data): 20 | print("your code here") 21 | 22 | class SUT(object): 23 | def __init__(self, collaborator): 24 | self.collaborator = collaborator 25 | 26 | def some_method(self): 27 | thread.start_new_thread(self.collaborator.write, ("something",)) 28 | 29 | 30 | If you try to test your collaborator is called using a Spy, you will get a wrong behavior: 31 | 32 | .. testcode:: 33 | .. sourcecode:: python 34 | 35 | # THE WRONG WAY 36 | class AsyncTests(unittest.TestCase): 37 | def test_wrong_try_to_test_an_async_invocation(self): 38 | # given 39 | spy = Spy(Collaborator) 40 | sut = SUT(spy) 41 | 42 | # when 43 | sut.some_method() 44 | 45 | # then 46 | assert_that(spy.write, called()) 47 | 48 | 49 | due to the ``called()`` assertion may happen before the ``write()`` invocation, although 50 | not always... 51 | 52 | You may be tempted to put a sleep before the assertion, but this is a bad solution. A 53 | right way to solve that issue is to use something like a barrier. The `threading.Event`__ 54 | may be used as a barrier. See this new test version: 55 | 56 | __ http://docs.python.org/2/library/threading.html#event-objects 57 | 58 | .. testcode:: 59 | .. sourcecode:: python 60 | 61 | # THE DIRTY WAY 62 | class AsyncTests(unittest.TestCase): 63 | def test_an_async_invocation_with_barrier(self): 64 | # given 65 | barrier = threading.Event() 66 | with Spy(Collaborator) as spy: 67 | spy.write.attach(lambda *args: barrier.set) 68 | 69 | sut = SUT(spy) 70 | 71 | # when 72 | sut.some_method() 73 | barrier.wait(1) 74 | 75 | # then 76 | assert_that(spy.write, called()) 77 | 78 | 79 | The ``spy.write.attach()`` is part of the doublex stub-observer `mechanism`__, a 80 | way to run arbitrary code when stubbed methods are called. 81 | 82 | __ http://python-doublex.readthedocs.org/en/latest/reference.html#stub-observers 83 | 84 | That works because the ``called()`` assertion is performed only when the spy releases the 85 | barrier. If the ``write()`` invocation never happens, the ``barrier.wait()`` continues 86 | after 1 second but the test fails, as must do. When all is right, the barrier waits just 87 | the required time. 88 | 89 | Well, this mechanism is a doublex builtin (the ``async_mode`` matcher) since release 1.5.1 90 | providing the same behavior in a clearer way. The next is functionally equivalent to the 91 | listing just above: 92 | 93 | .. testcode:: 94 | .. sourcecode:: python 95 | 96 | # THE DOUBLEX WAY 97 | class AsyncTests(unittest.TestCase): 98 | def test_test_an_async_invocation_with_doublex_async(self): 99 | # given 100 | spy = Spy(Collaborator) 101 | sut = SUT(spy) 102 | 103 | # when 104 | sut.some_method() 105 | 106 | # then 107 | assert_that(spy.write, called().async_mode(timeout=1)) 108 | 109 | 110 | .. Local Variables: 111 | .. coding: utf-8 112 | .. mode: rst 113 | .. mode: flyspell 114 | .. ispell-local-dictionary: "american" 115 | .. fill-column: 90 116 | .. End: 117 | -------------------------------------------------------------------------------- /docs/calls.rst: -------------------------------------------------------------------------------- 1 | calls: low-level access to invocation records 2 | --------------------------------------------- 3 | 4 | .. versionadded:: 1.6.3 5 | 6 | Invocation over spy methods are available in the ``calls`` attribute. You may use that to 7 | get invocation argument values and perform complex assertions (i.e: check invocations 8 | arguments were specific instances). However, you should prefer ``called()`` matcher 9 | assertions over this. An example: 10 | 11 | 12 | .. sourcecode:: python 13 | 14 | from doublex import Spy, assert_that, ANY_ARG, is_ 15 | 16 | class TheCollaborator(object): 17 | def method(self, *args, **kargs): 18 | pass 19 | 20 | with Spy(TheCollaborator) as spy: 21 | spy.method(ANY_ARG).returns(100) 22 | 23 | spy.method(1, 2, 3) 24 | spy.method(key=2, val=5) 25 | 26 | assert_that(spy.method.calls[0].args, is_((1, 2, 3))) 27 | assert_that(spy.method.calls[1].kargs, is_(dict(key=2, val=5))) 28 | assert_that(spy.method.calls[1].retval, is_(100)) 29 | 30 | 31 | .. Local Variables: 32 | .. coding: utf-8 33 | .. mode: rst 34 | .. mode: flyspell 35 | .. ispell-local-dictionary: "american" 36 | .. fill-columnd: 90 37 | .. End: 38 | -------------------------------------------------------------------------------- /docs/calls.rst.test: -------------------------------------------------------------------------------- 1 | calls: low-level access to invocation records 2 | --------------------------------------------- 3 | 4 | .. versionadded:: 1.6.3 5 | 6 | Invocation over spy methods are available in the ``calls`` attribute. You may use that to 7 | get invocation argument values and perform complex assertions (i.e: check invocations 8 | arguments were specific instances). However, you should prefer ``called()`` matcher 9 | assertions over this. An example: 10 | 11 | .. testcode:: calls 12 | .. sourcecode:: python 13 | 14 | from doublex import Spy, assert_that, ANY_ARG, is_ 15 | 16 | class TheCollaborator(object): 17 | def method(self, *args, **kargs): 18 | pass 19 | 20 | with Spy(TheCollaborator) as spy: 21 | spy.method(ANY_ARG).returns(100) 22 | 23 | spy.method(1, 2, 3) 24 | spy.method(key=2, val=5) 25 | 26 | assert_that(spy.method.calls[0].args, is_((1, 2, 3))) 27 | assert_that(spy.method.calls[1].kargs, is_(dict(key=2, val=5))) 28 | assert_that(spy.method.calls[1].retval, is_(100)) 29 | 30 | 31 | .. Local Variables: 32 | .. coding: utf-8 33 | .. mode: rst 34 | .. mode: flyspell 35 | .. ispell-local-dictionary: "american" 36 | .. fill-columnd: 90 37 | .. End: 38 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # doublex documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Dec 21 00:18:51 2013. 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 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = ['sphinx.ext.doctest'] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix of source filenames. 36 | source_suffix = '.rst' 37 | 38 | # The encoding of source files. 39 | #source_encoding = 'utf-8-sig' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'doublex' 46 | copyright = u'2013,2014, David Villa Alises' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | # The short X.Y version. 53 | version = '1.8' 54 | # The full version, including alpha/beta/rc tags. 55 | release = '1.8.1' 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all 72 | # documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | # If true, keep warnings as "system message" paragraphs in the built documents. 93 | #keep_warnings = False 94 | 95 | 96 | # -- Options for HTML output ---------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | html_theme = 'default' 101 | 102 | # Theme options are theme-specific and customize the look and feel of a theme 103 | # further. For a list of options available for each theme, see the 104 | # documentation. 105 | #html_theme_options = {} 106 | 107 | # Add any paths that contain custom themes here, relative to this directory. 108 | #html_theme_path = [] 109 | 110 | # The name for this set of Sphinx documents. If None, it defaults to 111 | # " v documentation". 112 | #html_title = None 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | #html_short_title = None 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | #html_logo = None 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | #html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | html_static_path = [] 130 | 131 | # Add any extra paths that contain custom files (such as robots.txt or 132 | # .htaccess) here, relative to this directory. These files are copied 133 | # directly to the root of the documentation. 134 | #html_extra_path = [] 135 | 136 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 137 | # using the given strftime format. 138 | #html_last_updated_fmt = '%b %d, %Y' 139 | 140 | # If true, SmartyPants will be used to convert quotes and dashes to 141 | # typographically correct entities. 142 | #html_use_smartypants = True 143 | 144 | # Custom sidebar templates, maps document names to template names. 145 | #html_sidebars = {} 146 | 147 | # Additional templates that should be rendered to pages, maps page names to 148 | # template names. 149 | #html_additional_pages = {} 150 | 151 | # If false, no module index is generated. 152 | #html_domain_indices = True 153 | 154 | # If false, no index is generated. 155 | #html_use_index = True 156 | 157 | # If true, the index is split into individual pages for each letter. 158 | #html_split_index = False 159 | 160 | # If true, links to the reST sources are added to the pages. 161 | #html_show_sourcelink = True 162 | 163 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 164 | #html_show_sphinx = True 165 | 166 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 167 | #html_show_copyright = True 168 | 169 | # If true, an OpenSearch description file will be output, and all pages will 170 | # contain a tag referring to it. The value of this option must be the 171 | # base URL from which the finished HTML is served. 172 | #html_use_opensearch = '' 173 | 174 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 175 | #html_file_suffix = None 176 | 177 | # Output file base name for HTML help builder. 178 | htmlhelp_basename = 'doublexdoc' 179 | 180 | 181 | # -- Options for LaTeX output --------------------------------------------- 182 | 183 | latex_elements = { 184 | # The paper size ('letterpaper' or 'a4paper'). 185 | #'papersize': 'letterpaper', 186 | 187 | # The font size ('10pt', '11pt' or '12pt'). 188 | #'pointsize': '10pt', 189 | 190 | # Additional stuff for the LaTeX preamble. 191 | #'preamble': '', 192 | } 193 | 194 | # Grouping the document tree into LaTeX files. List of tuples 195 | # (source start file, target name, title, 196 | # author, documentclass [howto, manual, or own class]). 197 | latex_documents = [ 198 | ('index', 'doublex.tex', u'doublex', 199 | u'David Villa Alises', 'manual'), 200 | ] 201 | 202 | # The name of an image file (relative to this directory) to place at the top of 203 | # the title page. 204 | #latex_logo = None 205 | 206 | # For "manual" documents, if this is true, then toplevel headings are parts, 207 | # not chapters. 208 | #latex_use_parts = False 209 | 210 | # If true, show page references after internal links. 211 | #latex_show_pagerefs = False 212 | 213 | # If true, show URL addresses after external links. 214 | #latex_show_urls = False 215 | 216 | # Documents to append as an appendix to all manuals. 217 | #latex_appendices = [] 218 | 219 | # If false, no module index is generated. 220 | #latex_domain_indices = True 221 | 222 | 223 | # -- Options for manual page output --------------------------------------- 224 | 225 | # One entry per manual page. List of tuples 226 | # (source start file, name, description, authors, manual section). 227 | man_pages = [ 228 | ('index', 'doublex', u'doublex', 229 | [u'David Villa Alises'], 1) 230 | ] 231 | 232 | # If true, show URL addresses after external links. 233 | #man_show_urls = False 234 | 235 | 236 | # -- Options for Texinfo output ------------------------------------------- 237 | 238 | # Grouping the document tree into Texinfo files. List of tuples 239 | # (source start file, target name, title, author, 240 | # dir menu entry, description, category) 241 | texinfo_documents = [ 242 | ('index', 'doublex', u'doublex', 243 | u'David Villa Alises', 'doublex', 'Python test doubles', 244 | 'Miscellaneous'), 245 | ] 246 | 247 | # Documents to append as an appendix to all manuals. 248 | #texinfo_appendices = [] 249 | 250 | # If false, no module index is generated. 251 | #texinfo_domain_indices = True 252 | 253 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 254 | #texinfo_show_urls = 'footnote' 255 | 256 | # If true, do not generate a @detailmenu in the "Top" node's menu. 257 | #texinfo_no_detailmenu = False 258 | 259 | 260 | # -- Options for Epub output ---------------------------------------------- 261 | 262 | # Bibliographic Dublin Core info. 263 | epub_title = u'doublex' 264 | epub_author = u'David Villa Alises' 265 | epub_publisher = u'David Villa Alises' 266 | epub_copyright = u'2013, David Villa Alises' 267 | 268 | # The basename for the epub file. It defaults to the project name. 269 | #epub_basename = u'doublex' 270 | 271 | # The HTML theme for the epub output. Since the default themes are not optimized 272 | # for small screen space, using the same theme for HTML and epub output is 273 | # usually not wise. This defaults to 'epub', a theme designed to save visual 274 | # space. 275 | #epub_theme = 'epub' 276 | 277 | # The language of the text. It defaults to the language option 278 | # or en if the language is not set. 279 | #epub_language = '' 280 | 281 | # The scheme of the identifier. Typical schemes are ISBN or URL. 282 | #epub_scheme = '' 283 | 284 | # The unique identifier of the text. This can be a ISBN number 285 | # or the project homepage. 286 | #epub_identifier = '' 287 | 288 | # A unique identification for the text. 289 | #epub_uid = '' 290 | 291 | # A tuple containing the cover image and cover page html template filenames. 292 | #epub_cover = () 293 | 294 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 295 | #epub_guide = () 296 | 297 | # HTML files that should be inserted before the pages created by sphinx. 298 | # The format is a list of tuples containing the path and title. 299 | #epub_pre_files = [] 300 | 301 | # HTML files shat should be inserted after the pages created by sphinx. 302 | # The format is a list of tuples containing the path and title. 303 | #epub_post_files = [] 304 | 305 | # A list of files that should not be packed into the epub file. 306 | #epub_exclude_files = [] 307 | 308 | # The depth of the table of contents in toc.ncx. 309 | #epub_tocdepth = 3 310 | 311 | # Allow duplicate toc entries. 312 | #epub_tocdup = True 313 | 314 | # Choose between 'default' and 'includehidden'. 315 | #epub_tocscope = 'default' 316 | 317 | # Fix unsupported image types using the PIL. 318 | #epub_fix_images = False 319 | 320 | # Scale large images. 321 | #epub_max_image_width = 0 322 | 323 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 324 | #epub_show_urls = 'inline' 325 | 326 | # If false, no index is generated. 327 | #epub_use_index = True 328 | -------------------------------------------------------------------------------- /docs/contents.rst.inc: -------------------------------------------------------------------------------- 1 | User's Guide 2 | ------------ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | install 8 | doubles 9 | reference 10 | methods 11 | properties 12 | delegates 13 | observers 14 | mimics 15 | async-spies 16 | inline-setup 17 | calls 18 | api 19 | pyDoubles 20 | release-notes 21 | 22 | 23 | Indices and tables 24 | ------------------ 25 | 26 | * :ref:`genindex` 27 | 28 | .. :ref:`modindex` 29 | .. :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/delegates.rst: -------------------------------------------------------------------------------- 1 | .. index:: 2 | single: Stub delegates 3 | 4 | .. _delegates: 5 | 6 | Stub delegates 7 | ============== 8 | 9 | The value returned by the stub may be delegated from a function, method or other 10 | callable... 11 | 12 | 13 | .. sourcecode:: python 14 | 15 | def get_user(): 16 | return "Freddy" 17 | 18 | with Stub() as stub: 19 | stub.user().delegates(get_user) 20 | stub.foo().delegates(lambda: "hello") 21 | 22 | assert_that(stub.user(), is_("Freddy")) 23 | assert_that(stub.foo(), is_("hello")) 24 | 25 | 26 | It may be delegated from iterables or generators too!: 27 | 28 | 29 | .. sourcecode:: python 30 | 31 | with Stub() as stub: 32 | stub.foo().delegates([1, 2, 3]) 33 | 34 | assert_that(stub.foo(), is_(1)) 35 | assert_that(stub.foo(), is_(2)) 36 | assert_that(stub.foo(), is_(3)) 37 | 38 | 39 | .. Local Variables: 40 | .. coding: utf-8 41 | .. mode: rst 42 | .. mode: flyspell 43 | .. ispell-local-dictionary: "american" 44 | .. fill-columnd: 90 45 | .. End: 46 | -------------------------------------------------------------------------------- /docs/delegates.rst.test: -------------------------------------------------------------------------------- 1 | .. index:: 2 | single: Stub delegates 3 | 4 | .. _delegates: 5 | 6 | Stub delegates 7 | ============== 8 | 9 | The value returned by the stub may be delegated from a function, method or other 10 | callable... 11 | 12 | .. testcode:: 13 | .. sourcecode:: python 14 | 15 | def get_user(): 16 | return "Freddy" 17 | 18 | with Stub() as stub: 19 | stub.user().delegates(get_user) 20 | stub.foo().delegates(lambda: "hello") 21 | 22 | assert_that(stub.user(), is_("Freddy")) 23 | assert_that(stub.foo(), is_("hello")) 24 | 25 | 26 | It may be delegated from iterables or generators too!: 27 | 28 | .. testcode:: 29 | .. sourcecode:: python 30 | 31 | with Stub() as stub: 32 | stub.foo().delegates([1, 2, 3]) 33 | 34 | assert_that(stub.foo(), is_(1)) 35 | assert_that(stub.foo(), is_(2)) 36 | assert_that(stub.foo(), is_(3)) 37 | 38 | 39 | .. Local Variables: 40 | .. coding: utf-8 41 | .. mode: rst 42 | .. mode: flyspell 43 | .. ispell-local-dictionary: "american" 44 | .. fill-columnd: 90 45 | .. End: 46 | -------------------------------------------------------------------------------- /docs/doubles.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Doubles 3 | ======= 4 | 5 | Some very basic examples are shown below. Remember that test doubles are created to be 6 | invoked by your `SUT `_, and a RealWorld™ 7 | test never directly invokes doubles. Here we do it that way, but just for simplicity. 8 | 9 | .. index:: 10 | single: Stub 11 | 12 | .. _stub: 13 | 14 | Stub 15 | ==== 16 | 17 | Hint: *Stubs tell you what you wanna hear.* 18 | 19 | A ``Stub`` is a double object that may be programmed to return specified values depending 20 | on method invocations and their arguments. You must use a context (the ``with`` keyword) 21 | for that. 22 | 23 | Invocations over the ``Stub`` must meet the collaborator interface: 24 | 25 | .. testsetup:: * 26 | 27 | import unittest 28 | 29 | class SomeException(Exception): 30 | pass 31 | 32 | class Collaborator(object): 33 | def hello(self): 34 | return "hello" 35 | 36 | def add(self, a, b): 37 | return a + b 38 | 39 | 40 | 41 | .. sourcecode:: python 42 | 43 | from doublex import Stub, ANY_ARG, assert_that, is_ 44 | 45 | class Collaborator: 46 | def hello(self): 47 | return "hello" 48 | 49 | def add(self, a, b): 50 | return a + b 51 | 52 | with Stub(Collaborator) as stub: 53 | stub.hello().raises(SomeException) 54 | stub.add(ANY_ARG).returns(4) 55 | 56 | assert_that(stub.add(2,3), is_(4)) 57 | 58 | 59 | If you call an nonexistent method you will get an ``AttributeError`` exception. 60 | 61 | 62 | .. sourcecode:: python 63 | 64 | >>> with Stub(Collaborator) as stub: 65 | ... stub.foo().returns(True) 66 | Traceback (most recent call last): 67 | ... 68 | AttributeError: 'Collaborator' object has no attribute 'foo' 69 | 70 | Wrong argument number: 71 | 72 | 73 | .. sourcecode:: python 74 | 75 | >>> with Stub(Collaborator) as stub: 76 | ... stub.hello(1).returns(2) # interface mismatch exception 77 | Traceback (most recent call last): 78 | ... 79 | TypeError: Collaborator.hello() takes exactly 1 argument (2 given) 80 | 81 | 82 | 83 | "free" Stub 84 | ----------- 85 | 86 | This allows you to invoke any method you want because it is not restricted to an interface. 87 | 88 | 89 | .. sourcecode:: python 90 | 91 | from doublex import Stub, assert_that, is_ 92 | 93 | # given 94 | with Stub() as stub: 95 | stub.foo('hi').returns(10) 96 | 97 | # when 98 | result = stub.foo('hi') 99 | 100 | # then 101 | assert_that(result, is_(10)) 102 | 103 | 104 | .. index:: 105 | single: Spy 106 | 107 | Spy 108 | === 109 | 110 | Hint: *Spies remember everything that happens to them.* 111 | 112 | Spy extends the Stub functionality allowing you to assert on the invocation it receives since its creation. 113 | 114 | Invocations over the Spy must meet the collaborator interface. 115 | 116 | 117 | .. sourcecode:: python 118 | 119 | from hamcrest import contains_string 120 | from doublex import Spy, assert_that, called 121 | 122 | class Sender: 123 | def say(self): 124 | return "hi" 125 | 126 | def send_mail(self, address, force=True): 127 | pass # [some amazing code] 128 | 129 | sender = Spy(Sender) 130 | 131 | sender.send_mail("john.doe@example.net") # right, Sender.send_mail interface support this 132 | 133 | assert_that(sender.send_mail, called()) 134 | assert_that(sender.send_mail, called().with_args("john.doe@example.net")) 135 | assert_that(sender.send_mail, called().with_args(contains_string("@example.net"))) 136 | 137 | sender.bar() # interface mismatch exception 138 | 139 | 140 | .. sourcecode:: python 141 | 142 | Traceback (most recent call last): 143 | ... 144 | AttributeError: 'Sender' object has no attribute 'bar' 145 | 146 | 147 | 148 | .. sourcecode:: python 149 | 150 | >>> sender = Spy(Sender) 151 | >>> sender.send_mail() 152 | Traceback (most recent call last): 153 | ... 154 | TypeError: Sender.send_mail() takes at least 2 arguments (1 given) 155 | 156 | 157 | .. sourcecode:: python 158 | 159 | >>> sender = Spy(Sender) 160 | >>> sender.send_mail(wrong=1) 161 | Traceback (most recent call last): 162 | ... 163 | TypeError: Sender.send_mail() got an unexpected keyword argument 'wrong' 164 | 165 | 166 | .. sourcecode:: python 167 | 168 | >>> sender = Spy(Sender) 169 | >>> sender.send_mail('foo', wrong=1) 170 | Traceback (most recent call last): 171 | ... 172 | TypeError: Sender.send_mail() got an unexpected keyword argument 'wrong' 173 | 174 | 175 | "free" Spy 176 | ---------- 177 | 178 | As the "free" Stub, this is a spy not restricted by a collaborator interface. 179 | 180 | 181 | .. sourcecode:: python 182 | 183 | from doublex import Stub, assert_that 184 | 185 | # given 186 | with Spy() as sender: 187 | sender.helo().returns("OK") 188 | 189 | # when 190 | sender.send_mail('hi') 191 | sender.send_mail('foo@bar.net') 192 | 193 | # then 194 | assert_that(sender.helo(), is_("OK")) 195 | assert_that(sender.send_mail, called()) 196 | assert_that(sender.send_mail, called().times(2)) 197 | assert_that(sender.send_mail, called().with_args('foo@bar.net')) 198 | 199 | .. index:: 200 | single: ProxySpy 201 | 202 | ProxySpy 203 | -------- 204 | 205 | Hint: *Proxy spies forward invocations to its actual instance.* 206 | 207 | The ``ProxySpy`` extends the ``Spy`` invoking the actual instance when the corresponding 208 | spy method is called 209 | 210 | .. warning:: 211 | Note the ``ProxySpy`` breaks isolation. It is not really a double. Therefore is always the worst double and the 212 | last resource. 213 | 214 | 215 | .. sourcecode:: python 216 | 217 | from doublex import ProxySpy, assert_that 218 | 219 | sender = ProxySpy(Sender()) # NOTE: It takes an instance (not class) 220 | 221 | assert_that(sender.say(), is_("hi")) 222 | assert_that(sender.say, called()) 223 | 224 | sender.say('boo!') # interface mismatch exception 225 | 226 | 227 | .. sourcecode:: python 228 | 229 | Traceback (most recent call last): 230 | ... 231 | TypeError: Sender.say() takes exactly 1 argument (2 given) 232 | 233 | 234 | .. index:: 235 | single: Mock 236 | 237 | .. _verify: 238 | 239 | Mock 240 | ==== 241 | 242 | Hint: *Mock forces the predefined script.* 243 | 244 | Mock objects may be programmed with a sequence of method calls. Later, the double must 245 | receive exactly the same sequence of invocations (including argument values). If the 246 | sequence does not match, an AssertionError is raised. "free" mocks are provided too: 247 | 248 | 249 | .. sourcecode:: python 250 | 251 | from doublex import Mock, assert_that, verify 252 | 253 | with Mock() as smtp: 254 | smtp.helo() 255 | smtp.mail(ANY_ARG) 256 | smtp.rcpt("bill@apple.com") 257 | smtp.data(ANY_ARG).returns(True).times(2) 258 | 259 | smtp.helo() 260 | smtp.mail("poormen@home.net") 261 | smtp.rcpt("bill@apple.com") 262 | smtp.data("somebody there?") 263 | smtp.data("I am afraid..") 264 | 265 | assert_that(smtp, verify()) 266 | 267 | 268 | ``verify()`` asserts invocation order. If your test does not require strict invocation 269 | order just use ``any_order_verify()`` matcher instead: 270 | 271 | 272 | .. sourcecode:: python 273 | 274 | from doublex import Mock, assert_that, any_order_verify 275 | 276 | with Mock() as mock: 277 | mock.foo() 278 | mock.bar() 279 | 280 | mock.bar() 281 | mock.foo() 282 | 283 | assert_that(mock, any_order_verify()) 284 | 285 | 286 | Programmed invocation sequence also may specify stubbed return values: 287 | 288 | 289 | .. sourcecode:: python 290 | 291 | from doublex import Mock, assert_that 292 | 293 | with Mock() as mock: 294 | mock.foo().returns(10) 295 | 296 | assert_that(mock.foo(), is_(10)) 297 | assert_that(mock, verify()) 298 | 299 | 300 | .. Local Variables: 301 | .. coding: utf-8 302 | .. mode: rst 303 | .. mode: flyspell 304 | .. ispell-local-dictionary: "american" 305 | .. fill-columnd: 90 306 | .. End: 307 | -------------------------------------------------------------------------------- /docs/doubles.rst.test: -------------------------------------------------------------------------------- 1 | ======= 2 | Doubles 3 | ======= 4 | 5 | Some very basic examples are shown below. Remember that test doubles are created to be 6 | invoked by your `SUT `_, and a RealWorld™ 7 | test never directly invokes doubles. Here we do it that way, but just for simplicity. 8 | 9 | .. index:: 10 | single: Stub 11 | 12 | .. _stub: 13 | 14 | Stub 15 | ==== 16 | 17 | Hint: *Stubs tell you what you wanna hear.* 18 | 19 | A ``Stub`` is a double object that may be programmed to return specified values depending 20 | on method invocations and their arguments. You must use a context (the ``with`` keyword) 21 | for that. 22 | 23 | Invocations over the ``Stub`` must meet the collaborator interface: 24 | 25 | .. testsetup:: * 26 | 27 | import unittest 28 | 29 | class SomeException(Exception): 30 | pass 31 | 32 | class Collaborator(object): 33 | def hello(self): 34 | return "hello" 35 | 36 | def add(self, a, b): 37 | return a + b 38 | 39 | 40 | .. testcode:: 41 | .. sourcecode:: python 42 | 43 | from doublex import Stub, ANY_ARG, assert_that, is_ 44 | 45 | class Collaborator: 46 | def hello(self): 47 | return "hello" 48 | 49 | def add(self, a, b): 50 | return a + b 51 | 52 | with Stub(Collaborator) as stub: 53 | stub.hello().raises(SomeException) 54 | stub.add(ANY_ARG).returns(4) 55 | 56 | assert_that(stub.add(2,3), is_(4)) 57 | 58 | 59 | If you call an nonexistent method you will get an ``AttributeError`` exception. 60 | 61 | .. doctest:: 62 | .. sourcecode:: python 63 | 64 | >>> with Stub(Collaborator) as stub: 65 | ... stub.foo().returns(True) 66 | Traceback (most recent call last): 67 | ... 68 | AttributeError: 'Collaborator' object has no attribute 'foo' 69 | 70 | Wrong argument number: 71 | 72 | .. doctest:: 73 | .. sourcecode:: python 74 | 75 | >>> with Stub(Collaborator) as stub: 76 | ... stub.hello(1).returns(2) # interface mismatch exception 77 | Traceback (most recent call last): 78 | ... 79 | TypeError: Collaborator.hello() takes exactly 1 argument (2 given) 80 | 81 | 82 | 83 | "free" Stub 84 | ----------- 85 | 86 | This allows you to invoke any method you want because it is not restricted to an interface. 87 | 88 | .. testcode:: 89 | .. sourcecode:: python 90 | 91 | from doublex import Stub, assert_that, is_ 92 | 93 | # given 94 | with Stub() as stub: 95 | stub.foo('hi').returns(10) 96 | 97 | # when 98 | result = stub.foo('hi') 99 | 100 | # then 101 | assert_that(result, is_(10)) 102 | 103 | 104 | .. index:: 105 | single: Spy 106 | 107 | Spy 108 | === 109 | 110 | Hint: *Spies remember everything that happens to them.* 111 | 112 | Spy extends the Stub functionality allowing you to assert on the invocation it receives since its creation. 113 | 114 | Invocations over the Spy must meet the collaborator interface. 115 | 116 | .. testcode:: 117 | .. sourcecode:: python 118 | 119 | from hamcrest import contains_string 120 | from doublex import Spy, assert_that, called 121 | 122 | class Sender: 123 | def say(self): 124 | return "hi" 125 | 126 | def send_mail(self, address, force=True): 127 | pass # [some amazing code] 128 | 129 | sender = Spy(Sender) 130 | 131 | sender.send_mail("john.doe@example.net") # right, Sender.send_mail interface support this 132 | 133 | assert_that(sender.send_mail, called()) 134 | assert_that(sender.send_mail, called().with_args("john.doe@example.net")) 135 | assert_that(sender.send_mail, called().with_args(contains_string("@example.net"))) 136 | 137 | sender.bar() # interface mismatch exception 138 | 139 | .. testoutput:: 140 | .. sourcecode:: python 141 | 142 | Traceback (most recent call last): 143 | ... 144 | AttributeError: 'Sender' object has no attribute 'bar' 145 | 146 | 147 | .. doctest:: 148 | .. sourcecode:: python 149 | 150 | >>> sender = Spy(Sender) 151 | >>> sender.send_mail() 152 | Traceback (most recent call last): 153 | ... 154 | TypeError: Sender.send_mail() takes at least 2 arguments (1 given) 155 | 156 | .. doctest:: 157 | .. sourcecode:: python 158 | 159 | >>> sender = Spy(Sender) 160 | >>> sender.send_mail(wrong=1) 161 | Traceback (most recent call last): 162 | ... 163 | TypeError: Sender.send_mail() got an unexpected keyword argument 'wrong' 164 | 165 | .. doctest:: 166 | .. sourcecode:: python 167 | 168 | >>> sender = Spy(Sender) 169 | >>> sender.send_mail('foo', wrong=1) 170 | Traceback (most recent call last): 171 | ... 172 | TypeError: Sender.send_mail() got an unexpected keyword argument 'wrong' 173 | 174 | 175 | "free" Spy 176 | ---------- 177 | 178 | As the "free" Stub, this is a spy not restricted by a collaborator interface. 179 | 180 | .. testcode:: 181 | .. sourcecode:: python 182 | 183 | from doublex import Stub, assert_that 184 | 185 | # given 186 | with Spy() as sender: 187 | sender.helo().returns("OK") 188 | 189 | # when 190 | sender.send_mail('hi') 191 | sender.send_mail('foo@bar.net') 192 | 193 | # then 194 | assert_that(sender.helo(), is_("OK")) 195 | assert_that(sender.send_mail, called()) 196 | assert_that(sender.send_mail, called().times(2)) 197 | assert_that(sender.send_mail, called().with_args('foo@bar.net')) 198 | 199 | .. index:: 200 | single: ProxySpy 201 | 202 | ProxySpy 203 | -------- 204 | 205 | Hint: *Proxy spies forward invocations to its actual instance.* 206 | 207 | The ``ProxySpy`` extends the ``Spy`` invoking the actual instance when the corresponding 208 | spy method is called 209 | 210 | .. warning:: 211 | Note the ``ProxySpy`` breaks isolation. It is not really a double. Therefore is always the worst double and the 212 | last resource. 213 | 214 | .. testcode:: 215 | .. sourcecode:: python 216 | 217 | from doublex import ProxySpy, assert_that 218 | 219 | sender = ProxySpy(Sender()) # NOTE: It takes an instance (not class) 220 | 221 | assert_that(sender.say(), is_("hi")) 222 | assert_that(sender.say, called()) 223 | 224 | sender.say('boo!') # interface mismatch exception 225 | 226 | .. testoutput:: 227 | .. sourcecode:: python 228 | 229 | Traceback (most recent call last): 230 | ... 231 | TypeError: Sender.say() takes exactly 1 argument (2 given) 232 | 233 | 234 | .. index:: 235 | single: Mock 236 | 237 | .. _verify: 238 | 239 | Mock 240 | ==== 241 | 242 | Hint: *Mock forces the predefined script.* 243 | 244 | Mock objects may be programmed with a sequence of method calls. Later, the double must 245 | receive exactly the same sequence of invocations (including argument values). If the 246 | sequence does not match, an AssertionError is raised. "free" mocks are provided too: 247 | 248 | .. testcode:: 249 | .. sourcecode:: python 250 | 251 | from doublex import ANY_ARG, Mock, assert_that, verify 252 | 253 | with Mock() as smtp: 254 | smtp.helo() 255 | smtp.mail(ANY_ARG) 256 | smtp.rcpt("bill@apple.com") 257 | smtp.data(ANY_ARG) 258 | smtp.data(ANY_ARG) 259 | 260 | smtp.helo() 261 | smtp.mail("poormen@home.net") 262 | smtp.rcpt("bill@apple.com") 263 | smtp.data("somebody there?") 264 | smtp.data("I am afraid..") 265 | 266 | assert_that(smtp, verify()) 267 | 268 | 269 | ``verify()`` asserts invocation order. If your test does not require strict invocation 270 | order just use ``any_order_verify()`` matcher instead: 271 | 272 | .. testcode:: 273 | .. sourcecode:: python 274 | 275 | from doublex import Mock, assert_that, any_order_verify 276 | 277 | with Mock() as mock: 278 | mock.foo() 279 | mock.bar() 280 | 281 | mock.bar() 282 | mock.foo() 283 | 284 | assert_that(mock, any_order_verify()) 285 | 286 | 287 | Programmed invocation sequence also may specify stubbed return values: 288 | 289 | .. testcode:: 290 | .. sourcecode:: python 291 | 292 | from doublex import Mock, assert_that 293 | 294 | with Mock() as mock: 295 | mock.foo().returns(10) 296 | 297 | assert_that(mock.foo(), is_(10)) 298 | assert_that(mock, verify()) 299 | 300 | 301 | .. Local Variables: 302 | .. coding: utf-8 303 | .. mode: rst 304 | .. mode: flyspell 305 | .. ispell-local-dictionary: "american" 306 | .. fill-columnd: 90 307 | .. End: 308 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | ======= 4 | doublex 5 | ======= 6 | 7 | .. include :: README.rst.inc 8 | .. include :: contents.rst.inc 9 | -------------------------------------------------------------------------------- /docs/inline-setup.rst: -------------------------------------------------------------------------------- 1 | Inline stubbing and mocking 2 | =========================== 3 | 4 | .. versionadded:: 1.8 - Proposed by `Carlos Ble `_ 5 | 6 | Several pyDoubles users ask for an alternative to set stubbing and mocking in a way 7 | similar to pyDoubles API, that is, instead of use the double context manager: 8 | 9 | .. sourcecode:: python 10 | 11 | with Stub() as stub: 12 | stub.foo(1).returns(100) 13 | 14 | with Mock() as mock: 15 | mock.bar(2).returns(50) 16 | 17 | You may invoke the :py:func:`when` and :py:func:`expect_call` functions to get the same 18 | setup. 19 | 20 | .. sourcecode:: python 21 | 22 | stub = Stub() 23 | when(stub).foo(1).returns(100) 24 | 25 | mock = Mock() 26 | expect_call(mock).bar(2).returns(50) 27 | 28 | 29 | Note that :py:func:`when` and :py:func:`expect_call` internally provide almost the same 30 | functionality. Two functions are provided only for test readability 31 | purposes. :py:func:`when` is intented for stubs, spies and proxyspies, and 32 | :py:func:`expect_call` is intented for mocks. 33 | 34 | 35 | .. Local Variables: 36 | .. coding: utf-8 37 | .. mode: rst 38 | .. mode: flyspell 39 | .. ispell-local-dictionary: "american" 40 | .. fill-column: 90 41 | .. End: 42 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Install 3 | ======= 4 | 5 | The simplest install method is to use ``pip``:: 6 | 7 | $ sudo pip install doublex 8 | 9 | Or you can download the last release tarball, available in the `PyPI project 10 | `_:: 11 | 12 | $ gunzip doublex-X.X.tar.gz 13 | $ tar xvf doublex-X.X.tar 14 | $ cd doublex-X.X/ 15 | 16 | To install:: 17 | 18 | $ sudo python setup.py install 19 | 20 | Or use pip:: 21 | 22 | $ sudo pip install doubles-X.X.tar.gz 23 | 24 | .. note:: 25 | doublex depends on PyHamcrest, but if you use ``pip`` it is automatically installed too. 26 | 27 | 28 | Also it is available as Debian and Ubuntu official packages, although may be outdated:: 29 | 30 | $ sudo apt-get install python-doublex 31 | 32 | 33 | .. Local Variables: 34 | .. coding: utf-8 35 | .. mode: rst 36 | .. mode: flyspell 37 | .. ispell-local-dictionary: "american" 38 | .. fill-columnd: 90 39 | .. End: 40 | -------------------------------------------------------------------------------- /docs/methods.rst: -------------------------------------------------------------------------------- 1 | .. _ad-hoc methods: 2 | 3 | Ad-hoc stub methods 4 | =================== 5 | 6 | Create a standalone stub method directly over any instance (even no doubles), with :py:func:`method_returning` and :py:func:`method_raising`: 7 | 8 | 9 | .. sourcecode:: python 10 | 11 | from doublex import method_returning, method_raising, assert_that 12 | 13 | collaborator = Collaborator() 14 | collaborator.foo = method_returning("bye") 15 | assert_that(collaborator.foo(), is_("bye")) 16 | 17 | collaborator.foo = method_raising(SomeException) 18 | collaborator.foo() # raises SomeException 19 | 20 | 21 | 22 | Traceback (most recent call last): 23 | ... 24 | SomeException 25 | 26 | 27 | 28 | .. Local Variables: 29 | .. coding: utf-8 30 | .. mode: rst 31 | .. mode: flyspell 32 | .. ispell-local-dictionary: "american" 33 | .. fill-columnd: 90 34 | .. End: 35 | -------------------------------------------------------------------------------- /docs/methods.rst.test: -------------------------------------------------------------------------------- 1 | .. _ad-hoc methods: 2 | 3 | Ad-hoc stub methods 4 | =================== 5 | 6 | Create a standalone stub method directly over any instance (even no doubles), with :py:func:`method_returning` and :py:func:`method_raising`: 7 | 8 | .. testcode:: 9 | .. sourcecode:: python 10 | 11 | from doublex import method_returning, method_raising, assert_that 12 | 13 | collaborator = Collaborator() 14 | collaborator.foo = method_returning("bye") 15 | assert_that(collaborator.foo(), is_("bye")) 16 | 17 | collaborator.foo = method_raising(SomeException) 18 | collaborator.foo() # raises SomeException 19 | 20 | .. testoutput:: 21 | 22 | Traceback (most recent call last): 23 | ... 24 | SomeException 25 | 26 | 27 | 28 | .. Local Variables: 29 | .. coding: utf-8 30 | .. mode: rst 31 | .. mode: flyspell 32 | .. ispell-local-dictionary: "american" 33 | .. fill-columnd: 90 34 | .. End: 35 | -------------------------------------------------------------------------------- /docs/mimics.rst: -------------------------------------------------------------------------------- 1 | .. index:: 2 | single: Mimic doubles 3 | 4 | Mimic doubles 5 | ============= 6 | 7 | Usually double instances behave as collaborator surrogates, but they do not expose the 8 | same class hierarchy, and usually this is pretty enough when the code uses "duck typing": 9 | 10 | .. testsetup:: mimic 11 | .. sourcecode:: python 12 | 13 | class A(object): 14 | pass 15 | 16 | class B(A): 17 | pass 18 | 19 | 20 | 21 | .. sourcecode:: python 22 | 23 | >>> from doublex import Spy 24 | >>> spy = Spy(B()) 25 | >>> isinstance(spy, Spy) 26 | True 27 | >>> isinstance(spy, B) 28 | False 29 | 30 | 31 | But some third party library DOES strict type checking using ``isinstance()``. That 32 | invalidates our doubles. For these cases you can use Mimic's. Mimic class decorates any 33 | double class to achieve full replacement instances (Liskov principle): 34 | 35 | 36 | .. sourcecode:: python 37 | 38 | >>> from doublex import Stub, Mimic 39 | >>> spy = Mimic(Spy, B) 40 | >>> isinstance(spy, B) 41 | True 42 | >>> isinstance(spy, A) 43 | True 44 | >>> isinstance(spy, Spy) 45 | True 46 | >>> isinstance(spy, Stub) 47 | True 48 | >>> isinstance(spy, object) 49 | True 50 | 51 | 52 | .. Local Variables: 53 | .. coding: utf-8 54 | .. mode: rst 55 | .. mode: flyspell 56 | .. ispell-local-dictionary: "american" 57 | .. fill-columnd: 90 58 | .. End: 59 | -------------------------------------------------------------------------------- /docs/mimics.rst.test: -------------------------------------------------------------------------------- 1 | .. index:: 2 | single: Mimic doubles 3 | 4 | Mimic doubles 5 | ============= 6 | 7 | Usually double instances behave as collaborator surrogates, but they do not expose the 8 | same class hierarchy, and usually this is pretty enough when the code uses "duck typing": 9 | 10 | .. testsetup:: mimic 11 | .. sourcecode:: python 12 | 13 | class A(object): 14 | pass 15 | 16 | class B(A): 17 | pass 18 | 19 | 20 | .. doctest:: mimic 21 | .. sourcecode:: python 22 | 23 | >>> from doublex import Spy 24 | >>> spy = Spy(B()) 25 | >>> isinstance(spy, Spy) 26 | True 27 | >>> isinstance(spy, B) 28 | False 29 | 30 | 31 | But some third party library DOES strict type checking using ``isinstance()``. That 32 | invalidates our doubles. For these cases you can use Mimic's. Mimic class decorates any 33 | double class to achieve full replacement instances (Liskov principle): 34 | 35 | .. doctest:: mimic 36 | .. sourcecode:: python 37 | 38 | >>> from doublex import Stub, Mimic 39 | >>> spy = Mimic(Spy, B) 40 | >>> isinstance(spy, B) 41 | True 42 | >>> isinstance(spy, A) 43 | True 44 | >>> isinstance(spy, Spy) 45 | True 46 | >>> isinstance(spy, Stub) 47 | True 48 | >>> isinstance(spy, object) 49 | True 50 | 51 | 52 | .. Local Variables: 53 | .. coding: utf-8 54 | .. mode: rst 55 | .. mode: flyspell 56 | .. ispell-local-dictionary: "american" 57 | .. fill-columnd: 90 58 | .. End: 59 | -------------------------------------------------------------------------------- /docs/observers.rst: -------------------------------------------------------------------------------- 1 | .. index:: 2 | single: Stub observers 3 | 4 | .. _observers: 5 | 6 | Stub observers 7 | ============== 8 | 9 | Stub observers allow you to execute extra code (similar to python-mock "side effects", but 10 | easier): 11 | 12 | 13 | .. sourcecode:: python 14 | 15 | class Observer(object): 16 | def __init__(self): 17 | self.state = None 18 | 19 | def notify(self, *args, **kargs): 20 | self.state = args[0] 21 | 22 | observer = Observer() 23 | stub = Stub() 24 | stub.foo.attach(observer.notify) 25 | stub.foo(2) 26 | 27 | assert_that(observer.state, is_(2)) 28 | 29 | 30 | .. Local Variables: 31 | .. coding: utf-8 32 | .. mode: rst 33 | .. mode: flyspell 34 | .. ispell-local-dictionary: "american" 35 | .. fill-columnd: 90 36 | .. End: 37 | -------------------------------------------------------------------------------- /docs/observers.rst.test: -------------------------------------------------------------------------------- 1 | .. index:: 2 | single: Stub observers 3 | 4 | .. _observers: 5 | 6 | Stub observers 7 | ============== 8 | 9 | Stub observers allow you to execute extra code (similar to python-mock "side effects", but 10 | easier): 11 | 12 | .. testcode:: 13 | .. sourcecode:: python 14 | 15 | class Observer(object): 16 | def __init__(self): 17 | self.state = None 18 | 19 | def notify(self, *args, **kargs): 20 | self.state = args[0] 21 | 22 | observer = Observer() 23 | stub = Stub() 24 | stub.foo.attach(observer.notify) 25 | stub.foo(2) 26 | 27 | assert_that(observer.state, is_(2)) 28 | 29 | 30 | .. Local Variables: 31 | .. coding: utf-8 32 | .. mode: rst 33 | .. mode: flyspell 34 | .. ispell-local-dictionary: "american" 35 | .. fill-columnd: 90 36 | .. End: 37 | -------------------------------------------------------------------------------- /docs/properties.rst: -------------------------------------------------------------------------------- 1 | .. index:: 2 | single: Properties 3 | 4 | .. _properties: 5 | 6 | Properties 7 | ========== 8 | 9 | **doublex** supports stub and spy properties in a pretty easy way in relation to other 10 | frameworks like python-mock. 11 | 12 | That requires two constraints: 13 | 14 | * It does not support "free" doubles. ie: you must give a collaborator in the constructor. 15 | * collaborator must be a new-style class. See the next example. 16 | 17 | 18 | Stubbing properties 19 | ------------------- 20 | 21 | 22 | .. sourcecode:: python 23 | 24 | from doublex import Spy, assert_that, is_ 25 | 26 | with Spy(Collaborator) as spy: 27 | spy.prop = 2 # stubbing 'prop' value 28 | 29 | assert_that(spy.prop, is_(2)) # double property getter invoked 30 | 31 | 32 | Spying properties 33 | ----------------- 34 | 35 | Continuing previous example: 36 | 37 | 38 | .. sourcecode:: python 39 | 40 | from doublex import Spy, assert_that, never 41 | from doublex import property_got, property_set 42 | 43 | class Collaborator(object): 44 | @property 45 | def prop(self): 46 | return 1 47 | 48 | @prop.setter 49 | def prop(self, value): 50 | pass 51 | 52 | spy = Spy(Collaborator) 53 | value = spy.prop 54 | 55 | assert_that(spy, property_got('prop')) # property 'prop' was read. 56 | 57 | spy.prop = 4 58 | spy.prop = 5 59 | spy.prop = 5 60 | 61 | assert_that(spy, property_set('prop')) # was set to any value 62 | assert_that(spy, property_set('prop').to(4)) 63 | assert_that(spy, property_set('prop').to(5).times(2)) 64 | assert_that(spy, never(property_set('prop').to(6))) 65 | 66 | 67 | Mocking properties 68 | ------------------ 69 | 70 | Getting property: 71 | 72 | 73 | .. sourcecode:: python 74 | 75 | from doublex import Mock, assert_that, verify 76 | 77 | with Mock(Collaborator) as mock: 78 | mock.prop 79 | 80 | mock.prop 81 | 82 | assert_that(mock, verify()) 83 | 84 | 85 | Setting property: 86 | 87 | 88 | .. sourcecode:: python 89 | 90 | from doublex import Mock, assert_that, verify 91 | 92 | with Mock(Collaborator) as mock: 93 | mock.prop = 5 94 | 95 | mock.prop = 5 96 | 97 | assert_that(mock, verify()) 98 | 99 | 100 | Using matchers: 101 | 102 | 103 | .. sourcecode:: python 104 | 105 | from hamcrest import all_of, greater_than, less_than 106 | from doublex import Mock, assert_that, verify 107 | 108 | with Mock(Collaborator) as mock: 109 | mock.prop = all_of(greater_than(8), less_than(12)) 110 | 111 | mock.prop = 10 112 | 113 | assert_that(mock, verify()) 114 | 115 | 116 | 117 | .. Local Variables: 118 | .. coding: utf-8 119 | .. mode: rst 120 | .. mode: flyspell 121 | .. ispell-local-dictionary: "american" 122 | .. fill-columnd: 90 123 | .. End: 124 | -------------------------------------------------------------------------------- /docs/properties.rst.test: -------------------------------------------------------------------------------- 1 | .. index:: 2 | single: Properties 3 | 4 | .. _properties: 5 | 6 | Properties 7 | ========== 8 | 9 | **doublex** supports stub and spy properties in a pretty easy way in relation to other 10 | frameworks like python-mock. 11 | 12 | That requires two constraints: 13 | 14 | * It does not support "free" doubles. ie: you must give a collaborator in the constructor. 15 | * collaborator must be a new-style class. See the next example. 16 | 17 | 18 | Stubbing properties 19 | ------------------- 20 | 21 | .. testcode:: stub-property 22 | .. sourcecode:: python 23 | 24 | from doublex import Spy, assert_that, is_ 25 | 26 | with Spy(Collaborator) as spy: 27 | spy.prop = 2 # stubbing 'prop' value 28 | 29 | assert_that(spy.prop, is_(2)) # double property getter invoked 30 | 31 | 32 | Spying properties 33 | ----------------- 34 | 35 | Continuing previous example: 36 | 37 | .. testcode:: spy-property 38 | .. sourcecode:: python 39 | 40 | from doublex import Spy, assert_that, never 41 | from doublex import property_got, property_set 42 | 43 | class Collaborator(object): 44 | @property 45 | def prop(self): 46 | return 1 47 | 48 | @prop.setter 49 | def prop(self, value): 50 | pass 51 | 52 | spy = Spy(Collaborator) 53 | value = spy.prop 54 | 55 | assert_that(spy, property_got('prop')) # property 'prop' was read. 56 | 57 | spy.prop = 4 58 | spy.prop = 5 59 | spy.prop = 5 60 | 61 | assert_that(spy, property_set('prop')) # was set to any value 62 | assert_that(spy, property_set('prop').to(4)) 63 | assert_that(spy, property_set('prop').to(5).times(2)) 64 | assert_that(spy, never(property_set('prop').to(6))) 65 | 66 | 67 | Mocking properties 68 | ------------------ 69 | 70 | Getting property: 71 | 72 | .. testcode:: spy-property 73 | .. sourcecode:: python 74 | 75 | from doublex import Mock, assert_that, verify 76 | 77 | with Mock(Collaborator) as mock: 78 | mock.prop 79 | 80 | mock.prop 81 | 82 | assert_that(mock, verify()) 83 | 84 | 85 | Setting property: 86 | 87 | .. testcode:: spy-property 88 | .. sourcecode:: python 89 | 90 | from doublex import Mock, assert_that, verify 91 | 92 | with Mock(Collaborator) as mock: 93 | mock.prop = 5 94 | 95 | mock.prop = 5 96 | 97 | assert_that(mock, verify()) 98 | 99 | 100 | Using matchers: 101 | 102 | .. testcode:: spy-property 103 | .. sourcecode:: python 104 | 105 | from hamcrest import all_of, greater_than, less_than 106 | from doublex import Mock, assert_that, verify 107 | 108 | with Mock(Collaborator) as mock: 109 | mock.prop = all_of(greater_than(8), less_than(12)) 110 | 111 | mock.prop = 10 112 | 113 | assert_that(mock, verify()) 114 | 115 | 116 | 117 | .. Local Variables: 118 | .. coding: utf-8 119 | .. mode: rst 120 | .. mode: flyspell 121 | .. ispell-local-dictionary: "american" 122 | .. fill-columnd: 90 123 | .. End: 124 | -------------------------------------------------------------------------------- /docs/pyDoubles.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | pyDoubles 3 | ========= 4 | 5 | **IMPORTANT: pyDoubles support is removed since version 1.9.** 6 | 7 | 8 | ``doublex`` started as a attempt to improve and simplify the codebase and API of the 9 | `pyDoubles `_ framework (by Carlos Ble). 10 | 11 | Respect to pyDoubles, ``doublex`` has these features: 12 | 13 | * Just hamcrest matchers (for all features). 14 | * Only ProxySpy requires an instance. Other doubles accept a class too, but they never 15 | instantiate it. 16 | * Properties may be stubbed and spied. 17 | * Stub observers: Notify arbitrary hooks when methods are invoked. Useful to add "side 18 | effects". 19 | * Stub delegates: Use callables, iterables or generators to create stub return values. 20 | * Mimic doubles: doubles that inherit the same collaborator subclasses. This provides full 21 | `LSP `_ for code that make 22 | strict type checking. 23 | 24 | ``doublex`` solves all the issues and supports all the feature requests notified in the 25 | pyDoubles issue tracker: 26 | 27 | * `assert keyword argument values `_ 28 | * `assert that a method is called exactly one `_ 29 | * `mocks impose invocation order `_ 30 | * `doubles have framework public API `_ 31 | * `stubs support keyword positional arguments `_ 32 | 33 | And some other features requested in the user group: 34 | 35 | * `doubles for properties `_ 36 | * `creating doubles without instantiating real class `_ 37 | * `using hamcrest with kwargs `_ 38 | 39 | 40 | ``doublex`` provides the pyDoubles API as a wrapper for easy transition to doublex for 41 | pyDoubles users. However, there are small differences. The bigger diference is that 42 | pyDoubles matchers are not supported anymore, although you may get the same feature using 43 | standard hamcrest matchers. Anyway, formally provided pyDoubles matchers are available as 44 | hamcrest aliases. 45 | 46 | ``doublex`` supports all the pyDoubles features and some more that can not be easily 47 | backported. If you are a pyDoubles user you can run your tests using doublex.pyDoubles 48 | module. However, we recommed the `native doublex API 49 | `_ for your new developments. 50 | 51 | In most cases the only required change in your code is the ``import`` sentence, that change from: 52 | 53 | .. sourcecode:: python 54 | 55 | import pyDoubles.framework.* 56 | 57 | to: 58 | 59 | .. sourcecode:: python 60 | 61 | from doublex.pyDoubles import * 62 | 63 | 64 | See the old pyDoubles documentation at ``__ (that was 65 | formerly available in the pydoubles.org site). 66 | 67 | 68 | 69 | .. Local Variables: 70 | .. coding: utf-8 71 | .. mode: rst 72 | .. mode: flyspell 73 | .. ispell-local-dictionary: "american" 74 | .. fill-columnd: 90 75 | .. End: 76 | -------------------------------------------------------------------------------- /docs/release-notes.rst: -------------------------------------------------------------------------------- 1 | Release notes / Changelog 2 | ========================= 3 | 4 | doublex 1.8.3 5 | ------------- 6 | 7 | * Fixed `issue 25`__ Python 3.5 type hints support. See `test`__. 8 | * Fixed `issue 23`__ Several tests failing because hamcrest.core.string_description.StringDescription is not a string anymore. 9 | 10 | __ https://bitbucket.org/DavidVilla/python-doublex/issues/25/support-from-python-35-type-hints-when 11 | __ https://bitbucket.org/DavidVilla/python-doublex/src/4b78564c9b3a8cc3fba170594c738ebbc9156996/doublex/test3/unit_tests.py?at=default&fileviewer=file-view-default#unit_tests.py-8 12 | __ https://bitbucket.org/DavidVilla/python-doublex/issues/23/doubles-test-failing 13 | 14 | 15 | doublex 1.8.2 16 | ------------- 17 | 18 | * Fixed `issue 12`__. :py:func:`returns_input` now may manage several parameters. See `test`__. 19 | * Fixed `issue 21`__. :py:func:`method_returning` and :py:func:`method_raising` are now spies. See `test`__. 20 | * Fixed `issue 22`__. See `test`__. 21 | * :py:func:`delegates` now accepts dictionaries. See `test`__. 22 | 23 | __ https://bitbucket.org/DavidVilla/python-doublex/issue/12 24 | __ https://bitbucket.org/DavidVilla/python-doublex/src/283adb2abef49be5f87bf58ccb83b3a313849c33/doublex/test/unit_tests.py?at=default#cl-116 25 | __ https://bitbucket.org/DavidVilla/python-doublex/issue/21 26 | __ https://bitbucket.org/DavidVilla/python-doublex/src/ace1edccb3fadbcf0992b5bf63f4e729ff877abd/doublex/test/unit_tests.py?at=default#cl-1461 27 | __ https://bitbucket.org/DavidVilla/python-doublex/issue/22 28 | __ https://bitbucket.org/DavidVilla/python-doublex/src/283adb2abef49be5f87bf58ccb83b3a313849c33/doublex/test/unit_tests.py?at=default#cl-1514 29 | __ https://bitbucket.org/DavidVilla/python-doublex/src/283adb2abef49be5f87bf58ccb83b3a313849c33/doublex/test/unit_tests.py?at=default#cl-1023 30 | 31 | 32 | 33 | doublex 1.8 34 | ----------- 35 | 36 | * NEW inline stubbing and mocking with :py:func:`when` and :py:func:`expect_call`. See 37 | `doc`__ and `tests`__. 38 | * Added support for mocking properties. See `doc`__ and `tests`__. 39 | * Testing with tox for Python 2.6, 2.7, 3.2 and 3.3. 40 | * Documentation now at ``_ 41 | 42 | __ http://python-doublex.readthedocs.org/en/latest/inline-setup.html 43 | __ https://bitbucket.org/DavidVilla/python-doublex/src/7b22f6d23455712b3e8894e40ae6272fc852762e/doublex/test/unit_tests.py?at=default#cl-1482 44 | __ http://python-doublex.readthedocs.org/en/latest/properties.html#mocking-properties 45 | __ https://bitbucket.org/DavidVilla/python-doublex/src/7b22f6d23455712b3e8894e40ae6272fc852762e/doublex/test/unit_tests.py?at=default#cl-1204 46 | 47 | 48 | doublex 1.7.2 49 | ------------- 50 | 51 | * Added support for varargs methods (\*args, \*\*kargs). Fixes `issue 14`__. 52 | * NEW tracer mechanism to log double invocations. See `test`__. 53 | * NEW module level ``wait_that()`` function. 54 | 55 | __ https://bitbucket.org/DavidVilla/python-doublex/issue/14/problem-spying-a-method-with-a-decorator 56 | __ https://bitbucket.org/DavidVilla/python-doublex/src/df2b3bda0eef64b5ddc6d6b3cc5a6380fb98e132/doublex/test/unit_tests.py?at=default#cl-1414 57 | 58 | 59 | doublex 1.7 60 | ----------- 61 | 62 | * NEW ``with_some_args()`` matcher to specify just relevant argument values in spy assertions. See `doc`__ and `tests`__. 63 | * NEW module level ``set_default_behavior()`` function to define behavior for non stubbed methods. Thanks to `Eduardo Ferro`__. See `doc`__ and `tests`__. 64 | 65 | __ http://python-doublex.readthedocs.org/en/latest/reference.html#with-some-args-asserting-just-relevant-arguments 66 | __ https://bitbucket.org/DavidVilla/python-doublex/src/147de5e7a52efae3c871c3065c082794b7272819/doublex/test/unit_tests.py?at=default#cl-1218 67 | __ https://bitbucket.org/eferro 68 | __ http://python-doublex.readthedocs.org/en/latest/reference.html#changing-default-stub-behavior 69 | __ https://bitbucket.org/DavidVilla/python-doublex/src/147de5e7a52efae3c871c3065c082794b7272819/doublex/test/unit_tests.py?at=default#cl-1243 70 | 71 | 72 | doublex 1.6.6 73 | ------------- 74 | 75 | * bug fix update: Fixes `issue 11`__. 76 | 77 | __ https://bitbucket.org/DavidVilla/python-doublex/issue/11/there-are-no-stub-empy_stub-in-the 78 | 79 | 80 | doublex 1.6.5 81 | ------------- 82 | 83 | * bug fix update: Fixes `issue 10`__. 84 | 85 | __ https://bitbucket.org/DavidVilla/python-doublex/issue/10/any_order_verify-fails-when-method-are 86 | 87 | 88 | doublex 1.6.4 89 | ------------- 90 | 91 | * Asynchronous spy assertion race condition bug fixed. 92 | * Reading double attributes returns collaborator.class attribute values by default. 93 | 94 | doublex 1.6.2 95 | ------------- 96 | 97 | * Invocation stubbed return value is now stored. 98 | 99 | * New low level spy API: double "calls" property provides access to invocations and their 100 | argument values. Each 'call' has an "args" sequence and "kargs dictionary". This 101 | provides support to perform individual assertions and direct access to invocation 102 | argument values. (see `test`__ and `doc`__). 103 | 104 | __ https://bitbucket.org/DavidVilla/python-doublex/src/ce8cdff71b8e3528380c305bf7d9ca75a64f6460/doublex/test/unit_tests.py?at=v1.6.2#cl-271 105 | __ http://python-doublex.readthedocs.org/en/latest/reference.html#calls-low-level-access-to-invocation-records 106 | 107 | 108 | doublex 1.6 109 | ----------- 110 | 111 | * First release supporting Python 3 (up to Python 3.2) [fixes `issue 7`__]. 112 | * Ad-hoc stub attributes (see `test`__). 113 | * Partial support for non native Python functions. 114 | * ProxySpy propagated stubbed invocations too (see `test`__). 115 | 116 | __ https://bitbucket.org/DavidVilla/python-doublex/issue/7 117 | __ https://bitbucket.org/DavidVilla/python-doublex/src/cb8ba0df2e024d602fed236bb5ed5a7ceee91b20/doublex/test/unit_tests.py?at=v1.6#cl-146 118 | __ https://bitbucket.org/DavidVilla/python-doublex/src/cb8ba0df2e024d602fed236bb5ed5a7ceee91b20/doublex/test/unit_tests.py?at=v1.6#cl-340 119 | 120 | 121 | doublex 1.5.1 122 | ------------- 123 | 124 | This release includes support for asynchronous spy assertions. See `this blog post 125 | `_ for the time being, soon in the official documentation. 126 | 127 | 128 | doublex/pyDoubles 1.5 129 | --------------------- 130 | 131 | Since this release, doublex supports the pyDoubles API by means a wrapper. See `pyDoubles `_ for details. 132 | 133 | In most cases the only required change in your code is the ``import`` sentence, that change from:: 134 | 135 | import pyDoubles.framework.* 136 | 137 | to:: 138 | 139 | from doublex.pyDoubles import * 140 | 141 | 142 | If you have problems migrating to the 1.5 release or migrating from pyDoubles to 143 | doublex, please ask for help in the `discussion forum 144 | `_ or in the `issue tracker 145 | `_. 146 | 147 | 148 | .. Local Variables: 149 | .. coding: utf-8 150 | .. mode: rst 151 | .. mode: flyspell 152 | .. ispell-local-dictionary: "american" 153 | .. fill-columnd: 90 154 | .. End: 155 | -------------------------------------------------------------------------------- /doctests/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -W 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | all: doctest 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " changes to make an overview of all changed/added/deprecated items" 35 | @echo " linkcheck to check all external links for integrity" 36 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 37 | 38 | clean: 39 | -rm -rf $(BUILDDIR)/* 40 | $(RM) -r _build *~ *.inc 41 | 42 | html: 43 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 44 | @echo 45 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 46 | 47 | dirhtml: 48 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 49 | @echo 50 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 51 | 52 | singlehtml: 53 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 54 | @echo 55 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 56 | 57 | pickle: 58 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 59 | @echo 60 | @echo "Build finished; now you can process the pickle files." 61 | 62 | json: 63 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 64 | @echo 65 | @echo "Build finished; now you can process the JSON files." 66 | 67 | htmlhelp: 68 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 69 | @echo 70 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 71 | ".hhp project file in $(BUILDDIR)/htmlhelp." 72 | 73 | qthelp: 74 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 75 | @echo 76 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 77 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 78 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/doubles.qhcp" 79 | @echo "To view the help file:" 80 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/doubles.qhc" 81 | 82 | devhelp: 83 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 84 | @echo 85 | @echo "Build finished." 86 | @echo "To view the help file:" 87 | @echo "# mkdir -p $$HOME/.local/share/devhelp/doubles" 88 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/doubles" 89 | @echo "# devhelp" 90 | 91 | epub: 92 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 93 | @echo 94 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 95 | 96 | latex: 97 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 98 | @echo 99 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 100 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 101 | "(use \`make latexpdf' here to do that automatically)." 102 | 103 | latexpdf: 104 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 105 | @echo "Running LaTeX files through pdflatex..." 106 | make -C $(BUILDDIR)/latex all-pdf 107 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 108 | 109 | text: 110 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 111 | @echo 112 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 113 | 114 | man: 115 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 116 | @echo 117 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 118 | 119 | changes: 120 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 121 | @echo 122 | @echo "The overview file is in $(BUILDDIR)/changes." 123 | 124 | linkcheck: 125 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 126 | @echo 127 | @echo "Link check complete; look for any errors in the above output " \ 128 | "or in $(BUILDDIR)/linkcheck/output.txt." 129 | 130 | %.rst.inc: ../docs/%.rst.test 131 | sed 's/^.. sourcecode:: python//g' $< > $@ 132 | 133 | GENERATED=doubles.rst.inc reference.rst.inc methods.rst.inc properties.rst.inc delegates.rst.inc observers.rst.inc mimics.rst.inc async-spies.rst.inc calls.rst.inc 134 | 135 | 136 | doctest: $(GENERATED) 137 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 138 | @echo "Testing of doctests in the sources finished, look at the " \ 139 | "results in $(BUILDDIR)/doctest/output.txt." 140 | -------------------------------------------------------------------------------- /doctests/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # doubles documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Feb 27 13:08:19 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # ensures that somemodule can be imported 17 | sys.path.append(os.path.dirname(__file__)) 18 | 19 | # -- General configuration ----------------------------------------------------- 20 | 21 | # If your documentation needs a minimal Sphinx version, state it here. 22 | #needs_sphinx = '1.0' 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be extensions 25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 26 | extensions = ['sphinx.ext.doctest'] 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ['_templates'] 30 | 31 | # The suffix of source filenames. 32 | source_suffix = '.rst' 33 | 34 | # The encoding of source files. 35 | #source_encoding = 'utf-8-sig' 36 | 37 | # The master toctree document. 38 | master_doc = 'index' 39 | 40 | # General information about the project. 41 | project = u'doublex' 42 | copyright = u'2013, David Villa Alises' 43 | 44 | # The version info for the project you're documenting, acts as replacement for 45 | # |version| and |release|, also used in various other places throughout the 46 | # built documents. 47 | # 48 | # The short X.Y version. 49 | version = '1.7' 50 | # The full version, including alpha/beta/rc tags. 51 | release = '1.7.2' 52 | 53 | # The language for content autogenerated by Sphinx. Refer to documentation 54 | # for a list of supported languages. 55 | #language = None 56 | 57 | # There are two options for replacing |today|: either, you set today to some 58 | # non-false value, then it is used: 59 | #today = '' 60 | # Else, today_fmt is used as the format for a strftime call. 61 | #today_fmt = '%B %d, %Y' 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | exclude_patterns = ['_build'] 66 | 67 | # The reST default role (used for this markup: `text`) to use for all documents. 68 | #default_role = None 69 | 70 | # If true, '()' will be appended to :func: etc. cross-reference text. 71 | #add_function_parentheses = True 72 | 73 | # If true, the current module name will be prepended to all description 74 | # unit titles (such as .. function::). 75 | #add_module_names = True 76 | 77 | # If true, sectionauthor and moduleauthor directives will be shown in the 78 | # output. They are ignored by default. 79 | #show_authors = False 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'sphinx' 83 | 84 | # A list of ignored prefixes for module index sorting. 85 | #modindex_common_prefix = [] 86 | 87 | 88 | # -- Options for HTML output --------------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | # html_theme_path = '.' 93 | # html_theme = 'python_mock_theme' 94 | 95 | # Theme options are theme-specific and customize the look and feel of a theme 96 | # further. For a list of options available for each theme, see the 97 | # documentation. 98 | html_theme_options = { 99 | } 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = [] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | html_show_sphinx = False 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | html_show_copyright = False 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'doublexdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'doublex.tex', u'doublex Documentation', 182 | u'David Villa Alises', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'doubles', u'doubles Documentation', 215 | [u'Gary Bernhardt'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /doctests/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | 3 | test 4 | -------------------------------------------------------------------------------- /doctests/test.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. include :: doubles.rst.inc 4 | .. include :: reference.rst.inc 5 | .. include :: methods.rst.inc 6 | .. include :: properties.rst.inc 7 | .. include :: delegates.rst.inc 8 | .. include :: observers.rst.inc 9 | .. include :: mimics.rst.inc 10 | .. include :: async-spies.rst.inc 11 | .. include :: calls.rst.inc 12 | -------------------------------------------------------------------------------- /doublex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 71 | 83 | X 96 | X 109 | 110 | 111 | -------------------------------------------------------------------------------- /doublex/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8; tab-width:4; mode:python -*- 2 | 3 | from .doubles import * 4 | from .matchers import * 5 | from .tracer import Tracer 6 | from .internal import WrongApiUsage 7 | 8 | try: 9 | from ._version import * 10 | except ImportError: 11 | pass 12 | 13 | def set_default_behavior(double, func): 14 | double._default_behavior = func 15 | 16 | 17 | def when(double): 18 | if not isinstance(double, Stub): 19 | raise WrongApiUsage("when() takes a double, '%s' given" % double) 20 | 21 | if isinstance(double, Mock): 22 | raise WrongApiUsage("when() takes a stub or spy. Use expect_call() for mocks") 23 | 24 | return double._activate_next() 25 | 26 | 27 | def expect_call(mock): 28 | if not isinstance(mock, Mock): 29 | raise WrongApiUsage("expect_call() takes a mock, '%s' given" % mock) 30 | 31 | return mock._activate_next() 32 | -------------------------------------------------------------------------------- /doublex/doubles.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8; tab-width:4; mode:python -*- 2 | 3 | # doublex 4 | # 5 | # Copyright © 2012, 2013 David Villa Alises 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | 21 | 22 | import inspect 23 | from typing import Generic 24 | 25 | import hamcrest 26 | 27 | from .internal import (ANY_ARG, OperationList, Method, MockBase, SpyBase, 28 | AttributeFactory, WrongApiUsage) 29 | from .proxy import create_proxy, get_class 30 | from .matchers import MockIsExpectedInvocation 31 | 32 | 33 | __all__ = ['Stub', 'Spy', 'ProxySpy', 'Mock', 'Mimic', 34 | 'method_returning', 'method_raising', 35 | 'ANY_ARG'] 36 | 37 | 38 | class Stub(object): 39 | _default_behavior = lambda x: None 40 | _new_attr_hooks = [] 41 | 42 | def __new__(cls, collaborator=None): 43 | '''Creates a fresh class clone per instance. This is required due to 44 | ad-hoc stub properties are class attributes''' 45 | klass = cls._clone_class() 46 | return object.__new__(klass) 47 | 48 | @classmethod 49 | def _clone_class(cls): 50 | return type(cls.__name__, (cls,), dict(cls.__dict__)) 51 | 52 | def __init__(self, collaborator=None): 53 | self._proxy = create_proxy(collaborator) 54 | self._stubs = OperationList() 55 | self._setting_up = False 56 | self._new_attr_hooks = self._new_attr_hooks[:] 57 | self._deactivate = False 58 | self.__class__.__setattr__ = self.__setattr__hook 59 | 60 | def _activate_next(self): 61 | self.__enter__() 62 | self._deactivate = True 63 | return self 64 | 65 | def __enter__(self): 66 | self._setting_up = True 67 | return self 68 | 69 | def __exit__(self, *args): 70 | self._setting_up = False 71 | 72 | def _manage_invocation(self, invocation): 73 | self._proxy.assure_signature_matches(invocation) 74 | 75 | if self._setting_up: 76 | self._stubs.append(invocation) 77 | return invocation 78 | 79 | self._prepare_invocation(invocation) 80 | 81 | stubbed_retval = self._default_behavior() 82 | if invocation in self._stubs: 83 | stubbed = self._stubs.lookup(invocation) 84 | stubbed_retval = stubbed._apply_stub(invocation) 85 | 86 | actual_retval = self._perform_invocation(invocation) 87 | 88 | retval = stubbed_retval if stubbed_retval is not None else actual_retval 89 | invocation.context.retval = retval 90 | return retval 91 | 92 | def _prepare_invocation(self, invocation): 93 | pass 94 | 95 | def _perform_invocation(self, invocation): 96 | return None 97 | 98 | def __getattr__(self, key): 99 | AttributeFactory.create(self, key) 100 | return object.__getattribute__(self, key) 101 | 102 | def __setattr__hook(self, key, value): 103 | if key in self.__dict__: 104 | object.__setattr__(self, key, value) 105 | return 106 | 107 | try: 108 | AttributeFactory.create(self, key) 109 | except AttributeError: 110 | # collaborator has not attribute 'key', creating it ad-hoc 111 | pass 112 | 113 | # descriptor protocol compliant 114 | object.__setattr__(self, key, value) 115 | 116 | def _classname(self): 117 | name = self._proxy.collaborator_classname() 118 | return name or self.__class__.__name__ 119 | 120 | 121 | class Spy(Stub, SpyBase): 122 | def __init__(self, collaborator=None): 123 | self._recorded = OperationList() 124 | super(Spy, self).__init__(collaborator) 125 | 126 | def _prepare_invocation(self, invocation): 127 | self._recorded.append(invocation) 128 | 129 | def _received_invocation(self, invocation, times, cmp_pred=None): 130 | return hamcrest.is_(times).matches( 131 | self._recorded.count(invocation, cmp_pred)) 132 | 133 | def _get_invocations_to(self, name): 134 | return [i for i in self._recorded 135 | if self._proxy.same_method(name, i.name)] 136 | 137 | 138 | class ProxySpy(Spy): 139 | def __init__(self, collaborator): 140 | self._assure_is_instance(collaborator) 141 | super(ProxySpy, self).__init__(collaborator) 142 | 143 | def _assure_is_instance(self, thing): 144 | if thing is None or inspect.isclass(thing): 145 | raise TypeError("ProxySpy takes an instance (got %s instead)" % thing) 146 | 147 | def _perform_invocation(self, invocation): 148 | return invocation._apply_on_collaborator() 149 | 150 | 151 | class Mock(Spy, MockBase): 152 | def _prepare_invocation(self, invocation): 153 | hamcrest.assert_that(self, MockIsExpectedInvocation(invocation)) 154 | super(Mock, self)._prepare_invocation(invocation) 155 | 156 | 157 | def Mimic(double, collab): 158 | def __getattribute__hook(self, key): 159 | if key in ['__class__', '__dict__', 160 | '_get_method', '_methods'] or \ 161 | key in [x[0] for x in inspect.getmembers(double)] or \ 162 | key in self.__dict__: 163 | return object.__getattribute__(self, key) 164 | 165 | return self._get_method(key) 166 | 167 | def _get_method(self, key): 168 | if key not in list(self._methods.keys()): 169 | typename = self._proxy.get_attr_typename(key) 170 | if typename not in ['instancemethod', 'function', 'method']: 171 | raise WrongApiUsage( 172 | "Mimic does not support attribute '%s' (type '%s')" % (key, typename)) 173 | 174 | method = Method(self, key) 175 | self._methods[key] = method 176 | 177 | return self._methods[key] 178 | 179 | assert issubclass(double, Stub), \ 180 | "Mimic() takes a double class as first argument (got %s instead)" & double 181 | 182 | collab_class = get_class(collab) 183 | base_classes = tuple(base for base in collab_class.__bases__ if base is not Generic) 184 | generated_class = type( 185 | "Mimic_%s_for_%s" % (double.__name__, collab_class.__name__), 186 | (double, collab_class) + base_classes, 187 | dict(_methods = {}, 188 | __getattribute__ = __getattribute__hook, 189 | _get_method = _get_method)) 190 | return generated_class(collab) 191 | 192 | 193 | def method_returning(value): 194 | with Spy() as spy: 195 | method = Method(spy, 'orphan') 196 | method(ANY_ARG).returns(value) 197 | return method 198 | 199 | 200 | def method_raising(exception): 201 | with Spy() as spy: 202 | method = Method(spy, 'orphan') 203 | method(ANY_ARG).raises(exception) 204 | return method 205 | -------------------------------------------------------------------------------- /doublex/matchers.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8; tab-width:4; mode:python -*- 2 | 3 | # doublex 4 | # 5 | # Copyright © 2012,2013 David Villa Alises 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | 21 | import sys 22 | import time 23 | import hamcrest 24 | from hamcrest.core.matcher import Matcher 25 | from hamcrest.core.base_matcher import BaseMatcher 26 | from hamcrest import is_, instance_of 27 | 28 | from .internal import ( 29 | Method, InvocationContext, ANY_ARG, MockBase, SpyBase, 30 | PropertyGet, PropertySet, WrongApiUsage, Invocation) 31 | 32 | __all__ = ['called', 33 | 'never', 34 | 'verify', 'any_order_verify', 35 | 'property_got', 'property_set', 36 | 'assert_that', 'wait_that', 37 | 'is_', 'instance_of'] 38 | 39 | 40 | # just hamcrest aliases 41 | at_least = hamcrest.greater_than_or_equal_to 42 | at_most = hamcrest.less_than_or_equal_to 43 | any_time = hamcrest.greater_than(0) 44 | 45 | 46 | class MatcherRequiredError(Exception): 47 | pass 48 | 49 | 50 | def assert_that(actual, matcher=None, reason=''): 51 | if matcher and not isinstance(matcher, Matcher): 52 | raise MatcherRequiredError("%s should be a hamcrest Matcher" % str(matcher)) 53 | return hamcrest.assert_that(actual, matcher, reason) 54 | 55 | 56 | def wait_that(actual, matcher, reason='', delta=1, timeout=5): 57 | ''' 58 | Poll the given matcher each 'delta' seconds until 'matcher' 59 | matches 'actual' or 'timeout' is reached. 60 | ''' 61 | exc = None 62 | init = time.time() 63 | timeout_reached = False 64 | while 1: 65 | try: 66 | if time.time() - init > timeout: 67 | timeout_reached = True 68 | break 69 | 70 | assert_that(actual, matcher, reason) 71 | break 72 | 73 | except AssertionError as e: 74 | time.sleep(delta) 75 | exc = e 76 | 77 | if timeout_reached: 78 | msg = exc.args[0] + ' after {0} seconds'.format(timeout) 79 | exc.args = msg, 80 | raise exc 81 | 82 | 83 | class OperationMatcher(BaseMatcher): 84 | pass 85 | 86 | 87 | class MethodCalled(OperationMatcher): 88 | def __init__(self, context=None, times=any_time): 89 | self.context = context or InvocationContext(ANY_ARG) 90 | self._times = times 91 | self._async_timeout = None 92 | 93 | def _matches(self, method): 94 | self._assure_is_spied_method(method) 95 | self.method = method 96 | if not self._async_timeout: 97 | return method._was_called(self.context, self._times) 98 | 99 | if self._async_timeout: 100 | if self._times != any_time: 101 | raise WrongApiUsage("'times' and 'async_mode' are exclusive") 102 | self.method._event.wait(self._async_timeout) 103 | 104 | return method._was_called(self.context, self._times) 105 | 106 | def _assure_is_spied_method(self, method): 107 | if not isinstance(method, Method) or not isinstance(method.double, SpyBase): 108 | raise WrongApiUsage("takes a spy method (got %s instead)" % method) 109 | 110 | def describe_to(self, description): 111 | description.append_text('these calls:\n') 112 | description.append_text(self.method._show(indent=10)) 113 | description.append_text(str(self.context)) 114 | if self._times != any_time: 115 | description.append_text(' -- times: %s' % self._times) 116 | 117 | def describe_mismatch(self, actual, description): 118 | description.append_text("calls that actually ocurred were:\n") 119 | description.append_text(self.method.double._recorded.show(indent=10)) 120 | 121 | def with_args(self, *args, **kargs): 122 | self.context.update_args(args, kargs) 123 | return self 124 | 125 | def with_some_args(self, **kargs): 126 | self.context.update_args(tuple(), kargs) 127 | self.context.check_some_args = True 128 | return self 129 | 130 | def async_mode(self, timeout): 131 | self._async_timeout = timeout 132 | return self 133 | 134 | def times(self, n): 135 | self._times = n 136 | return self 137 | 138 | 139 | # backward compatibility 140 | if sys.version_info < (3, 7): 141 | setattr(MethodCalled, 'async', MethodCalled.async_mode) 142 | 143 | 144 | def called(): 145 | return MethodCalled() 146 | 147 | 148 | class never(BaseMatcher): 149 | def __init__(self, matcher): 150 | if not isinstance(matcher, OperationMatcher): 151 | raise WrongApiUsage( 152 | "takes called/called_with instance (got %s instead)" % matcher) 153 | self.matcher = matcher 154 | 155 | def _matches(self, item): 156 | return not self.matcher.matches(item) 157 | 158 | def describe_to(self, description): 159 | description.append_text('none of ').append_description_of(self.matcher) 160 | 161 | def describe_mismatch(self, actual, description): 162 | self.matcher.describe_mismatch(actual, description) 163 | 164 | 165 | class MockIsExpectedInvocation(BaseMatcher): 166 | 'assert the invocation is a mock expectation' 167 | def __init__(self, invocation): 168 | self.invocation = invocation 169 | 170 | def _matches(self, mock): 171 | self.mock = mock 172 | return self.invocation in mock._stubs 173 | 174 | def describe_to(self, description): 175 | description.append_text("these calls:\n") 176 | description.append_text(self.mock._stubs.show(indent=10)) 177 | 178 | def describe_mismatch(self, actual, description): 179 | description.append_text("this call was not expected:\n") 180 | description.append_text(self.invocation._show(indent=10)) 181 | 182 | 183 | class verify(BaseMatcher): 184 | def _matches(self, mock): 185 | if not isinstance(mock, MockBase): 186 | raise WrongApiUsage("takes Mock instance (got %s instead)" % mock) 187 | 188 | self.mock = mock 189 | return self._expectations_match() 190 | 191 | def _expectations_match(self): 192 | return self.mock._stubs == self.mock._recorded 193 | 194 | def describe_to(self, description): 195 | description.append_text("these calls:\n") 196 | description.append_text(self.mock._stubs.show(indent=10)) 197 | 198 | def describe_mismatch(self, actual, description): 199 | description.append_text('calls that actually ocurred were:\n') 200 | description.append_text(self.mock._recorded.show(indent=10)) 201 | 202 | 203 | class any_order_verify(verify): 204 | def _expectations_match(self): 205 | return sorted(self.mock._stubs) == sorted(self.mock._recorded) 206 | 207 | 208 | class property_got(OperationMatcher): 209 | def __init__(self, propname, times=any_time): 210 | super(property_got, self).__init__() 211 | self.propname = propname 212 | self._times = times 213 | 214 | def _matches(self, double): 215 | self.double = double 216 | self.operation = PropertyGet(self.double, self.propname) 217 | return double._received_invocation( 218 | self.operation, 1, cmp_pred=Invocation.__eq__) 219 | 220 | def times(self, n): 221 | self._times = n 222 | return self 223 | 224 | def describe_to(self, description): 225 | description.append_text('these calls:\n') 226 | description.append_text(self.operation._show(indent=10)) 227 | if self._times != any_time: 228 | description.append_text(' -- times: %s' % self._times) 229 | 230 | def describe_mismatch(self, actual, description): 231 | description.append_text('calls that actually ocurred were:\n') 232 | description.append_text(self.double._recorded.show(indent=10)) 233 | 234 | 235 | class property_set(OperationMatcher): 236 | def __init__(self, property_name, value=hamcrest.anything(), times=any_time): 237 | super(property_set, self).__init__() 238 | self.property_name = property_name 239 | self.value = value 240 | self._times = times 241 | 242 | def _matches(self, double): 243 | self.double = double 244 | self.operation = PropertySet(self.double, self.property_name, self.value) 245 | return self.double._received_invocation( 246 | self.operation, self._times, cmp_pred=Invocation.__eq__) 247 | 248 | def to(self, value): 249 | self.value = value 250 | return self 251 | 252 | def times(self, n): 253 | self._times = n 254 | return self 255 | 256 | def describe_to(self, description): 257 | description.append_text('these calls:\n') 258 | description.append_text(self.operation._show(indent=10)) 259 | if self._times != any_time: 260 | description.append_text(' -- times: %s' % self._times) 261 | 262 | def describe_mismatch(self, actual, description): 263 | description.append_text('calls that actually ocurred were:\n') 264 | description.append_text(self.double._recorded.show(indent=10)) 265 | -------------------------------------------------------------------------------- /doublex/proxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8; tab-width:4; mode:python -*- 2 | 3 | # doublex 4 | # 5 | # Copyright © 2012,2013 David Villa Alises 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | import inspect 21 | from inspect import getcallargs 22 | 23 | from .internal import ANY_ARG 24 | 25 | 26 | def get_func(func): 27 | return func.__func__ 28 | 29 | 30 | def create_proxy(collaborator): 31 | if collaborator is None: 32 | return DummyProxy() 33 | 34 | return CollaboratorProxy(collaborator) 35 | 36 | 37 | class Proxy(object): 38 | def assure_signature_matches(self, invocation): 39 | pass 40 | 41 | def collaborator_classname(self): 42 | return None 43 | 44 | def get_signature(self, method_name): 45 | if self.is_property(method_name) or self.is_namedtuple_field(method_name): 46 | return PropertySignature(self, method_name) 47 | 48 | if not self.is_method_or_func(method_name): 49 | return BuiltinSignature(self, method_name) 50 | 51 | return MethodSignature(self, method_name) 52 | 53 | def is_property(self, attr_name): 54 | attr = getattr(self.collaborator_class, attr_name) 55 | return isinstance(attr, property) 56 | 57 | def collaborator_is_namedtuple(self): 58 | return issubclass(self.collaborator_class, tuple) and hasattr(self.collaborator_class, '_fields') 59 | 60 | def is_namedtuple_field(self, attr_name): 61 | attr = getattr(self.collaborator_class, attr_name) 62 | return self.collaborator_is_namedtuple() and type(attr).__name__ == '_tuplegetter' 63 | 64 | def is_method_or_func(self, method_name): 65 | func = getattr(self.collaborator, method_name) 66 | if inspect.ismethod(func): 67 | func = get_func(func) 68 | # func = func.im_func 69 | return inspect.isfunction(func) 70 | 71 | 72 | class DummyProxy(Proxy): 73 | def get_attr_typename(self, key): 74 | return 'instancemethod' 75 | 76 | def same_method(self, name1, name2): 77 | return name1 == name2 78 | 79 | def get_signature(self, method_name): 80 | return DummySignature() 81 | 82 | 83 | def get_class(something): 84 | if inspect.isclass(something): 85 | return something 86 | else: 87 | return something.__class__ 88 | 89 | 90 | class CollaboratorProxy(Proxy): 91 | '''Represent the collaborator object''' 92 | def __init__(self, collaborator): 93 | self.collaborator = collaborator 94 | self.collaborator_class = get_class(collaborator) 95 | 96 | def isclass(self): 97 | return inspect.isclass(self.collaborator) 98 | 99 | def get_class_attr(self, key): 100 | return getattr(self.collaborator_class, key) 101 | 102 | def get_attr(self, key): 103 | return getattr(self.collaborator, key) 104 | 105 | def collaborator_classname(self): 106 | return self.collaborator_class.__name__ 107 | 108 | def assure_signature_matches(self, invocation): 109 | signature = self.get_signature(invocation.name) 110 | signature.assure_matches(invocation.context) 111 | 112 | def get_attr_typename(self, key): 113 | def raise_no_attribute(): 114 | reason = "'%s' object has no attribute '%s'" % \ 115 | (self.collaborator_classname(), key) 116 | raise AttributeError(reason) 117 | 118 | try: 119 | attr = getattr(self.collaborator_class, key) 120 | return type(attr).__name__ 121 | except AttributeError: 122 | if self.collaborator is self.collaborator_class: 123 | raise_no_attribute() 124 | 125 | try: 126 | attr = getattr(self.collaborator, key) 127 | return type(attr).__name__ 128 | except AttributeError: 129 | raise_no_attribute() 130 | 131 | def same_method(self, name1, name2): 132 | return getattr(self.collaborator, name1) == \ 133 | getattr(self.collaborator, name2) 134 | 135 | def perform_invocation(self, invocation): 136 | method = getattr(self.collaborator, invocation.name) 137 | return invocation.context.apply_on(method) 138 | 139 | 140 | class Signature(object): 141 | def __init__(self, proxy, name): 142 | self.proxy = proxy 143 | self.name = name 144 | self.method = getattr(proxy.collaborator, name) 145 | 146 | def get_arg_spec(self): 147 | pass 148 | 149 | def get_call_args(self, context): 150 | retval = context.kargs.copy() 151 | for n, i in enumerate(context.args): 152 | retval['_positional_%s' % n] = i 153 | 154 | return retval 155 | 156 | def __eq__(self, other): 157 | return (self.proxy, self.name, self.method) == \ 158 | (other.proxy, other.name, other.method) 159 | 160 | 161 | class DummySignature(Signature): 162 | def __init__(self): 163 | pass 164 | 165 | 166 | class BuiltinSignature(Signature): 167 | "builtin collaborator method signature" 168 | def assure_matches(self, context): 169 | if self.method.__text_signature__: 170 | args = context.args 171 | if self.proxy.isclass(): 172 | args = (None,) + args # self 173 | getcallargs(self.method, *args, **context.kargs) 174 | return 175 | doc = self.method.__doc__ 176 | if not ')' in doc: 177 | return 178 | 179 | rpar = doc.find(')') 180 | params = doc[:rpar] 181 | nkargs = params.count('=') 182 | nargs = params.count(',') + 1 - nkargs 183 | if len(context.args) != nargs: 184 | raise TypeError('%s.%s() takes exactly %s argument (%s given)' % ( 185 | self.proxy.collaborator_classname(), self.name, 186 | nargs, len(context.args))) 187 | 188 | 189 | # Thanks to David Pärsson (https://github.com/davidparsson) 190 | # issue: https://bitbucket.org/DavidVilla/python-doublex/issues/25/support-from-python-35-type-hints-when 191 | def getfullargspec(method): 192 | return inspect.getfullargspec(method) 193 | 194 | 195 | class MethodSignature(Signature): 196 | "colaborator method signature" 197 | def __init__(self, proxy, name): 198 | super(MethodSignature, self).__init__(proxy, name) 199 | self.argspec = getfullargspec(self.method) 200 | 201 | def get_arg_spec(self): 202 | retval = getfullargspec(self.method) 203 | del retval.args[0] 204 | return retval 205 | 206 | def is_classmethod(self): 207 | return ( 208 | inspect.ismethod(self.method) and self.method.__self__ is self.proxy.collaborator_class 209 | ) 210 | 211 | def get_call_args(self, context): 212 | args = context.args 213 | is_classmethod = self.is_classmethod() 214 | if self.proxy.isclass() and not is_classmethod: 215 | args = (None,) + args # self 216 | 217 | retval = getcallargs(self.method, *args, **context.kargs) 218 | retval.pop('cls' if is_classmethod else 'self', None) 219 | return retval 220 | 221 | def assure_matches(self, context): 222 | if ANY_ARG.is_in(context.args): 223 | return 224 | 225 | try: 226 | self.get_call_args(context) 227 | except TypeError as e: 228 | raise TypeError("%s.%s" % (self.proxy.collaborator_classname(), e)) 229 | 230 | def __repr__(self): 231 | return "%s.%s%s" % (self.proxy.collaborator_classname(), 232 | self.name, 233 | inspect.signature(self.method)) 234 | 235 | 236 | class PropertySignature(Signature): 237 | def __init__(self, proxy, name): 238 | pass 239 | 240 | def assure_matches(self, context): 241 | pass 242 | -------------------------------------------------------------------------------- /doublex/test/README: -------------------------------------------------------------------------------- 1 | To run all tests just run "nosetests" in the parent directory. 2 | -------------------------------------------------------------------------------- /doublex/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doublex/test/any_arg_tests.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from doublex import Stub, Spy, when, called, ANY_ARG 4 | from hamcrest import assert_that, anything, is_, instance_of 5 | 6 | 7 | class CollaboratorWithProperty: 8 | @property 9 | def prop(self): 10 | pass 11 | 12 | 13 | class Collaborator: 14 | def method_accepting_property(self, prop): 15 | pass 16 | 17 | 18 | class RaisingEq: 19 | def __eq__(self, other): 20 | raise ValueError('I dont like comparisons') 21 | 22 | 23 | class AnyArgTests(TestCase): 24 | def test_any_arg_matches_property(self): 25 | prop_stub = Spy(CollaboratorWithProperty) 26 | when(prop_stub).prop.returns(5) 27 | 28 | with Stub(Collaborator) as stub: 29 | stub.method_accepting_property(prop=anything()).returns(2) 30 | 31 | assert_that(stub.method_accepting_property(prop_stub.prop), is_(2)) 32 | assert prop_stub.prop == 5 33 | 34 | def test_any_arg_checking_works_when_eq_raises(self): 35 | with Spy(Collaborator) as spy: 36 | spy.method_accepting_property(ANY_ARG).returns(6) 37 | 38 | assert_that(spy.method_accepting_property(RaisingEq()), is_(6)) 39 | assert_that(spy.method_accepting_property, called().with_args(instance_of(RaisingEq))) 40 | -------------------------------------------------------------------------------- /doublex/test/async_race_condition_tests.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; coding: utf-8 -*- 2 | 3 | # All bugs by Oscar Aceña 4 | 5 | import time 6 | 7 | try: 8 | import thread 9 | except ImportError: 10 | import _thread as thread 11 | 12 | import unittest 13 | 14 | from doublex import ProxySpy, assert_that, called 15 | 16 | 17 | class Collaborator(object): 18 | def write(self, data): 19 | time.sleep(0.3) 20 | 21 | 22 | class SUT(object): 23 | def __init__(self, collaborator): 24 | self.collaborator = collaborator 25 | 26 | def delayed_write(self): 27 | time.sleep(0.1) 28 | self.collaborator.write("something") 29 | 30 | def some_method(self): 31 | thread.start_new_thread(self.delayed_write, ()) 32 | 33 | 34 | class AsyncTests(unittest.TestCase): 35 | def test_wrong_try_to_test_an_async_invocation(self): 36 | # given 37 | spy = ProxySpy(Collaborator()) 38 | sut = SUT(spy) 39 | 40 | # when 41 | sut.some_method() 42 | 43 | # then 44 | assert_that(spy.write, called().async_mode(1)) 45 | -------------------------------------------------------------------------------- /doublex/test/chain_tests.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8; tab-width:4 -*- 2 | 3 | from unittest import TestCase 4 | import doublex 5 | 6 | 7 | class ChainTests(TestCase): 8 | def test_chain_default_behavior(self): 9 | stub = doublex.Stub() 10 | 11 | doublex.set_default_behavior(stub, doublex.Spy) 12 | chained_spy = stub.foo() 13 | chained_spy.bar() 14 | 15 | doublex.assert_that(chained_spy.bar, doublex.called()) 16 | -------------------------------------------------------------------------------- /doublex/test/issue_14_tests.py: -------------------------------------------------------------------------------- 1 | # Thanks to Guillermo Pascual (@pasku1) 2 | 3 | # When you spy a method that has a decorator and you want to check the 4 | # arguments with a hamcrest matcher, it seems like matchers are 5 | # ignored. 6 | 7 | from functools import wraps 8 | import unittest 9 | from doublex import * 10 | from hamcrest import * 11 | 12 | 13 | class Collaborator(object): 14 | def simple_decorator(func): 15 | @wraps(func) 16 | def wrapper(self, *args, **kwargs): 17 | return func(self, *args, **kwargs) 18 | return wrapper 19 | 20 | @simple_decorator 21 | def method_with_two_arguments(self, one, two): 22 | pass 23 | 24 | 25 | class ExampleTest(unittest.TestCase): 26 | def test_spying_a_method_with_a_decorator(self): 27 | collaborator = Spy(Collaborator) 28 | collaborator.method_with_two_arguments(1, 'foo bar') 29 | 30 | assert_that(collaborator.method_with_two_arguments, 31 | called().with_args(1, ends_with('bar'))) 32 | -------------------------------------------------------------------------------- /doublex/test/namedtuple_stub_tests.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | from unittest import TestCase 3 | 4 | from doublex import Stub, Spy, assert_that, property_got, called 5 | from hamcrest import is_ 6 | 7 | 8 | class NamedtupleCollaborator(NamedTuple): 9 | a: str 10 | b: int 11 | 12 | def method(self, arg): 13 | pass 14 | 15 | 16 | class NamedtupleStubTests(TestCase): 17 | def test_can_stub_namedtuple(self): 18 | with Stub(NamedtupleCollaborator) as stub: 19 | stub.a.returns('hi') 20 | stub.method(5).returns('Nothing') 21 | 22 | assert_that(stub.a, is_('hi')) 23 | assert_that(stub.method(5), is_('Nothing')) 24 | 25 | 26 | class NamedtupleSpyTests(TestCase): 27 | def test_can_spy_namedtuple(self): 28 | with Spy(NamedtupleCollaborator) as spy: 29 | spy.a.returns('hi') 30 | spy.method(5).returns('Nothing') 31 | 32 | _ = spy.a 33 | spy.method(5) 34 | 35 | assert_that(spy, property_got('a')) 36 | assert_that(spy.method, called().with_args(5).times(1)) 37 | -------------------------------------------------------------------------------- /doublex/test/report_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8; tab-width:4; mode:python -*- 2 | 3 | # doublex 4 | # 5 | # Copyright © 2012, 2013 David Villa Alises 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | 21 | 22 | from unittest import TestCase 23 | 24 | from hamcrest import assert_that, is_, is_not, contains_string, greater_than 25 | 26 | import doublex 27 | from doublex.internal import Invocation, InvocationContext 28 | from .unit_tests import ObjCollaborator 29 | 30 | 31 | def create_invocation(name, args=None, kargs=None, retval=None): 32 | stub = doublex.Stub() 33 | 34 | args = args or tuple() 35 | kargs = kargs or {} 36 | context = InvocationContext(*args, **kargs) 37 | context.retval = retval 38 | invocation = Invocation(stub, name, context) 39 | return invocation 40 | 41 | 42 | class InvocationReportTests(TestCase): 43 | def test_int_arg_method_returning_int(self): 44 | invocation = create_invocation('foo', (1,), None, retval=1) 45 | assert_that(str(invocation), is_('Stub.foo(1)-> 1')) 46 | 47 | def test_ANY_arg_method_returning_none(self): 48 | invocation = create_invocation('foo', (doublex.ANY_ARG,)) 49 | assert_that(str(invocation), is_('Stub.foo(ANY_ARG)')) 50 | 51 | # @nottest 52 | # def test_unicode_arg_method(self): 53 | # self.add_invocation('foo', (u'ñandú',)) 54 | # 55 | # print self.render() 56 | # 57 | # assert_that(self.render(), 58 | # is_("foo(u'ñandú')")) 59 | 60 | 61 | class MessageMixin(object): 62 | def assert_with_message(self, value, matcher, message): 63 | try: 64 | assert_that(value, matcher) 65 | self.fail("Exception should be raised") 66 | except AssertionError as e: 67 | assert_that(str(e).strip(), is_(message.strip())) 68 | 69 | 70 | class SpyReportTest(TestCase, MessageMixin): 71 | def test_called(self): 72 | spy = doublex.Spy() 73 | 74 | expected = ''' 75 | Expected: these calls: 76 | Spy.expected(ANY_ARG) 77 | but: calls that actually ocurred were: 78 | No one''' 79 | 80 | self.assert_with_message(spy.expected, doublex.called(), 81 | expected) 82 | 83 | def test_nerver_called(self): 84 | spy = doublex.Spy() 85 | 86 | spy.foo(1) 87 | spy.foo(2) 88 | spy.unexpected(5) 89 | 90 | self.assert_with_message( 91 | spy.unexpected, doublex.never(doublex.called()), 92 | ''' 93 | Expected: none of these calls: 94 | Spy.unexpected(ANY_ARG) 95 | but: calls that actually ocurred were: 96 | Spy.foo(1) 97 | Spy.foo(2) 98 | Spy.unexpected(5)''') 99 | 100 | def test_hamcrest_not_called(self): 101 | spy = doublex.Spy() 102 | spy.foo(1) 103 | spy.foo(2) 104 | spy.unexpected(5) 105 | 106 | self.assert_with_message( 107 | spy.unexpected, is_not(doublex.called()), 108 | ''' 109 | Expected: not these calls: 110 | Spy.unexpected(ANY_ARG) 111 | but: but was ''') 112 | 113 | def test_called_times_int(self): 114 | spy = doublex.Spy() 115 | 116 | spy.foo(1) 117 | spy.foo(2) 118 | 119 | self.assert_with_message( 120 | spy.foo, doublex.called().times(1), 121 | ''' 122 | Expected: these calls: 123 | Spy.foo(ANY_ARG) -- times: 1 124 | but: calls that actually ocurred were: 125 | Spy.foo(1) 126 | Spy.foo(2)''') 127 | 128 | def test_called_times_matcher(self): 129 | spy = doublex.Spy() 130 | 131 | spy.foo(1) 132 | spy.foo(2) 133 | 134 | self.assert_with_message( 135 | spy.foo, doublex.called().times(greater_than(3)), 136 | ''' 137 | Expected: these calls: 138 | Spy.foo(ANY_ARG) -- times: a value greater than <3> 139 | but: calls that actually ocurred were: 140 | Spy.foo(1) 141 | Spy.foo(2)''') 142 | 143 | def test_called_with(self): 144 | spy = doublex.Spy() 145 | 146 | spy.foo(1) 147 | spy.foo(2) 148 | 149 | self.assert_with_message( 150 | spy.expected, doublex.called().with_args(3), 151 | ''' 152 | Expected: these calls: 153 | Spy.expected(3) 154 | but: calls that actually ocurred were: 155 | Spy.foo(1) 156 | Spy.foo(2)''') 157 | 158 | def test_never_called_with(self): 159 | spy = doublex.Spy() 160 | 161 | spy.foo(1) 162 | spy.foo(2) 163 | spy.unexpected(2) 164 | 165 | self.assert_with_message( 166 | spy.unexpected, doublex.never(doublex.called().with_args(2)), 167 | ''' 168 | Expected: none of these calls: 169 | Spy.unexpected(2) 170 | but: calls that actually ocurred were: 171 | Spy.foo(1) 172 | Spy.foo(2) 173 | Spy.unexpected(2)''') 174 | 175 | def test_hamcrest_not_called_with(self): 176 | spy = doublex.Spy() 177 | 178 | spy.foo(1) 179 | spy.foo(2) 180 | spy.unexpected(2) 181 | 182 | self.assert_with_message( 183 | spy.unexpected, is_not(doublex.called().with_args(2)), 184 | ''' 185 | Expected: not these calls: 186 | Spy.unexpected(2) 187 | but: but was ''') 188 | 189 | def test_called_with_matcher(self): 190 | spy = doublex.Spy() 191 | 192 | self.assert_with_message( 193 | spy.unexpected, 194 | doublex.called().with_args(greater_than(1)), 195 | ''' 196 | Expected: these calls: 197 | Spy.unexpected(a value greater than <1>) 198 | but: calls that actually ocurred were: 199 | No one''') 200 | 201 | def test_never_called_with_matcher(self): 202 | spy = doublex.Spy() 203 | spy.unexpected(2) 204 | 205 | self.assert_with_message( 206 | spy.unexpected, 207 | doublex.never(doublex.called().with_args(greater_than(1))), 208 | ''' 209 | Expected: none of these calls: 210 | Spy.unexpected(a value greater than <1>) 211 | but: calls that actually ocurred were: 212 | Spy.unexpected(2)''') 213 | 214 | def test__hamcrest_not__called_with_matcher(self): 215 | spy = doublex.Spy() 216 | spy.unexpected(2) 217 | 218 | self.assert_with_message( 219 | spy.unexpected, 220 | is_not(doublex.called().with_args(greater_than(1))), 221 | ''' 222 | Expected: not these calls: 223 | Spy.unexpected(a value greater than <1>) 224 | but: but was ''') 225 | 226 | 227 | class MockReportTest(TestCase, MessageMixin): 228 | def setUp(self): 229 | self.mock = doublex.Mock() 230 | 231 | def assert_expectation_error(self, expected_message): 232 | self.assert_with_message(self.mock, doublex.verify(), 233 | expected_message) 234 | 235 | def test_expect_none_but_someting_unexpected_called(self): 236 | expected_message = ''' 237 | Expected: these calls: 238 | No one 239 | but: this call was not expected: 240 | Mock.unexpected() 241 | ''' 242 | 243 | try: 244 | self.mock.unexpected() 245 | self.fail("This should raise exception") 246 | except AssertionError as e: 247 | assert_that(str(e), is_(expected_message)) 248 | 249 | def test_expect_1_void_method_but_nothing_called(self): 250 | with self.mock: 251 | self.mock.expected() 252 | 253 | expected_message = ''' 254 | Expected: these calls: 255 | Mock.expected() 256 | but: calls that actually ocurred were: 257 | No one 258 | ''' 259 | 260 | self.assert_expectation_error(expected_message) 261 | 262 | def test_expect_2_void_methods_but_nothing_called(self): 263 | with self.mock: 264 | self.mock.foo() 265 | self.mock.bar() 266 | 267 | expected_message = ''' 268 | Expected: these calls: 269 | Mock.foo() 270 | Mock.bar() 271 | but: calls that actually ocurred were: 272 | No one 273 | ''' 274 | 275 | self.assert_expectation_error(expected_message) 276 | 277 | def test_expect_method_with_2_int_args_returning_int_but_nothing_called(self): 278 | with self.mock: 279 | self.mock.foo(1, 2).returns(1) 280 | 281 | expected_message = ''' 282 | Expected: these calls: 283 | Mock.foo(1, 2)-> 1 284 | but: calls that actually ocurred were: 285 | No one 286 | ''' 287 | 288 | self.assert_expectation_error(expected_message) 289 | 290 | def test_except_method_with_2_str_args_returning_str_but_nothing_called(self): 291 | with self.mock: 292 | self.mock.foo('a', 'b').returns('c') 293 | 294 | expected_message = ''' 295 | Expected: these calls: 296 | Mock.foo('a', 'b')-> 'c' 297 | but: calls that actually ocurred were: 298 | No one 299 | ''' 300 | 301 | self.assert_expectation_error(expected_message) 302 | 303 | def test_except_method_with_2_kwargs_returning_dict_but_nothing_called(self): 304 | with self.mock: 305 | self.mock.foo(num=1, color='red').returns({'key': 1}) 306 | 307 | expected_message = ''' 308 | Expected: these calls: 309 | Mock.foo(color='red', num=1)-> {'key': 1} 310 | but: calls that actually ocurred were: 311 | No one 312 | ''' 313 | 314 | self.assert_expectation_error(expected_message) 315 | 316 | def test_expect_4_calls_but_only_2_called(self): 317 | with self.mock: 318 | self.mock.foo() 319 | self.mock.foo() 320 | self.mock.bar() 321 | self.mock.bar() 322 | 323 | self.mock.foo() 324 | self.mock.bar() 325 | 326 | expected_message = ''' 327 | Expected: these calls: 328 | Mock.foo() 329 | Mock.foo() 330 | Mock.bar() 331 | Mock.bar() 332 | but: calls that actually ocurred were: 333 | Mock.foo() 334 | Mock.bar() 335 | ''' 336 | 337 | self.assert_expectation_error(expected_message) 338 | 339 | 340 | class PropertyReportTests(TestCase, MessageMixin): 341 | def test_expected_get(self): 342 | spy = doublex.Spy(ObjCollaborator) 343 | 344 | expected_message = ''' 345 | Expected: these calls: 346 | get ObjCollaborator.prop 347 | but: calls that actually ocurred were: 348 | No one 349 | ''' 350 | 351 | self.assert_with_message( 352 | spy, doublex.property_got('prop'), 353 | expected_message) 354 | 355 | def test_unexpected_get(self): 356 | expected_message = ''' 357 | Expected: none of these calls: 358 | get ObjCollaborator.prop 359 | but: calls that actually ocurred were: 360 | get ObjCollaborator.prop 361 | ''' 362 | 363 | spy = doublex.Spy(ObjCollaborator) 364 | spy.prop 365 | 366 | self.assert_with_message( 367 | spy, doublex.never(doublex.property_got('prop')), 368 | expected_message) 369 | 370 | def test_expected_set(self): 371 | spy = doublex.Spy(ObjCollaborator) 372 | 373 | expected_message = ''' 374 | Expected: these calls: 375 | set ObjCollaborator.prop to ANYTHING 376 | but: calls that actually ocurred were: 377 | No one 378 | ''' 379 | 380 | self.assert_with_message( 381 | spy, doublex.property_set('prop'), 382 | expected_message) 383 | 384 | def test_unexpected_set(self): 385 | expected_message = ''' 386 | Expected: none of these calls: 387 | set ObjCollaborator.prop to ANYTHING 388 | but: calls that actually ocurred were: 389 | set ObjCollaborator.prop to unexpected 390 | ''' 391 | 392 | spy = doublex.Spy(ObjCollaborator) 393 | spy.prop = 'unexpected' 394 | 395 | self.assert_with_message( 396 | spy, doublex.never(doublex.property_set('prop')), 397 | expected_message) 398 | -------------------------------------------------------------------------------- /doublex/tracer.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8; tab-width:4; mode:python -*- 2 | 3 | from .doubles import Stub 4 | from .internal import Method, Property, WrongApiUsage 5 | 6 | 7 | class MethodTracer(object): 8 | def __init__(self, logger, method): 9 | self.logger = logger 10 | self.method = method 11 | 12 | def __call__(self, *args, **kargs): 13 | self.logger(str(self.method._create_invocation(args, kargs))) 14 | 15 | 16 | class PropertyTracer(object): 17 | def __init__(self, logger, prop): 18 | self.logger = logger 19 | self.prop = prop 20 | 21 | def __call__(self, *args, **kargs): 22 | propname = "%s.%s" % (self.prop.double._classname(), self.prop.key) 23 | if args: 24 | self.logger("%s set to %s" % (propname, args[0])) 25 | else: 26 | self.logger("%s gotten" % (propname)) 27 | 28 | 29 | class Tracer(object): 30 | def __init__(self, logger): 31 | self.logger = logger 32 | 33 | def trace(self, target): 34 | if isinstance(target, Method): 35 | self.trace_method(target) 36 | elif isinstance(target, Stub) or issubclass(target, Stub): 37 | self.trace_class(target) 38 | else: 39 | raise WrongApiUsage('Can not trace %s' % target) 40 | 41 | def trace_method(self, method): 42 | method.attach(MethodTracer(self.logger, method)) 43 | 44 | def trace_class(self, double): 45 | def attach_new_method(attr): 46 | if isinstance(attr, Method): 47 | attr.attach(MethodTracer(self.logger, attr)) 48 | elif isinstance(attr, Property): 49 | attr.attach(PropertyTracer(self.logger, attr)) 50 | 51 | double._new_attr_hooks.append(attach_new_method) 52 | -------------------------------------------------------------------------------- /pydoubles-site/doublex-documentation: -------------------------------------------------------------------------------- 1 | [migrated] 2 | 3 | For the time being you can find the doublex API documentation at: 4 | 5 | https://bitbucket.org/DavidVilla/python-doublex/wiki 6 | 7 |

What provides doublex respect to pyDoubles?

8 | 9 | Respect to pyDoubles, doublex...: 10 |
    11 |
  • Use just hamcrest matchers (for all features).
  • 12 |
  • Only ProxySpy requires an instance. Other doubles accept a class too, and they never instantiate it.
  • 13 |
  • Stub observers: Notify arbitrary hooks when methods are invoked. Useful to add "side effects".
  • 14 |
  • Stub delegates: Use callables, iterables or generators to create stub return values.
  • 15 |
  • Mimic doubles: doubles that inherit the same collaborator subclasses. This provides full LSP for code that make strict type checking.
  • 16 |
17 | doublex support all the issues notified in the pyDoubles issue tracker: 18 | 25 | And other features requested in the user group: 26 | 31 |   32 | -------------------------------------------------------------------------------- /pydoubles-site/downloads: -------------------------------------------------------------------------------- 1 | Get latest release from here 2 | 3 |
 4 | gunzip doublex-X.X.tar.gz
 5 | tar xvf doublex-X.X.tar
 6 | cd doublex-X.X/
 7 | sudo python setup.py install
 8 | 
9 | 10 | Or use pip: 11 | 12 |
$ sudo pip install doubles-X.X.tar.gz
13 | 14 | Pydoubles is also available on Pypi: 15 | 16 |
$ sudo pip install doublex
17 | 18 | You can also get the latest source code from the mercurial repository. Check out the project: 19 | 20 |
$ hg clone https://bitbucket.org/DavidVilla/python-doublex
21 | 22 | Browse the source code, get support and notify bugs in the issue tracker. 23 | -------------------------------------------------------------------------------- /pydoubles-site/overview: -------------------------------------------------------------------------------- 1 | [migrated] 2 | 3 |

What is pyDoubles?

4 | 5 | pyDoubles is a test doubles framework for the Python platform. Test doubles frameworks are also called mocking frameworks. pyDoubles can be used as a testing tool or as a Test Driven Development tool. 6 | 7 | It generates stubs, spies, and mock objects using a fluent interface that will make your unit tests more readable. Moreover, it's been designed to make your tests less fragile when possible. 8 | 9 | The development of pyDoubles has been completely test-driven from scratch. The project is under continuous evolution, but you can extend the framework with your own requirements. The code is simple and well documented with unit tests. 10 | 11 |

What is doublex?

12 | 13 | doublex is a new doubles framework that optionally provides the pyDoubles legacy API. It supports all the pyDoubles features and some more that can not be easely backported. If you are a pyDoubles user you can run your tests using doublex.pyDoubles module. However, we recommed the native doublex API for your new developments. 14 | 15 |

Supported test doubles

16 | 17 | Find out what test doubles are according to Gerard Meszaros. pyDoubles offers mainly three kind of doubles: 18 | 19 |

Stub

20 | 21 | Replaces the implementation of one or more methods in the object instance which plays the role of collaborator or dependency, returning the value that we explicitly write down in the test. A stub is actually a method but it is also common to use the noun stub for a class with stubbed methods. The stub does not have any kind or memory. 22 | 23 | Stubs are used mainly for state validation or along with spies or mocks. 24 | 25 |

Spy

26 | 27 | Replaces the implementation as a stub does, but it is also able to register and remember what methods are called during the test execution and how they are invoked. 28 | 29 | They are used for interaction/behavior verification. 30 | 31 |

Mock

32 | 33 | Contains the same features than the Stub and therefore the Spy, but it is very strict in the behavior specification it should expect from the System Under Tests. Before calling any method in the mock object, the framework should be told (in the test) which methods we expect to be called in order for them to succeed. Otherwise, the test will fail with an "UnexpectedBehavior" exception. 34 | 35 | Mock objects are used when we have to be very precise in the behavior specification. They usually make the tests more fragile than a spy but still are necessary in many cases. It is common to use mock objects together with stubs in tests. 36 | 37 |

New to test doubles?

38 | 39 | A unit test is comprised of three parts: Arrange/Act/Assert or Given/When/Then or whatever you want to call them. The scenario has to be created, exercised, and eventually we verify that the expected behavior happened. The test doubles framework is used to create the scenario (create the objects), and verify behavior after the execution but it does not make sense to invoke test doubles' methods in the test code. If you call the doubles' methods in the test code, you are testing the framework itself, which has been already tested (better than that, we crafted it using TDD). Make sure the calls to the doubles' methods happen in your production code. 40 | 41 |

Why another framework?

42 | 43 | pyDoubles is inspired in mockito and jMock for Java, and also inspired in Rhino.Mocks for .Net. There are other frameworks for Python that work really well, but after some time using them, we were not really happy with the syntax and the readability of the tests. Fragile tests were also a problem. Some well-known frameworks available for Python are: mocker, mockito-python, mock, pymox. 44 | 45 | pyDoubles is open source and free software, released under the Apache License Version 2.0 46 | 47 | Take a look at the project's blog 48 | -------------------------------------------------------------------------------- /pydoubles-site/pydoubles-documentation: -------------------------------------------------------------------------------- 1 | [migrated] 2 | 3 |
class SimpleExample(unittest.TestCase):
  4 |    def test_ask_the_sender_to_send_the_report(self):
  5 |         sender = spy(Sender())
  6 |         service = SavingsService(sender)
  7 | 
  8 |         service.analyze_month()
  9 |         assert_that_method(sender.send_email).was_called(
 10 |                         ).with_args('reports@x.com', ANY_ARG)
11 |

Import the framework in your tests

12 |
import unittest
 13 | from doublex.pyDoubles import *
14 | If you are afraid of importing everything from the pyDoubles.framework module, you can use custom imports, although it has been carefully designed to not conflict with your own classes. 15 |
import unittest
 16 | from doublex.pyDoubles import stub, spy, mock
 17 | from doublex.pyDoubles import when, expect_call, assert_that_method
 18 | from doublex.pyDoubles import method_returning, method_raising
19 | You can import Hamcrest matchers which are fully supported: 20 |
from hamcrest import *
21 |

Which doubles do you need?

22 | You can choose to stub out a method in a regular object instance, to stub the whole object, or to create three types of spies and two types of mock objects. 23 |

Stubs

24 | There are several ways to stub out methods. 25 |
Stub out a single method
26 | If you just need to replace a single method in the collaborator object and you don't care about the input parameters, you can stub out just that single method: 27 |
collaborator = Collaborator() # create the actual object
 28 | collaborator.some_calculation = method_returning(10)
29 | Now, when your production code invokes the method "some_calculation" in the collaborator object, the framework will return 10, no matter what parameters are passed in as the input. 30 | 31 | If you want the method to raise an exception when called use this: 32 |
collaborator.some_calculation = method_raising(ApplicationException())
33 | You can pass in any type of exception. 34 |
Stub out the whole object
35 | Now the collaborator instance won't be the actual object but a replacement. 36 |
collaborator = stub(Collaborator())
37 | Any method will return "None" when called with any input parameters. 38 | If you want to change the return value you can use the "when" sentence: 39 |
when(collaborator.some_calculation).then_return(10)
40 | Now, when your production code invokes "some_calculation" method, the stub will return 10, no matter what arguments are passed in. 41 | You can also specify different return values depending on the input: 42 |
when(collaborator.some_calculation).with_args(5).then_return(10)
 43 | when(collaborator.some_calculation).with_args(10).then_return(20)
44 | This means that "collaborator.some_calculation(5)" will return 10, and that it will return 20 when the input is 10. You can define as many input/output specifications as you want. 45 |
when(collaborator.some_calculation).with_args(5).then_return(10)
 46 | when(collaborator.some_calculation).then_return(20)
47 | This time, "collaborator.some_calculation(5)" will return 10, and it will return 20 in any other case. 48 |
Any argument matches
49 | The special keyword ANY_ARG is a wildcard for any argument in the 50 | stubbed method: 51 |
when(collaborator.some_other_method).with_args(5, ANY_ARG).then_return(10)
52 | The method "some_other_method" will return 10 as long as the first parameter is 5, no matter what the second parameter is. You can use any combination of "ANY_ARG" arguments. But remember that if all of them are ANY, you shouldn't specify the arguments, just use this: 53 |
when(collaborator.some_other_method).then_return(10)
54 | It is also possible to make the method return exactly the first parameter passed in: 55 |
when(collaborator.some_other_method).then_return_input()
56 | So this call: collaborator.some_other_method(10) wil return 10. 57 |
Matchers
58 | You can also specify that arguments will match a certain function. Say that you want to return a value only if the input argument contains the substring "abc": 59 |
when(collaborator.some_method).with_args(
 60 |         str_containing("abc")).then_return(10)
61 | In the last release, pyDoubles matchers are just aliases for the hamcrest counterparts. See release notes. 62 |
Hamcrest Matchers
63 | Since pyDoubles v1.2, we fully support Hamcrest matchers. 64 | They are used exactly like pyDoubles matchers: 65 |
from hamcrest import *
 66 | from doublex.pyDoubles import *
 67 | 
 68 |     def test_has_entry_matcher(self):
 69 |         list = {'one':1, 'two':2}
 70 |         when(self.spy.one_arg_method).with_args(
 71 |             has_entry(equal_to('two'), 2)).then_return(1000)
 72 |         assert_that(1000, equal_to(self.spy.one_arg_method(list)))
 73 | 
 74 |     def test_all_of_matcher(self):
 75 |         text = 'hello'
 76 |         when(self.spy.one_arg_method).with_args(
 77 |             all_of(starts_with('h'), instance_of(str))).then_return(1000)
 78 |         assert_that(1000, equal_to(self.spy.one_arg_method(text)))
79 | Note that the tests above are just showhing the pyDoubles framework working together with Hamcrest, they are not good examples of unit tests for your production code. 80 | The method assert_that comes from Hamcrest, as well as the matchers: has_entry, equal_to, all_of, starts_with, instance_of. 81 | Notice that all_of and any_of, allow you to define more than one matcher for a single argument, which is really powerful. 82 | For more informacion on matchers, read this blog post. 83 |
Stub out the whole unexisting object
84 | If the Collaborator class does not exist yet, or you don't want the framework to check that the call to the stub object method matches the actual API in the actual object, you can use an "empty" stub. 85 |
collaborator = empty_stub()
 86 | when(collaborator.alpha_operation).then_return("whatever")
87 | The framework is creating the method "alpha_operation" dynamically 88 | and making it return "whatever". 89 | 90 | The use of empty_stub, empty_spy or empty_mock is not recommended because you lose the API match check. We only use them as the construction of the object is too complex among other circumstances. 91 |

Spies

92 | Please read the documentation above about stubs, because the API to 93 | define method behaviors is the same for stubs and spies. To create 94 | the object: 95 |
collaborator = spy(Collaborator())
96 | After the execution of the system under test, we want to validate 97 | that certain call was made: 98 |
assert_that_method(collaborator.send_email).was_called()
99 | That will make the test pass if method "send_email" was invoked one or more times, no matter what arguments were passed in. 100 | We can also be precise about the arguments: 101 |
assert_that_method(collaborator.send_email).was_called().with_args("example@iexpertos.com")
102 | Notice that you can combine the "when" statement with the called assertion: 103 |
def test_sut_asks_the_collaborator_to_send_the_email(self):
104 |    sender = spy(Sender())
105 |    when(sender.send_email).then_return(SUCCESS)
106 |    object_under_test = Sut(sender)
107 | 
108 |    object_under_test.some_action()
109 | 
110 |    assert_that_method(
111 |  sender.send_email).was_called().with_args("example@iexpertos.com")
112 | Any other call to any method in the "sender" double will return "None" and will not interrupt the test. We are not telling all that happens between the sender and the SUT, we are just asserting on what we want to verify. 113 | 114 | The ANY_ARG matcher can be used to verify the call as well: 115 |
assert_that_method(collaborator.some_other_method).was_called().with_args(5, ANY_ARG)
116 | Matchers can also be used in the assertion: 117 |
assert_that_method(collaborator.some_other_method).was_called().with_args(5, str_containing("abc"))
118 | It is also possible to assert that wasn't called using: 119 |
assert_that_method(collaborator.some_method).was_never_called()
120 | You can assert on the number of times a call was made: 121 |
assert_that_method(collaborator.some_method).was_called().times(2)
122 | assert_that_method(collaborator.some_method).was_called(
123 |      ).with_args(SOME_VALUE, OTHER_VALUE).times(2)
124 | You can also create an "empty_spy" to not base the object in a 125 | certain instance: 126 |
sender = empty_spy()
127 |
The ProxySpy
128 | There is a special type of spy supported by the framework which 129 | is the ProxySpy: 130 |
collaborator = proxy_spy(Collaborator())
131 | The proxy spy will record any call made to the object but rather than replacing the actual methods in the actual object, it will execute them. So the actual methods in the Collaborator will be invoked by default. You can replace the methods one by one using the "when" statement: 132 |
when(collaborator.some_calculation).then_return(1000)
133 | Now "some_calculation" method will be a stub method but the remaining methods in the class will be the regular implementation. 134 | 135 | The ProxySpy might be interesting when you don't know what the actual method will return in a given scenario, but still you want to check that some call is made. It can be used for debugging purposes. 136 |

Mocks

137 | Before calls are made, they have to be expected: 138 |
def test_sut_asks_the_collaborator_to_send_the_email(self):
139 |    sender = mock(Sender())
140 |    expect_call(sender.send_email)
141 |    object_under_test = Sut(sender)
142 | 
143 |    object_under_test.some_action()
144 | 
145 |    sender.assert_that_is_satisfied()
146 | The test is quite similar to the one using a spy. However the framework behaves different. If any other call to the sender is made during "some_action", the test will fail. This makes the test more fragile. However, it makes sure that this interaction is the only one between the two objects, and this might be important for you. 147 |
More precise expectations
148 | You can also expect the call to have certain input parameters: 149 |
expect_call(sender.send_email).with_args("example@iexpertos.com")
150 |
Setting the return of the expected call
151 | Additionally, if you want to return anything when the expected call 152 | occurs, there are two ways: 153 |
expect_call(sender.send_email).returning(SUCCESS)
154 | Which will return SUCCESS whatever arguments you pass in, or 155 |
expect_call(sender.send_email).with_args("wrong_email").returning(FAILURE)
156 | Which expects the method to be invoked with "wrong_email" and will return FAILURE. 157 | 158 | Mocks are strict so if you expect the call to happen several times, be explicit with that: 159 |
expect_call(sender.send_email).times(2)
160 |
expect_call(sender.send_email).with_args("admin@iexpertos.com").times(2)
161 | Make sure the "times" part is at the end of the sentence: 162 |
expect_call(sender.send_email).with_args("admin@iexpertos.com").returning('OK').times(2)
163 | As you might have seen, the "when" statement is not used for mocks, only for stubs and spies. Mock objects use the "expect_call" syntax together with the "assert_that_is_satisfied" 164 | (instance method). 165 |

More documentation

166 | The best and most updated documentation are the unit tests of the framework itself. We encourage the user to read the tests and see what features are supported in every commit into the source code repository: 167 | pyDoublesTests/unit.py 168 | 169 | You can also read about what's new in every release in the blog 170 | -------------------------------------------------------------------------------- /pydoubles-site/release-notes: -------------------------------------------------------------------------------- 1 |

2 |

doublex 1.6.6

3 |
    4 |
  • bug fix update: Fixes issue 11.
  • 5 |
6 |

doublex 1.6.5

7 |
    8 |
  • bug fix update: Fixes issue 10.
  • 9 |
10 |
11 |

doublex 1.6.4

12 |
    13 |
  • Asynchronous spy assertion race condition bug fixed.
  • 14 |
  • Reading double attributes returns collaborator.class attribute values by default.
  • 15 |
16 |
17 |

doublex 1.6.2

18 |
    19 |
  • Invocation stubbed return value is now stored.
  • 20 |
  • New low level spy API: double method  "calls" property provides access to invocations and their argument values. Each 'call' has an "args" sequence and "kargs dictionary". This provides support to perform individual assertions and direct access to invocation argument values. (see test and doc).
  • 21 |
22 |

doublex 1.6

23 |
    24 |
  • First release supporting Python-3 (up to Python-3.2) [fixes issue 7].
  • 25 |
  • Ad-hoc stub attributes (see test).
  • 26 |
  • Partial support for non native Python functions.
  • 27 |
  • ProxySpy propagated stubbed invocations too (see test).
  • 28 |
29 |

doublex 1.5.1

30 | This release includes support for asynchronous spy assertions. See this blog post for the time being, soon in the official documentation. 31 |

doublex/pyDoubles 1.5

32 | Since this release the pyDoubles API is provided as a wrapper to doublex. However, there are small differences. pyDoubles matchers are not supported anymore, although you may get the same feature using standard hamcrest matchers. Anyway, legacy pyDoubles matchers are provided as hamcrest aliases. 33 | 34 | In most cases the only required change in your code is the module name, that change from: 35 |
from pyDoubles.framework.*
36 | to: 37 |
from doublex.pyDoubles import *
38 | If you have problems migrating to the new 1.5 release or migrating from pyDoubles to doublex, please ask for help in the discussion forum or in the issue tracker. 39 | -------------------------------------------------------------------------------- /pydoubles-site/support: -------------------------------------------------------------------------------- 1 |

Free support

2 | Mailing list: http://groups.google.com/group/pydoubles 3 | 4 | Issue tracker, mercurial repository: 5 | https://bitbucket.org/carlosble/pydoubles/overview 6 | Thanks to BitBucket! 7 | 8 |

Commercial support

9 | The development team of pyDoubles is a software company based in Spain. We are happy to help other companies with the usage and extension of pyDoubles. If you want to have custom features or direct support, please contact us at info@iexpertos.com 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "doublex" 7 | dynamic = ["version"] 8 | description = "Python test doubles" 9 | readme = "README.rst" 10 | requires-python = ">=3.7" 11 | license = { file = "LICENSE" } 12 | keywords = ["unit tests", "doubles", "stub", "spy", "mock"] 13 | authors = [ 14 | { name = "David Villa Alises", email = "David.Villa@gmail.com" }, 15 | { name = "David Pärsson", email = "david@parsson.se" }, 16 | ] 17 | classifiers = [ 18 | "Intended Audience :: Developers", 19 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10", 26 | "Programming Language :: Python :: 3.11", 27 | "Topic :: Software Development", 28 | "Topic :: Software Development :: Quality Assurance", 29 | "Topic :: Software Development :: Testing", 30 | ] 31 | dependencies = [ 32 | "PyHamcrest", 33 | ] 34 | 35 | [project.urls] 36 | repository = "https://github.com/DavidVilla/python-doublex" 37 | 38 | [tool.hatch.version] 39 | source = "vcs" 40 | 41 | [tool.hatch.build] 42 | only-packages = true 43 | exclude = ["doublex/test/*"] 44 | 45 | [tool.hatch.publish.index] 46 | disable = true 47 | 48 | [tool.hatch.build.hooks.vcs] 49 | version-file = "doublex/_version.py" 50 | -------------------------------------------------------------------------------- /slides/Makefile: -------------------------------------------------------------------------------- 1 | 2 | LINKS=css lib plugin js 3 | 4 | all: reveal.js $(LINKS) 5 | 6 | reveal.js: 7 | git clone --depth 1 https://github.com/hakimel/reveal.js.git 8 | 9 | $(LINKS): 10 | ln -s reveal.js/$@ $@ 11 | 12 | clean: 13 | $(RM) -r reveal.js 14 | $(RM) $(LINKS) 15 | -------------------------------------------------------------------------------- /slides/beamer/Makefile: -------------------------------------------------------------------------------- 1 | include arco/latex.mk 2 | -------------------------------------------------------------------------------- /slides/beamer/custom.sty: -------------------------------------------------------------------------------- 1 | \usepackage[spanish]{babel} 2 | \usepackage[utf8]{inputenc} 3 | \usepackage[T1]{fontenc} 4 | \usepackage{times} 5 | 6 | \usepackage{graphicx} 7 | \graphicspath{{figures/}} 8 | 9 | 10 | \usepackage{listings} 11 | \newcommand{\lstfont}{\ttfamily\fontfamily{pcr}} 12 | \lstset{ 13 | % frame 14 | frame = Ltb, 15 | framerule = 0pt, 16 | aboveskip = 0.5cm, 17 | framextopmargin = 3pt, 18 | framexbottommargin = 3pt, 19 | framexleftmargin = 0.4cm, 20 | framesep = 0pt, 21 | rulesep = .4pt, 22 | %-- 23 | % caption 24 | belowcaptionskip = 5pt, 25 | %-- 26 | % text style 27 | stringstyle = \ttfamily, 28 | showstringspaces = false, 29 | basicstyle = \scriptsize\lstfont, 30 | keywordstyle = \bfseries, 31 | %-- 32 | % numbers 33 | numbers = none, 34 | numbersep = 15pt, 35 | numberstyle = \scriptsize\lstfont, 36 | numberfirstline = false, 37 | % 38 | breaklines = true, 39 | emptylines = 1, % several empty lines are shown as one 40 | % 41 | literate = % caracteres especiales 42 | {á}{{\'a}}1 {Á}{{\'A}}1 43 | {é}{{\'e}}1 {É}{{\'E}}1 44 | {í}{{\'i}}1 {Í}{{\'I}}1 45 | {ó}{{\'o}}1 {Ó}{{\'O}}1 46 | {ú}{{\'u}}1 {Ú}{{\'U}}1 47 | {ñ}{{\~n}}1 {Ñ}{{\~N}}1 48 | {¡}{{!`}}1 {¿}{{?`}}1 49 | } 50 | 51 | \parskip=10pt 52 | 53 | \mode 54 | { 55 | \usetheme{Frankfurt} 56 | 57 | \setbeamercovered{transparent} 58 | % or whatever (possibly just delete it) 59 | } 60 | 61 | % Delete this, if you do not want the table of contents to pop up at 62 | % the beginning of each subsection: 63 | \AtBeginSubsection[] 64 | { 65 | \begin{frame}{Outline} 66 | \tableofcontents[currentsection,currentsubsection] 67 | \end{frame} 68 | } 69 | 70 | 71 | % If you wish to uncover everything in a step-wise fashion, uncomment 72 | % the following command: 73 | 74 | %\beamerdefaultoverlayspecification{<+->} 75 | -------------------------------------------------------------------------------- /slides/beamer/slides.tex: -------------------------------------------------------------------------------- 1 | \documentclass[11pt]{beamer} 2 | \usepackage{custom} 3 | 4 | \title{doublex: Python test doubles framework} 5 | \subtitle{\bfseries\small\url{http://bitbucket.org/DavidVilla/python-doublex}} 6 | \author{\bfseries\small\texttt{@david\_vi11a}} 7 | %\institute{} 8 | \date{} 9 | %\subject{} 10 | 11 | 12 | \begin{document} 13 | 14 | \begin{frame} 15 | \titlepage 16 | \end{frame} 17 | 18 | \begin{frame}{Contents} 19 | \tableofcontents 20 | \end{frame} 21 | 22 | \section{Intro} 23 | 24 | \begin{frame}{Another doubles library for Python?} 25 | Yes, why not? 26 | \end{frame} 27 | 28 | \begin{frame}{doublex features} 29 | \begin{itemize} 30 | \item Stubs 31 | \item Spies 32 | \item Mocks 33 | \item ah hoc stub methods 34 | \item stub delegates 35 | \item stub observers 36 | \item properties 37 | \item hamcrest matchers for \textbf{all} assertions 38 | \item wrapper for legacy pyDoubles API 39 | \item doublex never instantiates your classes! 40 | \end{itemize} 41 | \end{frame} 42 | 43 | \section{Stubs} 44 | 45 | \begin{frame}[fragile]{Stubs} 46 | \framesubtitle{set fixed return value} 47 | 48 | \begin{exampleblock}{} 49 | \begin{lstlisting}[language=Python] 50 | class Collaborator: 51 | def add(self, x, y): 52 | return x + y 53 | 54 | with Stub(Collaborator) as stub: 55 | stub.add(ANY_ARG).returns(1000) 56 | 57 | assert_that(stub.add(2, 2), is_(1000)) 58 | \end{lstlisting} 59 | \end{exampleblock} 60 | 61 | \end{frame} 62 | 63 | \begin{frame}[fragile]{Stubs} 64 | \framesubtitle{\... by calling arg values} 65 | Undefined stub methods returns None. 66 | 67 | \begin{exampleblock}{} 68 | \begin{lstlisting}[language=Python] 69 | with Stub(Collaborator) as stub: 70 | stub.add(2, 2).returns(1000) 71 | stub.add(3, ANY_ARG).returns(0) 72 | 73 | assert_that(stub.add(1, 1), is_(None)) 74 | assert_that(stub.add(2, 2), is_(1000)) 75 | assert_that(stub.add(3, 0), is_(0)) 76 | \end{lstlisting} 77 | \end{exampleblock} 78 | 79 | \end{frame} 80 | 81 | 82 | \begin{frame}[fragile]{Stubs} 83 | \framesubtitle{\... by hamcrest matcher} 84 | 85 | \begin{exampleblock}{} 86 | \begin{lstlisting}[language=Python] 87 | with Stub(Collaborator) as stub: 88 | stub.add(2, greater_than(4)).returns(4) 89 | 90 | assert_that(stub.add(2, 1), is_(None)) 91 | assert_that(stub.add(2, 5), is_(4)) 92 | \end{lstlisting} 93 | \end{exampleblock} 94 | 95 | \end{frame} 96 | 97 | 98 | \begin{frame}[fragile]{Stubs} 99 | \framesubtitle{\... by composite hamcrest matcher} 100 | 101 | \begin{exampleblock}{} 102 | \begin{lstlisting}[language=Python] 103 | with Stub(Collaborator) as stub: 104 | stub.foo(has_length(all_of( 105 | greater_than(4), less_than(8)))).returns(1000) 106 | 107 | assert_that(stub.add(2, "bad"), is_(None)) 108 | assert_that(stub.add(2, "enough"), is_(1000)) 109 | \end{lstlisting} 110 | \end{exampleblock} 111 | 112 | \end{frame} 113 | 114 | 115 | \section{Spies} 116 | 117 | \begin{frame}[fragile]{Spies} 118 | \framesubtitle{checking called methods} 119 | 120 | \begin{exampleblock}{} 121 | \begin{lstlisting}[language=Python] 122 | spy = Spy(Collaborator) 123 | spy.add(2, 3) 124 | spy.add("hi", 3.0) 125 | spy.add([1, 2], 'a') 126 | 127 | assert_that(spy.add, called()) 128 | \end{lstlisting} 129 | \end{exampleblock} 130 | 131 | \end{frame} 132 | 133 | \begin{frame}[fragile]{Spies} 134 | \framesubtitle{collaborator signature checking} 135 | 136 | \begin{exampleblock}{} 137 | \begin{lstlisting}[language=Python] 138 | spy = Spy(Collaborator) 139 | spy.add() 140 | TypeError: __main__.Collaborator.add() takes 141 | exactly 3 arguments (1 given) 142 | \end{lstlisting} 143 | \end{exampleblock} 144 | 145 | \end{frame} 146 | 147 | \begin{frame}[fragile]{Spies} 148 | \framesubtitle{checking called times (with matcher too!)} 149 | 150 | \begin{exampleblock}{} 151 | \begin{lstlisting}[language=Python] 152 | spy = Spy(Collaborator) 153 | spy.add(2, 3) 154 | spy.add("hi", 3.0) 155 | spy.add([1, 2], 'a') 156 | 157 | assert_that(spy.add, called().times(3)) 158 | assert_that(spy.add, called().times(greater_than(2))) 159 | \end{lstlisting} 160 | \end{exampleblock} 161 | 162 | \end{frame} 163 | 164 | \begin{frame}[fragile]{Spies} 165 | \framesubtitle{filter by argument value: \texttt{with\_args()})} 166 | 167 | \begin{exampleblock}{} 168 | \begin{lstlisting}[language=Python] 169 | spy = Spy(Collaborator) 170 | spy.add(2, 3) 171 | spy.add(2, 8) 172 | spy.add("hi", 3.0) 173 | 174 | assert_that(spy.add, called().with_args(2, ANY_ARG)).times(2) 175 | assert_that(spy.add, never(called().with_args(0, 0))) 176 | \end{lstlisting} 177 | \end{exampleblock} 178 | \end{frame} 179 | 180 | \begin{frame}[fragile]{Spies} 181 | \framesubtitle{filter by key argument (with matcher)} 182 | 183 | \begin{exampleblock}{} 184 | \begin{lstlisting}[language=Python] 185 | spy = Spy() 186 | spy.foo(name="Mary") 187 | 188 | assert_that(spy.foo, called().with_args(name="Mary")) 189 | assert_that(spy.foo, 190 | called().with_args(name=contains_string("ar"))) 191 | \end{lstlisting} 192 | \end{exampleblock} 193 | 194 | \end{frame} 195 | 196 | 197 | \begin{frame}[fragile]{Spies} 198 | \framesubtitle{Verbose meaning-full report messages!} 199 | 200 | \begin{exampleblock}{} 201 | \begin{lstlisting}[language=Python] 202 | spy = Spy() 203 | spy.foo(1) 204 | spy.bar("hi") 205 | 206 | assert_that(spy.foo, called().with_args(4)) 207 | AssertionError: 208 | Expected: these calls: 209 | Spy.foo(4) 210 | but: calls that actually ocurred were: 211 | Spy.foo(1) 212 | Spy.bar('hi') 213 | \end{lstlisting} 214 | \end{exampleblock} 215 | 216 | \end{frame} 217 | 218 | \subsection{ProxySpy} 219 | 220 | \begin{frame}[fragile]{ProxySpy} 221 | 222 | \begin{exampleblock}{} 223 | \begin{lstlisting}[language=Python] 224 | with ProxySpy(Collaborator()) as spy: 225 | spy.add(2, 2).returns(1000) 226 | 227 | assert_that(spy.add(2, 2), is_(1000)) 228 | assert_that(spy.add(1, 1), is_(2)) 229 | \end{lstlisting} 230 | \end{exampleblock} 231 | 232 | \end{frame} 233 | 234 | 235 | \section{Mocks} 236 | 237 | \begin{frame}[fragile]{Mocks} 238 | 239 | \begin{exampleblock}{} 240 | \begin{lstlisting}[language=Python] 241 | with Mock() as smtp: 242 | smtp.helo() 243 | smtp.mail(ANY_ARG) 244 | smtp.rcpt("bill@apple.com") 245 | smtp.data(ANY_ARG).returns(True).times(2) 246 | 247 | smtp.helo() 248 | smtp.mail("poormen@home.net") 249 | smtp.rcpt("bill@apple.com") 250 | smtp.data("somebody there?") 251 | assert_that(smtp.data("I am afraid.."), is_(True)) 252 | 253 | assert_that(smtp, verify()) 254 | \end{lstlisting} 255 | \end{exampleblock} 256 | \end{frame} 257 | 258 | \begin{frame}[fragile]{Mocks} 259 | \framesubtitle{invocation order is important} 260 | 261 | \begin{exampleblock}{} 262 | \begin{lstlisting}[language=Python] 263 | with Mock() as mock: 264 | mock.foo() 265 | mock.bar() 266 | 267 | mock.bar() 268 | mock.foo() 269 | 270 | assert_that(mock, verify()) 271 | AssertionError: 272 | Expected: these calls: 273 | Mock.foo() 274 | Mock.bar() 275 | but: calls that actually ocurred were: 276 | Mock.bar() 277 | Mock.foo() 278 | \end{lstlisting} 279 | \end{exampleblock} 280 | \end{frame} 281 | 282 | \begin{frame}[fragile]{Mocks} 283 | \framesubtitle{unless you do not mind: \texttt{any\_order\_verify()}} 284 | 285 | \begin{exampleblock}{} 286 | \begin{lstlisting}[language=Python] 287 | with Mock() as mock: 288 | mock.foo() 289 | mock.bar() 290 | 291 | mock.bar() 292 | mock.foo() 293 | 294 | assert_that(mock, any_order_verify()) 295 | \end{lstlisting} 296 | \end{exampleblock} 297 | \end{frame} 298 | 299 | 300 | \section{ah hoc stub methods} 301 | 302 | \begin{frame}[fragile]{ah hoc stub methods} 303 | 304 | \begin{exampleblock}{} 305 | \begin{lstlisting}[language=Python] 306 | collaborator = Collaborator() 307 | collaborator.foo = method_returning('bye') 308 | assert_that(self.collaborator.foo(), is_('bye')) 309 | 310 | collaborator.foo = method_raising(SomeException) 311 | collaborator.foo() 312 | SomeException: 313 | \end{lstlisting} 314 | \end{exampleblock} 315 | \end{frame} 316 | 317 | \section{stub observers} 318 | 319 | \begin{frame}[fragile]{stub observers} 320 | 321 | \begin{exampleblock}{} 322 | \begin{lstlisting}[language=Python] 323 | class Observer(object): 324 | def __init__(self): 325 | self.state = None 326 | 327 | def update(self, *args, **kargs): 328 | self.state = args[0] 329 | 330 | observer = Observer() 331 | stub = Stub() 332 | stub.foo.attach(observer.update) 333 | stub.foo(2) 334 | 335 | assert_that(observer.state, is_(2)) 336 | \end{lstlisting} 337 | \end{exampleblock} 338 | \end{frame} 339 | 340 | \section{stub delegates} 341 | 342 | \begin{frame}[fragile]{stub delegates} 343 | \framesubtitle{delegating to callables} 344 | 345 | \begin{exampleblock}{} 346 | \begin{lstlisting}[language=Python] 347 | def get_user(): 348 | return "Freddy" 349 | 350 | with Stub() as stub: 351 | stub.user().delegates(get_user) 352 | stub.foo().delegates(lambda: "hello") 353 | 354 | assert_that(stub.user(), is_("Freddy")) 355 | assert_that(stub.foo(), is_("hello")) 356 | \end{lstlisting} 357 | \end{exampleblock} 358 | \end{frame} 359 | 360 | \begin{frame}[fragile]{stub delegates} 361 | \framesubtitle{delegating to iterables} 362 | 363 | \begin{exampleblock}{} 364 | \begin{lstlisting}[language=Python] 365 | with Stub() as stub: 366 | stub.foo().delegates([1, 2, 3]) 367 | 368 | assert_that(stub.foo(), is_(1)) 369 | assert_that(stub.foo(), is_(2)) 370 | assert_that(stub.foo(), is_(3)) 371 | \end{lstlisting} 372 | \end{exampleblock} 373 | \end{frame} 374 | 375 | \section{properties} 376 | 377 | \begin{frame}[fragile]{stubbing properties} 378 | 379 | \begin{exampleblock}{} 380 | \begin{lstlisting}[language=Python] 381 | class Collaborator(object): 382 | @property 383 | def prop(self): 384 | return 1 385 | 386 | @prop.setter 387 | def prop(self, value): 388 | pass 389 | 390 | with Spy(Collaborator) as spy: 391 | spy.prop = 2 392 | 393 | assert_that(spy.prop, is_(2)) # double property getter invoked 394 | \end{lstlisting} 395 | \end{exampleblock} 396 | \end{frame} 397 | 398 | \begin{frame}[fragile]{spying properties (with matchers!)} 399 | 400 | \begin{exampleblock}{} 401 | \begin{lstlisting}[language=Python] 402 | assert_that(spy, property_got('prop')) 403 | 404 | spy.prop = 4 # double property setter invoked 405 | spy.prop = 5 # -- 406 | spy.prop = 5 # -- 407 | 408 | assert_that(spy, property_set('prop')) # set to any value 409 | assert_that(spy, property_set('prop').to(4)) 410 | assert_that(spy, property_set('prop').to(5).times(2)) 411 | assert_that(spy, 412 | never(property_set('prop').to(greater_than(6)))) 413 | \end{lstlisting} 414 | \end{exampleblock} 415 | \end{frame} 416 | 417 | 418 | \section{Mimics} 419 | 420 | \begin{frame}[fragile]{normal doubles support only duck-typing} 421 | 422 | \begin{exampleblock}{} 423 | \begin{lstlisting}[language=Python] 424 | class A(object): 425 | pass 426 | 427 | class B(A): 428 | pass 429 | 430 | >>> spy = Spy(B()) 431 | >>> isinstance(spy, Spy) 432 | True 433 | >>> isinstance(spy, B) 434 | False 435 | \end{lstlisting} 436 | \end{exampleblock} 437 | \end{frame} 438 | 439 | \begin{frame}[fragile]{Mimics support full LSP} 440 | 441 | \begin{exampleblock}{} 442 | \begin{lstlisting}[language=Python] 443 | >>> spy = Mimic(Spy, B) 444 | >>> isinstance(spy, B) 445 | True 446 | >>> isinstance(spy, A) 447 | True 448 | >>> isinstance(spy, Spy) 449 | True 450 | >>> isinstance(spy, Stub) 451 | True 452 | >>> isinstance(spy, object) 453 | True 454 | \end{lstlisting} 455 | \end{exampleblock} 456 | \end{frame} 457 | 458 | \section{Questions} 459 | 460 | \begin{frame}{} 461 | \begin{center} 462 | {\huge Questions?} 463 | \end{center} 464 | 465 | \end{frame} 466 | 467 | \end{document} 468 | 469 | %% Local Variables: 470 | %% coding: utf-8 471 | %% mode: flyspell 472 | %% ispell-local-dictionary: "american" 473 | %% End: 474 | -------------------------------------------------------------------------------- /slides/sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- mode:python; coding:utf-8; tab-width:4 -*- 3 | 4 | from unittest import TestCase 5 | from doublex import (Stub, Spy, ProxySpy, Mock, 6 | assert_that, called, ANY_ARG, never, 7 | verify, any_order_verify) 8 | from hamcrest import greater_than, anything, contains_string 9 | 10 | 11 | class AlreadyExists(Exception): 12 | pass 13 | 14 | 15 | class InvalidPassword(Exception): 16 | pass 17 | 18 | 19 | class AccountStore: 20 | def save(self, login, password): 21 | pass 22 | 23 | def has_user(self, login): 24 | pass 25 | 26 | 27 | class Group(set): 28 | def __init__(self, name): 29 | pass 30 | 31 | 32 | class PasswordService: 33 | def generate(self): 34 | pass 35 | 36 | 37 | class AccountService: 38 | def __init__(self, store, password_service): 39 | self.store = store 40 | self.password_service = password_service 41 | 42 | def create_user(self, login): 43 | if self.store.has_user(login): 44 | raise AlreadyExists() 45 | 46 | password = self.password_service.generate() 47 | if not password: 48 | raise InvalidPassword() 49 | 50 | self.store.save(login, password) 51 | 52 | def create_group(self, group_name, user_names): 53 | group = Group(group_name) 54 | for name in user_names: 55 | try: 56 | self.create_user(name) 57 | except AlreadyExists: 58 | pass 59 | group.add(name) 60 | 61 | 62 | class AccountTests(TestCase): 63 | def test_account_creation__free_stub(self): 64 | with Stub() as password_service: 65 | password_service.generate().returns('some') 66 | 67 | store = Spy(AccountStore) 68 | service = AccountService(store, password_service) 69 | 70 | service.create_group('team', ['John', 'Peter', 'Alice']) 71 | 72 | assert_that(store.save, called()) 73 | 74 | def test_account_creation__restricted_stub(self): 75 | with Stub(PasswordService) as password_service: 76 | password_service.generate().returns('some') 77 | 78 | store = Spy(AccountStore) 79 | service = AccountService(store, password_service) 80 | 81 | service.create_user('John') 82 | 83 | assert_that(store.save, called()) 84 | 85 | def test_account_creation__3_accounts(self): 86 | with Stub(PasswordService) as password_service: 87 | password_service.generate().returns('some') 88 | 89 | store = Spy(AccountStore) 90 | service = AccountService(store, password_service) 91 | 92 | service.create_group('team', ['John', 'Peter', 'Alice']) 93 | 94 | assert_that(store.save, called().times(3)) 95 | assert_that(store.save, called().times(greater_than(2))) 96 | 97 | def test_account_creation__argument_values(self): 98 | with Stub(PasswordService) as password_service: 99 | password_service.generate().returns('some') 100 | 101 | store = Spy(AccountStore) 102 | service = AccountService(store, password_service) 103 | 104 | service.create_user('John') 105 | 106 | assert_that(store.save, called().with_args('John', 'some')) 107 | assert_that(store.save, called().with_args('John', ANY_ARG)) 108 | assert_that(store.save, never(called().with_args('Alice', anything()))) 109 | assert_that(store.save, 110 | called().with_args(contains_string('oh'), ANY_ARG)) 111 | 112 | def test_account_creation__report_message(self): 113 | with Stub(PasswordService) as password_service: 114 | password_service.generate().returns('some') 115 | 116 | store = Spy(AccountStore) 117 | service = AccountService(store, password_service) 118 | 119 | service.create_group('team', ['John', 'Alice']) 120 | 121 | # assert_that(store.save, called().with_args('Peter')) 122 | 123 | def test_account_already_exists(self): 124 | with Stub(PasswordService) as password_service: 125 | password_service.generate().returns('some') 126 | 127 | with ProxySpy(AccountStore()) as store: 128 | store.has_user('John').returns(True) 129 | 130 | service = AccountService(store, password_service) 131 | 132 | with self.assertRaises(AlreadyExists): 133 | service.create_user('John') 134 | 135 | def test_account_behaviour_with_mock(self): 136 | with Stub(PasswordService) as password_service: 137 | password_service.generate().returns('some') 138 | 139 | with Mock(AccountStore) as store: 140 | store.has_user('John') 141 | store.save('John', 'some') 142 | store.has_user('Peter') 143 | store.save('Peter', 'some') 144 | 145 | service = AccountService(store, password_service) 146 | 147 | service.create_group('team', ['John', 'Peter']) 148 | 149 | assert_that(store, verify()) 150 | 151 | # def test_account_behaviour_with_mock_any_order(self): 152 | # with Stub(PasswordService) as password_service: 153 | # password_service.generate().returns('some') 154 | # 155 | # with Mock(AccountStore) as store: 156 | # store.has_user('John') 157 | # store.has_user('Peter') 158 | # store.save('John', 'some') 159 | # store.save('Peter', 'some') 160 | # 161 | # service = AccountService(store, password_service) 162 | # 163 | # service.create_user('John') 164 | # service.create_user('Peter') 165 | # 166 | # assert_that(store, any_order_verify()) 167 | 168 | def test_stub_delegates(self): 169 | def get_pass(): 170 | return "12345" 171 | 172 | with Stub(PasswordService) as password_service: 173 | password_service.generate().delegates(get_pass) 174 | 175 | store = Spy(AccountStore) 176 | service = AccountService(store, password_service) 177 | 178 | service.create_user('John') 179 | 180 | assert_that(store.save, called().with_args('John', '12345')) 181 | 182 | def test_stub_delegates_list(self): 183 | with Stub(PasswordService) as password_service: 184 | password_service.generate().delegates(["12345", "mypass", "nothing"]) 185 | 186 | store = Spy(AccountStore) 187 | service = AccountService(store, password_service) 188 | 189 | service.create_group('team', ['John', 'Peter', 'Alice']) 190 | 191 | assert_that(store.save, called().with_args('John', '12345')) 192 | assert_that(store.save, called().with_args('Peter', 'mypass')) 193 | assert_that(store.save, called().with_args('Alice', 'nothing')) 194 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37, py38, py39, py310, py311, docs 3 | 4 | [testenv] 5 | deps=nose2 6 | commands=nose2 7 | 8 | [gh-actions] 9 | python = 10 | 3.7: py37 11 | 3.8: py38 12 | 3.9: py39 13 | 3.10: py310 14 | 3.11: py311, docs 15 | 16 | [testenv:docs] 17 | allowlist_externals = make 18 | deps = sphinx 19 | commands = 20 | make -C docs 21 | make -C doctests 22 | 23 | 24 | # using 2to3 with nose 25 | #:https://bitbucket.org/kumar303/fudge/src/9cafce359a21/tox.ini#cl-26 26 | 27 | #[testenv:py34] 28 | # https://bitbucket.org/hpk42/tox/issue/127/possible-problem-with-python34 29 | -------------------------------------------------------------------------------- /unittest.cfg: -------------------------------------------------------------------------------- 1 | [unittest] 2 | test-file-pattern = *_tests.py 3 | --------------------------------------------------------------------------------