diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..a660fe0 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,19 @@ +[bumpversion] +current_version = 1.2.0 +commit = True +message = Bump version to {new_version} +tag = True +tag_name = {new_version} +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? +serialize = + {major}.{minor}.{patch}-{release} + {major}.{minor}.{patch} + +[bumpversion:file:mockito/__init__.py] + +[bumpversion:part:release] +optional_value = release +values = + dev + release + diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..f9e3fba --- /dev/null +++ b/.flake8 @@ -0,0 +1,14 @@ +[flake8] +ignore = + D, + W291, + W293, + W391, + W503, + E302, + E303, + E306, + E731 +exclude = .git,.cache,docs,.env,.build,build +max-complexity = 10 +max-line-length = 79 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 224e7f0..3f5268d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -.pc/ +*.pyc +build +docs/_build +.eggs/ +.pytest_cache +__pycache__ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..75ae86d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +language: python +python: +- '2.7' +- '3.4' +- '3.5' +- '3.6' +- '3.7' +- '3.8' +install: + - if [[ $TRAVIS_PYTHON_VERSION == 3.8 ]]; then pip install flake8; fi + - pip install . +script: + - if [[ $TRAVIS_PYTHON_VERSION == 3.8 ]]; then flake8 .; fi + - py.test +deploy: + provider: pypi + user: herrkaste + password: + secure: ou5t21x3ysjuRA4oj0oEJIiwffkrsKMoyBL0AhBc+Qq7bxFIEMCdTgfkh1lrWrhGA0xNIAwDHOL9gJrpYaqeLUx6F0mCQc2zRfNzYNf/t4x0+23WsIGQ1HxWGCW9ixLmtXU+zFGK6pUoLZjPdCT0HjZsAjgKOudTv4M1+BlUhFnmAvdmBKjl3jfNY4h5JWbVrhPg6HzMgfNI+vQ7JIFjHZ4a0i2BqEbTMt/2UZGal+Mau0sEO3/y4Ud0LcTRhtA6VA0J7nEcv85q+/JhqmbXTs9h6Bz1KC3V4nMPaaIpGqhrX20eLI6fxULlB/yuBq5jNXSvDMeH9lRyv5AlDUy9NAh++JciXSYYp3p984V/LEwRKM3VyB+ZUUe+KeLN7rk6d/Q2elFW9IHpw9cSsmbl1zrG4GjP+eCpCOw0lrLO6MAijSCGXEzWN+5ViwMDrGCS/6CjRRUBRxcXBebeo6ZB6Wkw+JWdFLW3s/OMzDeVtOEkuP6qdR7VMNn2uYOkPbiDZO4d5UGS09gGMWYasqxP/QJth2yuF95uQmqOhLuGSzI02YS6+L1/Xh2fEmsD8LFF3ATfA0MZ/phHjjvD/ZUmnVgGczW9p1zEohJ9EDQsV4P2fHzNP6nblcx7iBTzKsEsqcjTpOn7UYhFAsyiga17dhcfa5IU2nSb0JzzIeWdM0Q= + on: + tags: true + python: 3.8 + branch: master + distributions: sdist bdist_wheel + repo: kaste/mockito-python diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..e6f4b55 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,205 @@ +MOCKITO CHANGE LOG +================== + +Release 1.2.0 (November 25, 2019) +--------------------------------- + +- Code base now is python 3 compatible. No 2to3 anymore. +- Fine tune error messages on unexpected calls or verifications. E.g. if you expect ``when(dog).bark('Wuff')`` but on call time do ``dog.bark('Wufff')``. Likewise, if you call ``dog.bark('Miau')`` and then ``verify(dog).bark('Maui')``. +- @felixonmars fixed a small compatibility issue with python 3.8 +- Mocking properties has become a bit easier. (#26) E.g. + + prop = mock() + when(m).__get__(...).thenReturn(23) + m = mock({'name': prop}) + + +Release 1.1.1 (August 28, 2018) +------------------------------- + +- Fix: The context manager (``with``) has now a proper implementation +- Fix: Calling ``patch`` with two arguments can now be used with ``with`` +- Fix: Do not treat the placeholder arguments (Ellipsis, args, kwargs) as special on call time anymore. (T.i. they only have a meaning when stubbing or verifying.) +- Enhancement: Changed some truthy or equality tests to identity (``is``) tests. This reduces edge-cases where some user object defines ``__eq__`` or ``__bool__``. (Hello _numpy_!) + + +Release 1.1.0 (May 2, 2018) +--------------------------- + +- Added ``forget_invocations`` function. Thanks to @maximkulkin + +This is generally useful if you already call mocks during your setup routine. +Now you could call ``forget_invocations`` at the end of your setup, and +have a clean 'recording' for your actual test code. T.i. you don't have +to count the invocations from your setup code anymore. + + +Release 1.0.12 (June 3, 2017) +----------------------------- + +- Better error messages for failed verifications. By @lwoydziak + + +Release 1.0.7 - 1.0.10 (January 31 - February 2, 2017) +------------------------------------------------------ + +- ``verifyZeroInteractions`` implemented. This is actually a *breaking change*, because ``verifyZeroInteractions`` was an alias for ``verifyNoMoreInteractions`` (sic!). If you used it, just call the other function. + +- ``verifyStubbedInvocationsAreUsed`` implemented. This is meant to be called right before an ``unstub`` and should improve long time maintenance. It doesn't help during design time. Note that `pytest-mockito` automatically calls this for you. + +- All `verify*` functions now warn you if you pass in an object which was never stubbed. + + +Release 1.0.0 - 1.0.5 (January 24 - 27, 2017) +--------------------------------------------- + +This is a major update; mostly because of internal code reorganization (`imports`) it cannot be guaranteed that this will not break for you. Though if you just used the public API you should be fine. None of the vintage old tests have been removed and they at least pass. + +In general unclassified imports (``from mocktio import *``) are not recommended. But if you did, we do not export `Mock` anymore. `Mock` has been deprecated long ago and is now for internal use only. You must use `mock`. + +Another important change is, that *mockito*'s strict mode is far more strict than before. We now generally try to match the signature of the target method +with your usage. Usually this should help you find bugs in your code, because +it will make it easier to spot changing interfaces. + +- ``mock``, ``when``, ``verify`` return mostly empty objects. It is unlikely to have a method_name clash. + +- Specced mocks ``instance = mock(Class)`` will pass isinstance tests like ``isinstance(instance, Class)`` + +- For ``when`` and ``verify`` the function signature or argument matchers can be greatly simplified. E.g. ``when(requests).get(...).thenReturn('OK')`` will match any argument you pass in. There are ``args`` and ``kwargs`` matchers as well. So ``when(requests).get('https://...', **kwargs).thenReturn(...)`` will make an exact match on the first argument, the url, and ignore all the headers and other stuff. + +- Mocks can be preconfigured: ``mock({'text': 'OK'})``. For specced mocks this would be e.g. ``mock({'text': 'OK'}, spec=requests.Response)``. + +- If you mock or patch an object, the function signatures will be matched. So:: + + def foo(a, b=1): ... + + when(main).foo(12) # will pass + when(main).foo(c=13) # will raise immediately + +- Mock Dummies are now callable:: + + m = mock() + m(1, 2) + verify(m).__call__(...) + +- ``Mock()`` is now an implementation detail; it is **not** exported anymore. Use ``mock()``. + +- You can unstub individual patched objects ``unstub(obj)``. (Before it was all or nothing.) + +- Added basic context manager support when using ``when``. Note that ``verify`` has to be called within the with context. + +:: + + with when(rex).waggle().thenReturn('Yup'): + assert rex.waggle() == 'Yup' + verify(rex).waggle() + +- Aliased ``any_`` to ``ANY``, ``args`` to ``ARGS`` and ``kwargs`` to ``KWARGS``. You can use python's builtin ``any`` as a stand in for ``ANY``. + +- As a convenience you can use our ``any_`` matcher like a type instead of ``any_()``:: + + dummy(1) + verify(dummy).__call__(ANY) + +- Added ``when2``, ``expect``, ``spy2`` + +- Make the mocked function (replacement) more inspectable. Copy `__doc__`, `__name__` etc. + +- You can configure magic methods on mocks:: + + dummy = mock() + when(dummy).__getitem__(1).thenReturn(2) + assert dummy[1] == 2 + + + +Release 0.7.1 (December 27, 2016) +--------------------------------- + +- Fix: Allow ``verifyNoMoreInteractions`` call for real (stubbed) objects + + +Release 0.7.0 (July 15, 2016) +----------------------------- + +- Added a ton of new argument matchers. Namely:: + + 'and_', 'or_', 'not_', 'eq', 'neq', 'lt', 'lte', 'gt', 'gte', + 'arg_that', 'matches', 'captor' + +- Aliases ``any`` matcher to ``any_`` because it's a builtin. +- Fixes an issue where mockito could not correctly verify your function invocations, if you grabbed a method from its object and used it ('detached') as a plain function:: + + m = mock() + f = m.foo # detach + f(1, 2) # pass it around and use it like a function + f(2, 3) + verify(m).foo(...) # finally verify interactions + +Thank you @maximkulkin + + +Release 0.6.1 (May 20, 2016) +---------------------------- + +- Added ``thenAnswer(callable)``. The callable will be called to compute the answer the stubbed method will return. For that it will receive the arguments of the caller:: + + m = mock() + when(m).do_times(any(), any()).thenAnswer(lambda one, two: one * two) + self.assertEquals(20, m.do_times(5, 4)) + +Thank you @stubbsd + +Release 0.6.0 (April 25, 2016) +------------------------------ + +- Print keyword arguments nicely. +- Be very forgiving about return values and assume None as default. T.i. ``when(Dog).bark('Miau').thenReturn()`` is enough to return None. +- Make keyword argument order unimportant. +- BREAKING CHANGE: Throw early when calling not expected methods in strict mode. + +Release 0.5.3 (April 23, 2016) +------------------------------ + +- Remove hard coded distribute setup files. + +Release 0.5.1 (August 4, 2010) +------------------------------ +BUG Fixes: + - Fixed issue #9 [http://bitbucket.org/szczepiq/mockito-python/issue/9] : Generating stubs from classes caused method to be replaced in original classes. + +Release 0.5.0 (July 26, 2010) +----------------------------- +API Changes: + - Added possibility to spy on real objects. + - Added "never" syntactic sugar for verifications. + +BUG Fixes: + - Fixed issue with named arguments matching. + +Other Changes: + - Python 2.7 support + - Deprecated APIs now generate deprecation warnings. + +Release 0.4.0 (July 2, 2010) +---------------------------- +API Changes: + - Added possibility to verify invocations in order. + +BUG Fixes: + - Fixed issue with installing mockito from egg without distribute installed. + +Release 0.3.1 +------------- +Bug-fix release. + +Bug Fixes: + - Fixed annoying issue #8 [http://bitbucket.org/szczepiq/mockito-python/issue/8] + +Release 0.3.0 +------------- +API Changes: + - Renamed mock creation method from "Mock" (upper "M") to "mock". Old name stays for compatibility until 1.0 release. +Other Changes: + - Official Python3 support via distutils + 2to3. + diff --git a/LICENSE b/LICENSE index 3b9dd7f..37fd116 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +Copyright (c) 2008-2019 Szczepan Faber, Serhiy Oplakanets, Herr Kaste Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -16,4 +16,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 4872190..0b68611 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ -recursive-include mockito_test *.py - include AUTHORS +include CHANGES.txt include LICENSE include README.rst + +global-exclude *.py[co] \ No newline at end of file diff --git a/PKG-INFO b/PKG-INFO deleted file mode 100644 index 25926ea..0000000 --- a/PKG-INFO +++ /dev/null @@ -1,16 +0,0 @@ -Metadata-Version: 1.0 -Name: mockito -Version: 0.5.2 -Summary: Spying framework -Home-page: http://code.google.com/p/mockito-python -Author: Justin Hopper -Author-email: mockito-python@googlegroups.com -License: MIT -Download-URL: http://code.google.com/p/mockito-python/downloads/list -Description: Mockito is a spying framework based on Java library with the same name. -Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Topic :: Software Development :: Testing -Classifier: Programming Language :: Python :: 3 diff --git a/README.rst b/README.rst index 0894648..299db24 100644 --- a/README.rst +++ b/README.rst @@ -1,60 +1,53 @@ -Mockito is a spying framework based on Java library with the same name. +Mockito is a spying framework originally based on `the Java library with the same name +`_. -1. To install: +.. image:: https://travis-ci.org/kaste/mockito-python.svg?branch=master + :target: https://travis-ci.org/kaste/mockito-python - $ python setup.py install -2. To run all tests: +Install +======= - $ python setup.py test +``pip install mockito`` -3. For more info, see: - __ http://code.google.com/p/mockito-python/ - - Feel free to contribute more documentation or feedback! -4. Our user and developer discussion group is: +Quick Start +=========== - __ http://groups.google.com/group/mockito-python +90% use case is that you want to stub out a side effect. -5. Mockito is licensed under the MIT license +:: -6. Library was tested with the following Python versions: + from mockito import when, mock, unstub - Python 2.4.6 - Python 2.5.4 - Python 2.6.1 - Python 2.7 - Python 3.1.2 - -7. (Generated from mockito_demo_test.py) Basic usage: + when(os.path).exists('/foo').thenReturn(True) - import unittest - from mockito import mock, when, verify - - class DemoTest(unittest.TestCase): - def testStubbing(self): - # create a mock - ourMock = mock() - - # stub it - when(ourMock).getStuff("cool").thenReturn("cool stuff") - - # use the mock - self.assertEqual("cool stuff", ourMock.getStuff("cool")) - - # what happens when you pass different argument? - self.assertEqual(None, ourMock.getStuff("different argument")) - - def testVerification(self): - # create a mock - theMock = mock() - - # use the mock - theMock.doStuff("cool") - - # verify the interactions. Method and parameters must match. Otherwise verification error. - verify(theMock).doStuff("cool") - - \ No newline at end of file + # or: + import requests # the famous library + # you actually want to return a Response-like obj, we'll fake it + response = mock({'status_code': 200, 'text': 'Ok'}) + when(requests).get(...).thenReturn(response) + + # use it + requests.get('http://google.com/') + + # clean up + unstub() + + + + +Read the docs +============= + +http://mockito-python.readthedocs.io/en/latest/ + + +Run the tests +------------- + +:: + + pip install pytest + py.test diff --git a/bump.bat b/bump.bat new file mode 100644 index 0000000..faefee6 --- /dev/null +++ b/bump.bat @@ -0,0 +1,2 @@ +bumpversion release --tag +bumpversion patch --no-tag --message "master is {new_version}" \ No newline at end of file diff --git a/distribute_setup.py b/distribute_setup.py deleted file mode 100644 index 0021336..0000000 --- a/distribute_setup.py +++ /dev/null @@ -1,477 +0,0 @@ -#!python -"""Bootstrap distribute installation - -If you want to use setuptools in your package's setup.py, just include this -file in the same directory with it, and add this to the top of your setup.py:: - - from distribute_setup import use_setuptools - use_setuptools() - -If you want to require a specific version of setuptools, set a download -mirror, or use an alternate download directory, you can do so by supplying -the appropriate options to ``use_setuptools()``. - -This file can also be run as a script to install or upgrade setuptools. -""" -import os -import sys -import time -import fnmatch -import tempfile -import tarfile -from distutils import log - -try: - from site import USER_SITE -except ImportError: - USER_SITE = None - -try: - import subprocess - - def _python_cmd(*args): - args = (sys.executable,) + args - return subprocess.call(args) == 0 - -except ImportError: - # will be used for python 2.3 - def _python_cmd(*args): - args = (sys.executable,) + args - # quoting arguments if windows - if sys.platform == 'win32': - def quote(arg): - if ' ' in arg: - return '"%s"' % arg - return arg - args = [quote(arg) for arg in args] - return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 - -DEFAULT_VERSION = "0.6.10" -DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" -SETUPTOOLS_FAKED_VERSION = "0.6c11" - -SETUPTOOLS_PKG_INFO = """\ -Metadata-Version: 1.0 -Name: setuptools -Version: %s -Summary: xxxx -Home-page: xxx -Author: xxx -Author-email: xxx -License: xxx -Description: xxx -""" % SETUPTOOLS_FAKED_VERSION - - -def _install(tarball): - # extracting the tarball - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - tar = tarfile.open(tarball) - _extractall(tar) - tar.close() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - - # installing - log.warn('Installing Distribute') - if not _python_cmd('setup.py', 'install'): - log.warn('Something went wrong during the installation.') - log.warn('See the error message above.') - finally: - os.chdir(old_wd) - - -def _build_egg(egg, tarball, to_dir): - # extracting the tarball - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - tar = tarfile.open(tarball) - _extractall(tar) - tar.close() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - - # building an egg - log.warn('Building a Distribute egg in %s', to_dir) - _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) - - finally: - os.chdir(old_wd) - # returning the result - log.warn(egg) - if not os.path.exists(egg): - raise IOError('Could not build the egg.') - - -def _do_download(version, download_base, to_dir, download_delay): - egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' - % (version, sys.version_info[0], sys.version_info[1])) - if not os.path.exists(egg): - tarball = download_setuptools(version, download_base, - to_dir, download_delay) - _build_egg(egg, tarball, to_dir) - sys.path.insert(0, egg) - import setuptools - setuptools.bootstrap_install_from = egg - - -def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, download_delay=15, no_fake=True): - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - was_imported = 'pkg_resources' in sys.modules or \ - 'setuptools' in sys.modules - try: - try: - import pkg_resources - if not hasattr(pkg_resources, '_distribute'): - if not no_fake: - _fake_setuptools() - raise ImportError - except ImportError: - return _do_download(version, download_base, to_dir, download_delay) - try: - pkg_resources.require("distribute>="+version) - return - except pkg_resources.VersionConflict: - e = sys.exc_info()[1] - if was_imported: - sys.stderr.write( - "The required version of distribute (>=%s) is not available,\n" - "and can't be installed while this script is running. Please\n" - "install a more recent version first, using\n" - "'easy_install -U distribute'." - "\n\n(Currently using %r)\n" % (version, e.args[0])) - sys.exit(2) - else: - del pkg_resources, sys.modules['pkg_resources'] # reload ok - return _do_download(version, download_base, to_dir, - download_delay) - except pkg_resources.DistributionNotFound: - return _do_download(version, download_base, to_dir, - download_delay) - finally: - if not no_fake: - _create_fake_setuptools_pkg_info(to_dir) - -def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, delay=15): - """Download distribute from a specified location and return its filename - - `version` should be a valid distribute version number that is available - as an egg for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - `delay` is the number of seconds to pause before an actual download - attempt. - """ - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - try: - from urllib.request import urlopen - except ImportError: - from urllib2 import urlopen - tgz_name = "distribute-%s.tar.gz" % version - url = download_base + tgz_name - saveto = os.path.join(to_dir, tgz_name) - src = dst = None - if not os.path.exists(saveto): # Avoid repeated downloads - try: - log.warn("Downloading %s", url) - src = urlopen(url) - # Read/write all in one block, so we don't create a corrupt file - # if the download is interrupted. - data = src.read() - dst = open(saveto, "wb") - dst.write(data) - finally: - if src: - src.close() - if dst: - dst.close() - return os.path.realpath(saveto) - - -def _patch_file(path, content): - """Will backup the file then patch it""" - existing_content = open(path).read() - if existing_content == content: - # already patched - log.warn('Already patched.') - return False - log.warn('Patching...') - _rename_path(path) - f = open(path, 'w') - try: - f.write(content) - finally: - f.close() - return True - - -def _same_content(path, content): - return open(path).read() == content - -def _no_sandbox(function): - def __no_sandbox(*args, **kw): - try: - from setuptools.sandbox import DirectorySandbox - def violation(*args): - pass - DirectorySandbox._old = DirectorySandbox._violation - DirectorySandbox._violation = violation - patched = True - except ImportError: - patched = False - - try: - return function(*args, **kw) - finally: - if patched: - DirectorySandbox._violation = DirectorySandbox._old - del DirectorySandbox._old - - return __no_sandbox - -@_no_sandbox -def _rename_path(path): - new_name = path + '.OLD.%s' % time.time() - log.warn('Renaming %s into %s', path, new_name) - os.rename(path, new_name) - return new_name - -def _remove_flat_installation(placeholder): - if not os.path.isdir(placeholder): - log.warn('Unkown installation at %s', placeholder) - return False - found = False - for file in os.listdir(placeholder): - if fnmatch.fnmatch(file, 'setuptools*.egg-info'): - found = True - break - if not found: - log.warn('Could not locate setuptools*.egg-info') - return - - log.warn('Removing elements out of the way...') - pkg_info = os.path.join(placeholder, file) - if os.path.isdir(pkg_info): - patched = _patch_egg_dir(pkg_info) - else: - patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) - - if not patched: - log.warn('%s already patched.', pkg_info) - return False - # now let's move the files out of the way - for element in ('setuptools', 'pkg_resources.py', 'site.py'): - element = os.path.join(placeholder, element) - if os.path.exists(element): - _rename_path(element) - else: - log.warn('Could not find the %s element of the ' - 'Setuptools distribution', element) - return True - - -def _after_install(dist): - log.warn('After install bootstrap.') - placeholder = dist.get_command_obj('install').install_purelib - _create_fake_setuptools_pkg_info(placeholder) - -@_no_sandbox -def _create_fake_setuptools_pkg_info(placeholder): - if not placeholder or not os.path.exists(placeholder): - log.warn('Could not find the install location') - return - pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) - setuptools_file = 'setuptools-%s-py%s.egg-info' % \ - (SETUPTOOLS_FAKED_VERSION, pyver) - pkg_info = os.path.join(placeholder, setuptools_file) - if os.path.exists(pkg_info): - log.warn('%s already exists', pkg_info) - return - - log.warn('Creating %s', pkg_info) - f = open(pkg_info, 'w') - try: - f.write(SETUPTOOLS_PKG_INFO) - finally: - f.close() - - pth_file = os.path.join(placeholder, 'setuptools.pth') - log.warn('Creating %s', pth_file) - f = open(pth_file, 'w') - try: - f.write(os.path.join(os.curdir, setuptools_file)) - finally: - f.close() - -def _patch_egg_dir(path): - # let's check if it's already patched - pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') - if os.path.exists(pkg_info): - if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): - log.warn('%s already patched.', pkg_info) - return False - _rename_path(path) - os.mkdir(path) - os.mkdir(os.path.join(path, 'EGG-INFO')) - pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') - f = open(pkg_info, 'w') - try: - f.write(SETUPTOOLS_PKG_INFO) - finally: - f.close() - return True - - -def _before_install(): - log.warn('Before install bootstrap.') - _fake_setuptools() - - -def _under_prefix(location): - if 'install' not in sys.argv: - return True - args = sys.argv[sys.argv.index('install')+1:] - for index, arg in enumerate(args): - for option in ('--root', '--prefix'): - if arg.startswith('%s=' % option): - top_dir = arg.split('root=')[-1] - return location.startswith(top_dir) - elif arg == option: - if len(args) > index: - top_dir = args[index+1] - return location.startswith(top_dir) - elif option == '--user' and USER_SITE is not None: - return location.startswith(USER_SITE) - return True - - -def _fake_setuptools(): - log.warn('Scanning installed packages') - try: - import pkg_resources - except ImportError: - # we're cool - log.warn('Setuptools or Distribute does not seem to be installed.') - return - ws = pkg_resources.working_set - try: - setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', - replacement=False)) - except TypeError: - # old distribute API - setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) - - if setuptools_dist is None: - log.warn('No setuptools distribution found') - return - # detecting if it was already faked - setuptools_location = setuptools_dist.location - log.warn('Setuptools installation detected at %s', setuptools_location) - - # if --root or --preix was provided, and if - # setuptools is not located in them, we don't patch it - if not _under_prefix(setuptools_location): - log.warn('Not patching, --root or --prefix is installing Distribute' - ' in another location') - return - - # let's see if its an egg - if not setuptools_location.endswith('.egg'): - log.warn('Non-egg installation') - res = _remove_flat_installation(setuptools_location) - if not res: - return - else: - log.warn('Egg installation') - pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') - if (os.path.exists(pkg_info) and - _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): - log.warn('Already patched.') - return - log.warn('Patching...') - # let's create a fake egg replacing setuptools one - res = _patch_egg_dir(setuptools_location) - if not res: - return - log.warn('Patched done.') - _relaunch() - - -def _relaunch(): - log.warn('Relaunching...') - # we have to relaunch the process - args = [sys.executable] + sys.argv - sys.exit(subprocess.call(args)) - - -def _extractall(self, path=".", members=None): - """Extract all members from the archive to the current working - directory and set owner, modification time and permissions on - directories afterwards. `path' specifies a different directory - to extract to. `members' is optional and must be a subset of the - list returned by getmembers(). - """ - import copy - import operator - from tarfile import ExtractError - directories = [] - - if members is None: - members = self - - for tarinfo in members: - if tarinfo.isdir(): - # Extract directories with a safe mode. - directories.append(tarinfo) - tarinfo = copy.copy(tarinfo) - tarinfo.mode = 448 # decimal for oct 0700 - self.extract(tarinfo, path) - - # Reverse sort directories. - if sys.version_info < (2, 4): - def sorter(dir1, dir2): - return cmp(dir1.name, dir2.name) - directories.sort(sorter) - directories.reverse() - else: - directories.sort(key=operator.attrgetter('name'), reverse=True) - - # Set correct owner, mtime and filemode on directories. - for tarinfo in directories: - dirpath = os.path.join(path, tarinfo.name) - try: - self.chown(tarinfo, dirpath) - self.utime(tarinfo, dirpath) - self.chmod(tarinfo, dirpath) - except ExtractError: - e = sys.exc_info()[1] - if self.errorlevel > 1: - raise - else: - self._dbg(1, "tarfile: %s" % e) - - -def main(argv, version=DEFAULT_VERSION): - """Install or upgrade setuptools and EasyInstall""" - tarball = download_setuptools() - _install(tarball) - - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..9c47b4f --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,230 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) + $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/mockito-python.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/mockito-python.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/mockito-python" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/mockito-python" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/_static/alabaster.css b/docs/_static/alabaster.css new file mode 100644 index 0000000..082fc4f --- /dev/null +++ b/docs/_static/alabaster.css @@ -0,0 +1,713 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 940px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 940px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: 'Garamond', 'Georgia', serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; + font-size: 1em; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: ; + border-bottom: 1px solid #fafafa; +} + +dd div.admonition { +/* margin-left: -60px; + padding-left: 60px; +*/} + +div.admonition p.admonition-title { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + font-size: 18px; + margin: 0 0 10px 0; + padding: 0; + /*line-height: 1;*/ +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: #fff; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #EEE; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +dl dl pre { + margin-left: -90px; + padding-left: 90px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Make nested-list/multi-paragraph items look better in Releases changelog + * pages. Without this, docutils' magical list fuckery causes inconsistent + * formatting between different release sub-lists. + */ +div#changelog > div.section > ul > li > p:only-child { + margin-bottom: 0; +} + +/* Hide fugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +code { + background-color: #eee; +} +code span.pre { + padding: 0px 4px; +} + +dl { + margin: 3em 0; +} + +div.note { + background-color: #dceef3; + border: 1px solid #85aabf; +} + +dt:target, .highlighted { + background-color: #fbe54e; +} \ No newline at end of file diff --git a/docs/changes.rst b/docs/changes.rst new file mode 100644 index 0000000..86cad0f --- /dev/null +++ b/docs/changes.rst @@ -0,0 +1 @@ +.. include:: ../CHANGES.txt \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..f7e3bbb --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- +# +# mockito-python documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 26 14:00:19 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import pkg_resources + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + # 'sphinx.ext.githubpages', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'mockito-python' +copyright = u'2016, Szczepan Faber, Serhiy Oplakanets, herr.kaste' +author = u'Szczepan Faber, Serhiy Oplakanets, herr.kaste' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +# version = u'0.6' +# The full version, including alpha/beta/rc tags. +# release = u'0.6.1' + +try: + release = pkg_resources.get_distribution('mockito').version +except pkg_resources.DistributionNotFound: + print('mockito must be installed to build the documentation.') + print('Install from source using `pip install -e .` in a virtualenv.') + sys.exit(1) + +if 'dev' in release: + release = ''.join(release.partition('dev')[:2]) + +version = '.'.join(release.split('.')[:2]) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +#html_title = u'mockito-python v0.6.1' + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +#html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} +html_sidebars = { '**': ['localtoc.html', 'relations.html', 'searchbox.html'], } + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'mockito-pythondoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'mockito-python.tex', u'mockito-python Documentation', + u'Szczepan Faber, Serhiy Oplakanets, herr.kaste', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'mockito-python', u'mockito-python Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'mockito-python', u'mockito-python Documentation', + author, 'mockito-python', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..a34f338 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,110 @@ +.. mockito-python documentation master file, created by + sphinx-quickstart on Tue Apr 26 14:00:19 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +.. module:: mockito + +Mockito is a spying framework originally based on the Java library with the same name. + +.. image:: https://travis-ci.org/kaste/mockito-python.svg?branch=master + :target: https://travis-ci.org/kaste/mockito-python + + +Install +------- + +.. code-block:: python + + pip install mockito + +If you already use `pytest`, consider using the plugin `pytest-mockito `_. + + +Use +--- + +.. code-block:: python + + from mockito import when, mock, unstub + + when(os.path).exists('/foo').thenReturn(True) + + # or: + import requests # the famous library + # you actually want to return a Response-like obj, we'll fake it + response = mock({'status_code': 200, 'text': 'Ok'}) + when(requests).get(...).thenReturn(response) + + # use it + requests.get('http://google.com/') + + # clean up + unstub() + + +Features +-------- + +Super easy to set up different answers. + +:: + + # Well, you know the internet + when(requests).get(...).thenReturn(mock({'status': 501})) \ + .thenRaise(Timeout("I'm flaky")) \ + .thenReturn(mock({'status': 200, 'text': 'Ok'})) + +State-of-the-art, high-five argument matchers:: + + # Use the Ellipsis, if you don't care + when(deferred).defer(...).thenRaise(Timeout) + + # Or **kwargs + from mockito import kwargs # or KWARGS + when(requests).get('http://my-api.com/user', **kwargs) + + # The usual matchers + from mockito import ANY, or_, not_ + number = or_(ANY(int), ANY(float)) + when(math).sqrt(not_(number)).thenRaise( + TypeError('argument must be a number')) + +No need to `verify` (`assert_called_with`) all the time:: + + # Different arguments, different answers + when(foo).bar(1).thenReturn(2) + when(foo).bar(2).thenReturn(3) + + # but: + foo.bar(3) # throws immediately: unexpected invocation + + # because of that you just know that when + # you get a `2`, you called it with `1` + + +Signature checking:: + + # when stubbing + when(requests).get() # throws immediately: TypeError url required + + # when calling + request.get(location='http://example.com/') # TypeError + + +Read +---- + +.. toctree:: + :maxdepth: 1 + + walk-through + recipes + the-functions + the-matchers + Changelog + + + +Report issues, contribute more documentation or give feedback at `Github `_! + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..7799067 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,281 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. epub3 to make an epub3 + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + echo. dummy to check syntax errors of document sources + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\mockito-python.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\mockito-python.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "epub3" ( + %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +if "%1" == "dummy" ( + %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. Dummy builder generates no files. + goto end +) + +:end diff --git a/docs/nutshell.rst b/docs/nutshell.rst new file mode 100644 index 0000000..2da28e4 --- /dev/null +++ b/docs/nutshell.rst @@ -0,0 +1,139 @@ +TL;DR +----- + + +:: + + >>> from mockito import * + >>> myMock = mock() + >>> when(myMock).getStuff().thenReturn('stuff') + + >>> myMock.getStuff() + 'stuff' + >>> verify(myMock).getStuff() + + >>> when(myMock).doSomething().thenRaise(Exception('Did a bad thing')) + + >>> myMock.doSomething() + Traceback (most recent call last): + <...> + Exception: Did a bad thing + +No difference whatsoever when you mock modules + +:: + + >>> import os.path + >>> when(os.path).exists('somewhere/somewhat').thenReturn(True) + + >>> when(os.path).exists('somewhere/something').thenReturn(False) + + >>> os.path.exists('somewhere/somewhat') + True + >>> os.path.exists('somewhere/something') + False + >>> os.path.exists('another_place') + Traceback (most recent call last): + <...> + mockito.invocation.InvocationError: You called exists with ('another_place',) as + arguments but we did not expect that. + + >>> when(os.path).exist('./somewhat').thenReturn(True) + Traceback (most recent call last): + <...> + mockito.invocation.InvocationError: You tried to stub a method 'exist' the objec + t () doesn't have. + +If that's too strict, you can change it + +:: + + >>> when(os.path, strict=False).exist('another_place').thenReturn('well, nice he + re') + + >>> os.path.exist('another_place') + 'well, nice here' + >>> os.path.exist('and here?') + >>> + +No surprise, you can do the same with your classes + +:: + + >>> class Dog(object): + ... def bark(self): + ... return "Wau" + ... + >>> when(Dog).bark().thenReturn('Miau!') + + >>> rex = Dog() + >>> rex.bark() + 'Miau!' + +or just with instances, first unstub + +:: + + >>> unstub() + >>> rex.bark() + 'Wau' + +then do + +:: + + >>> when(rex).bark().thenReturn('Grrrrr').thenReturn('Wuff') + + +and get something different on consecutive calls + +:: + + >>> rex.bark() + 'Grrrrr' + >>> rex.bark() + 'Wuff' + >>> rex.bark() + 'Wuff' + +and since you stubbed an instance, a different instance will not be stubbed + +:: + + >>> bello = Dog() + >>> bello.bark() + 'Wau' + +You have 4 modifiers when verifying + +:: + + >>> verify(rex, times=3).bark() + >>> verify(rex, atleast=1).bark() + >>> verify(rex, atmost=3).bark() + >>> verify(rex, between=[1,3]).bark() + >>> + +Finally, we have two matchers + +:: + + >>> myMock = mock() + >>> when(myMock).do(any(int)).thenReturn('A number') + + >>> when(myMock).do(any(str)).thenReturn('A string') + + >>> myMock.do(2) + 'A number' + >>> myMock.do('times') + 'A string' + + >>> verify(myMock).do(any(int)) + >>> verify(myMock).do(any(str)) + >>> verify(myMock).do(contains('time')) + + >>> exit() + +.. toctree:: + :maxdepth: 2 + diff --git a/docs/recipes.rst b/docs/recipes.rst new file mode 100644 index 0000000..6a43efa --- /dev/null +++ b/docs/recipes.rst @@ -0,0 +1,75 @@ +.. module:: mockito + + +Recipes +======= + + +Classes as factories +-------------------- + +We want to test the following code:: + + import requests + + def fetch(url): + session = requests.Session() + return session.get(url) + +In a traditional sense this code is not designed for *testability*. But we don't care here. + +Python has no `new` keyword to get fresh instances from classes. Man, that was a good decision, Guido! So the uppercase `S` in `requests.Session()` doesn't have to stop us in any way. It looks like a function call, and we treat it like such: The plan is to replace `Session` with a factory function that returns a (mocked) session:: + + from mockito import when, mock, verifyStubbedInvocationsAreUsed + + def test_fetch(unstub): + url = 'http://example.com/' + response = mock({'text': 'Ok'}, spec=requests.Response) + # remember: `mock` here just creates an empty object specced after + # requests.Session + session = mock(requests.Session) + # `when` here configures the mock + when(session).get(url).thenReturn(response) + # `when` *patches* the globally available *requests* module + when(requests).Session().thenReturn(session) # <= + + res = fetch(url) + assert res.text == 'Ok' + + # no need to verify anything here, if we get the expected response + # back, `url` must have been passed through the system, otherwise + # mockito would have thrown. + # We *could* ensure that our mocks are actually used, if we want: + verifyStubbedInvocationsAreUsed() + + +Faking magic methods +-------------------- + +We want to test the following code:: + + import requests + + def fetch_2(url): + with requests.Session() as session: + return session.get(url) + +It's basically the same problem, but we need to add support for the context manager, the `with` interface:: + + from mockito import when, mock, args + + def test_fetch_with(unstub): + url = 'http://example.com/' + response = mock({'text': 'Ok'}, spec=requests.Response) + + session = mock(requests.Session) + when(session).get(url).thenReturn(response) + when(session).__enter__().thenReturn(session) # <= + when(session).__exit__(*args) # <= + + when(requests).Session().thenReturn(session) + + res = fetch_2(url) + assert res.text == 'Ok' + + diff --git a/docs/the-functions.rst b/docs/the-functions.rst new file mode 100644 index 0000000..e5cd7df --- /dev/null +++ b/docs/the-functions.rst @@ -0,0 +1,27 @@ +.. module:: mockito + + +The functions +============= + +Stable entrypoints are: :func:`when`, :func:`mock`, :func:`unstub`, :func:`verify`, :func:`spy`. Experimental or new function introduces with v1.0.x are: :func:`when2`, :func:`expect`, :func:`verifyNoUnwantedInteractions`, :func:`verifyStubbedInvocationsAreUsed`, :func:`patch` + +.. autofunction:: when +.. autofunction:: when2 +.. autofunction:: patch +.. autofunction:: expect +.. autofunction:: mock +.. autofunction:: unstub +.. autofunction:: forget_invocations +.. autofunction:: spy +.. autofunction:: spy2 + +This looks like a plethora of verification functions, and especially since you often don't need to `verify` at all. + +.. autofunction:: verify +.. autofunction:: verifyNoMoreInteractions +.. autofunction:: verifyZeroInteractions +.. autofunction:: verifyNoUnwantedInteractions +.. autofunction:: verifyStubbedInvocationsAreUsed + + diff --git a/docs/the-matchers.rst b/docs/the-matchers.rst new file mode 100644 index 0000000..029b1b8 --- /dev/null +++ b/docs/the-matchers.rst @@ -0,0 +1,6 @@ +The matchers +============ + + +.. automodule:: mockito.matchers + :members: diff --git a/docs/walk-through.rst b/docs/walk-through.rst new file mode 100644 index 0000000..3a18c99 --- /dev/null +++ b/docs/walk-through.rst @@ -0,0 +1,204 @@ +The Walk-through +================ + +The 90% use case is that want to stub out a side effect. This is also known as (monkey-)patching. With mockito, it's:: + + from mockito import when + + # stub `os.path.exists` + when(os.path).exists('/foo').thenReturn(True) + + os.path.exists('/foo') # => True + os.path.exists('/bar') # -> throws unexpected invocation + +So in difference to traditional patching, in mockito you always specify concrete arguments (a call signature), and its outcome, usually a return value via `thenReturn` or a raised exception via `thenRaise`. That effectively turns function calls into constants for the time of the test. + +Do **not** forget to :func:`unstub` of course! + +:: + + from mockito import unstub + unstub() # restore os.path module + + +Now we mix global module patching with mocks. We want to test the following function using the fab `requests` library:: + + import requests + + def get_text(url): + res = requests.get(url) + if 200 <= res.status_code < 300: + return res.text + return None + +How, dare, we did not inject our dependencies! Obviously we can get over that by patching at the module level like before:: + + when(requests).get('https://example.com/api').thenReturn(...) + +But what should we return? We know it's a `requests.Response` object, (Actually I know this bc I typed this in the ipython REPL first.) But how to construct such a `Response`, its `__init__` doesn't even take any arguments? + +Should we actually use a 'real' response object? No, we fake it using :func:`mock`. + +:: + + # setup + response = mock({ + 'status_code': 200, + 'text': 'Ok' + }, spec=requests.Response) + when(requests).get('https://example.com/api').thenReturn(response) + + # run + assert get_text('https://example.com/api') == 'Ok' + + # done! + +Say you want to mock the class Dog:: + + class Dog(object): + def bark(self): + return 'Wuff' + + + # either mock the class + when(Dog).bark().thenReturn('Miau!') + # now all instances have a different behavior + rex = Dog() + assert rex.bark() == 'Miau!' + + # or mock a concrete instance + when(rex).bark().thenReturn('Grrrr') + assert rex.bark() == 'Grrrr' + # a different dog will still 'Miau!' + assert Dog().bark() == 'Miau!' + + # be sure to call unstub() once in while + unstub() + + +Sure, you can verify your interactions:: + + from mockito import verify + # once again + rex = Dog() + when(rex).bark().thenReturn('Grrrr') + + rex.bark() + rex.bark() + + # `times` defaults to 1 + verify(rex, times=2).bark() + + +In general mockito is very picky:: + + # this will fail because `Dog` has no method named `waggle` + when(rex).waggle().thenReturn('Nope') + # this will fail because `bark` does not take any arguments + when(rex).bark('Grrr').thenReturn('Nope') + + + # given this function + def bark(sound, post='!'): + return sound + post + + from mockito import kwargs + when(main).bark('Grrr', **kwargs).thenReturn('Nope') + + # now this one will fail + bark('Grrr') # because there are no keyword arguments used + # this one will fail because `then` does not match the function signature + bark('Grrr', then='!!') + # this one will go + bark('Grrr', post='?') + + # there is also an args matcher + def add_tasks(*tasks, verbose=False): + pass + + from mockito import args + # If you omit the `thenReturn` it will just return `None` + when(main).add_tasks(*args) + + add_tasks('task1', 'task2') # will go + add_tasks() # will fail + add_tasks('task1', verbose=True) # will fail too + + # On Python 3 you can also use `...` + when(main).add_tasks(...) + # when(main).add_tasks(Ellipsis) on Python 2 + + add_tasks('task1') # will go + add_tasks(verbose=True) # will go + add_tasks('task1', verbose=True) # will go + add_tasks() # will go + + +To start with an empty stub use :func:`mock`:: + + from mockito import mock + + obj = mock() + + # pass it around, eventually it will be used + obj.say('Hi') + + # back in the tests, verify the interactions + verify(obj).say('Hi') + + # by default all invoked methods take any arguments and return None + # you can configure your expected method calls with the ususal `when` + when(obj).say('Hi').thenReturn('Ho') + + # There is also a shortcut to set some attributes + obj = mock({ + 'hi': 'ho' + }) + + assert obj.hi == 'ho' + + # This would work for methods as well; in this case + obj = mock({ + 'say': lambda _: 'Ho' + }) + + # But you don't have any argument and signature matching + assert obj.say('Anything') == 'Ho' + + # At least you can verify your calls + verify(obj).say(...) + + # Btw, you can make screaming strict mocks:: + obj = mock(strict=True) # every unconfigured, unexpected call will raise + + +You can use an empty stub specced against a concrete class:: + + # Given the above `Dog` + rex = mock(Dog) + + # Now you can stub out any known method on `Dog` but other will throw + when(rex).bark().thenReturn('Miau') + # this one will fail + when(rex).waggle() + + # These mocks are in general very strict, so even this will fail + rex.health # unconfigured attribute + + # Of course you can just set it in a setup routine + rex.health = 121 + + # Or again preconfigure + rex = mock({'health': 121}, spec=Dog) + + # preconfigure stubbed method + rex = mock({'bark': lambda sound: 'Miau'}, spec=Dog) + + # as you specced the mock, you get at least function signature matching + # `bark` does not take any arguments so + rex.bark('sound') # will throw TypeError + + # Btw, you can make loose specced mocks:: + rex = mock(Dog, strict=False) + + diff --git a/mockito/__init__.py b/mockito/__init__.py index 6939a50..d6a711f 100644 --- a/mockito/__init__.py +++ b/mockito/__init__.py @@ -1,40 +1,71 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. '''Mockito is a Test Spy framework.''' -from mockito import mock, verify, verifyNoMoreInteractions, verifyZeroInteractions, when, unstub, ArgumentError -import inorder -from spying import spy -from verification import VerificationError +from .mockito import ( + when, + when2, + patch, + expect, + unstub, + forget_invocations, + verify, + verifyNoMoreInteractions, + verifyZeroInteractions, + verifyNoUnwantedInteractions, + verifyStubbedInvocationsAreUsed, + ArgumentError, +) +from . import inorder +from .spying import spy, spy2 +from .mocking import mock +from .verification import VerificationError -# Imports for compatibility -from mocking import Mock -from matchers import any, contains, times # use package import (``from mockito.matchers import any, contains``) instead of ``from mockito import any, contains`` -from verification import never +from .matchers import * # noqa: F401 F403 +from .matchers import any, contains, times +from .verification import never -__all__ = ['mock', 'spy', 'verify', 'verifyNoMoreInteractions', 'verifyZeroInteractions', 'inorder', 'when', 'unstub', 'VerificationError', 'ArgumentError', - 'Mock', # deprecated - 'any', # compatibility - 'contains', # compatibility - 'never', # compatibility - 'times' # deprecated - ] +__version__ = '1.2.0' + +__all__ = [ + 'mock', + 'spy', + 'spy2', + 'when', + 'when2', + 'patch', + 'expect', + 'verify', + 'verifyNoMoreInteractions', + 'verifyZeroInteractions', + 'verifyNoUnwantedInteractions', + 'verifyStubbedInvocationsAreUsed', + 'inorder', + 'unstub', + 'forget_invocations', + 'VerificationError', + 'ArgumentError', + 'any', # compatibility + 'contains', # compatibility + 'never', # compatibility + 'times', # deprecated +] diff --git a/mockito/inorder.py b/mockito/inorder.py index 60ecad2..f1ed6db 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -1,29 +1,27 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. -from mockito import verify as verify_main - -__author__ = "Serhiy Oplakanets " +from .mockito import verify as verify_main def verify(object, *args, **kwargs): - kwargs['inorder'] = True - return verify_main(object, *args, **kwargs) + kwargs['inorder'] = True + return verify_main(object, *args, **kwargs) diff --git a/mockito/invocation.py b/mockito/invocation.py index 1bcbbaf..10954c5 100644 --- a/mockito/invocation.py +++ b/mockito/invocation.py @@ -1,188 +1,441 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import matchers - - -class InvocationError(AssertionError): +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from . import matchers +import operator +from . import signature +from . import verification as verificationModule +from .utils import contains_strict + +from collections import deque +import functools + + +class InvocationError(AttributeError): pass + +__tracebackhide__ = operator.methodcaller( + "errisinstance", + (InvocationError, verificationModule.VerificationError) +) + + class Invocation(object): - def __init__(self, mock, method_name): - self.method_name = method_name - self.mock = mock - self.verified = False - self.verified_inorder = False - self.params = () - self.named_params = {} - self.answers = [] - self.strict = mock.strict - - def _remember_params(self, params, named_params): - self.params = params - self.named_params = named_params - - def __repr__(self): - return self.method_name + "(" + ", ".join([repr(p) for p in self.params]) + ")" - - def answer_first(self): - return self.answers[0].answer() - + def __init__(self, mock, method_name): + self.mock = mock + self.method_name = method_name + self.strict = mock.strict + + self.params = () + self.named_params = {} + + def _remember_params(self, params, named_params): + self.params = params + self.named_params = named_params + + def __repr__(self): + args = [repr(p) if p is not Ellipsis else '...' + for p in self.params] + kwargs = ["%s=%r" % (key, val) + if key is not matchers.KWARGS_SENTINEL else '**kwargs' + for key, val in self.named_params.items()] + params = ", ".join(args + kwargs) + return "%s(%s)" % (self.method_name, params) + + +class RememberedInvocation(Invocation): + def __init__(self, mock, method_name): + super(RememberedInvocation, self).__init__(mock, method_name) + self.verified = False + self.verified_inorder = False + + def ensure_mocked_object_has_method(self, method_name): + if not self.mock.has_method(method_name): + raise InvocationError( + "You tried to call a method '%s' the object (%s) doesn't " + "have." % (method_name, self.mock.mocked_obj)) + + def ensure_signature_matches(self, method_name, args, kwargs): + sig = self.mock.get_signature(method_name) + if not sig: + return + + signature.match_signature(sig, args, kwargs) + + def __call__(self, *params, **named_params): + if self.strict: + self.ensure_mocked_object_has_method(self.method_name) + self.ensure_signature_matches( + self.method_name, params, named_params) + + self._remember_params(params, named_params) + self.mock.remember(self) + + for matching_invocation in self.mock.stubbed_invocations: + if matching_invocation.matches(self): + matching_invocation.should_answer(self) + return matching_invocation.answer_first( + *params, **named_params) + + if self.strict: + stubbed_invocations = [ + invoc + for invoc in self.mock.stubbed_invocations + if invoc.method_name == self.method_name + ] + raise InvocationError( + """ +Called but not expected: + + %s + +Stubbed invocations are: + + %s + +""" + % ( + self, + "\n ".join( + str(invoc) for invoc in reversed(stubbed_invocations) + ) + ) + ) + + return None + + +class RememberedProxyInvocation(Invocation): + '''Remeber params and proxy to method of original object. + + Calls method on original object and returns it's return value. + ''' + def __init__(self, mock, method_name): + super(RememberedProxyInvocation, self).__init__(mock, method_name) + self.verified = False + self.verified_inorder = False + + def __call__(self, *params, **named_params): + self._remember_params(params, named_params) + self.mock.remember(self) + obj = self.mock.spec + try: + method = getattr(obj, self.method_name) + except AttributeError: + raise AttributeError( + "You tried to call method '%s' which '%s' instance does not " + "have." % (self.method_name, obj)) + return method(*params, **named_params) + + class MatchingInvocation(Invocation): - @staticmethod - def compare(p1, p2): - if isinstance(p1, matchers.Matcher): - if not p1.matches(p2): return False - elif p1 != p2: return False - return True - - def matches(self, invocation): - if self.method_name != invocation.method_name: - return False - if len(self.params) != len(invocation.params): - return False - if len(self.named_params) != len(invocation.named_params): - return False - if self.named_params.keys() != invocation.named_params.keys(): - return False - - for x, p1 in enumerate(self.params): - if not self.compare(p1, invocation.params[x]): - return False - - for x, p1 in self.named_params.iteritems(): - if not self.compare(p1, invocation.named_params[x]): - return False - - return True - -class RememberedInvocation(Invocation): - def __call__(self, *params, **named_params): - self._remember_params(params, named_params) - self.mock.remember(self) - - for matching_invocation in self.mock.stubbed_invocations: - if matching_invocation.matches(self): - return matching_invocation.answer_first() - - return None - -class RememberedProxyInvocation(Invocation): - '''Remeber params and proxy to method of original object. - - Calls method on original object and returns it's return value. - ''' - def __call__(self, *params, **named_params): - self._remember_params(params, named_params) - self.mock.remember(self) - obj = self.mock.original_object - try: - method = getattr(obj, self.method_name) - except AttributeError: - raise AttributeError("You tried to call method '%s' which '%s' instance does not have." % (self.method_name, obj.__class__.__name__)) - return method(*params, **named_params) + @staticmethod + def compare(p1, p2): + if isinstance(p1, matchers.Matcher): + if not p1.matches(p2): + return False + elif p1 != p2: + return False + return True + + def _remember_params(self, params, named_params): + if ( + contains_strict(params, Ellipsis) + and (params[-1] is not Ellipsis or named_params) + ): + raise TypeError('Ellipsis must be the last argument you specify.') + + if contains_strict(params, matchers.args): + raise TypeError('args must be used as *args') + + if ( + contains_strict(params, matchers.kwargs) + or contains_strict(params, matchers.KWARGS_SENTINEL) + ): + raise TypeError('kwargs must be used as **kwargs') + + def wrap(p): + if p is any or p is matchers.any_: + return matchers.any_() + return p + + self.params = tuple(wrap(p) for p in params) + self.named_params = {k: wrap(v) for k, v in named_params.items()} + + # Note: matches(a, b) does not imply matches(b, a) because + # the left side might contain wildcards (like Ellipsis) or matchers. + # In its current form the right side is a concrete call signature. + def matches(self, invocation): # noqa: C901 (too complex) + if self.method_name != invocation.method_name: + return False + + for x, p1 in enumerate(self.params): + # assume Ellipsis is the last thing a user declares + if p1 is Ellipsis: + return True + + if p1 is matchers.ARGS_SENTINEL: + break + + try: + p2 = invocation.params[x] + except IndexError: + return False + + if not self.compare(p1, p2): + return False + else: + if len(self.params) != len(invocation.params): + return False + + for key, p1 in sorted( + self.named_params.items(), + key=lambda k_v: 1 if k_v[0] is matchers.KWARGS_SENTINEL else 0 + ): + if key is matchers.KWARGS_SENTINEL: + break + + try: + p2 = invocation.named_params[key] + except KeyError: + return False + + if not self.compare(p1, p2): + return False + else: + if len(self.named_params) != len(invocation.named_params): + return False + + return True + class VerifiableInvocation(MatchingInvocation): - def __call__(self, *params, **named_params): - self._remember_params(params, named_params) - matched_invocations = [] - for invocation in self.mock.invocations: - if self.matches(invocation): - matched_invocations.append(invocation) - - verification = self.mock.pull_verification() - verification.verify(self, len(matched_invocations)) - - for invocation in matched_invocations: - invocation.verified = True - + def __init__(self, mock, method_name, verification): + super(VerifiableInvocation, self).__init__(mock, method_name) + self.verification = verification + + def __call__(self, *params, **named_params): + self._remember_params(params, named_params) + matched_invocations = [] + for invocation in self.mock.invocations: + if self.matches(invocation): + matched_invocations.append(invocation) + + self.verification.verify(self, len(matched_invocations)) + + # check (real) invocations as verified + for invocation in matched_invocations: + invocation.verified = True + + # check stubs as 'used' + if verification_has_lower_bound_of_zero(self.verification): + for stub in self.mock.stubbed_invocations: + # Remember: matches(a, b) does not imply matches(b, a) + # (see above!), so we check for both + if stub.matches(self) or self.matches(stub): + stub.allow_zero_invocations = True + + +def verification_has_lower_bound_of_zero(verification): + if ( + isinstance(verification, verificationModule.Times) + and verification.wanted_count == 0 + ): + return True + + if ( + isinstance(verification, verificationModule.Between) + and verification.wanted_from == 0 + ): + return True + + return False + + class StubbedInvocation(MatchingInvocation): - def __init__(self, *params): - super(StubbedInvocation, self).__init__(*params) - if self.mock.strict: - self.ensure_mocked_object_has_method(self.method_name) - - def ensure_mocked_object_has_method(self, method_name): - if not self.mock.has_method(method_name): - raise InvocationError("You tried to stub a method '%s' the object (%s) doesn't have." - % (method_name, self.mock.mocked_obj)) - - - def __call__(self, *params, **named_params): - self._remember_params(params, named_params) - return AnswerSelector(self) - - def stub_with(self, answer): - self.answers.append(answer) - self.mock.stub(self.method_name) - self.mock.finish_stubbing(self) - + def __init__(self, mock, method_name, verification=None, strict=None): + super(StubbedInvocation, self).__init__(mock, method_name) + + #: Holds the verification set up via `expect`. + #: The verification will be verified implicitly, while using this stub. + self.verification = verification + + if strict is not None: + self.strict = strict + + self.answers = CompositeAnswer() + + #: Counts how many times this stub has been 'used'. + #: A stub gets used, when a real invocation matches its argument + #: signature, and asks for an answer. + self.used = 0 + + #: Set if `verifyStubbedInvocationsAreUsed` should pass, regardless + #: of any factual invocation. E.g. set by `verify(..., times=0)` + if verification_has_lower_bound_of_zero(verification): + self.allow_zero_invocations = True + else: + self.allow_zero_invocations = False + + + def ensure_mocked_object_has_method(self, method_name): + if not self.mock.has_method(method_name): + raise InvocationError( + "You tried to stub a method '%s' the object (%s) doesn't " + "have." % (method_name, self.mock.mocked_obj)) + + def ensure_signature_matches(self, method_name, args, kwargs): + sig = self.mock.get_signature(method_name) + if not sig: + return + + signature.match_signature_allowing_placeholders(sig, args, kwargs) + + def __call__(self, *params, **named_params): + if self.strict: + self.ensure_mocked_object_has_method(self.method_name) + self.ensure_signature_matches( + self.method_name, params, named_params) + self._remember_params(params, named_params) + + self.mock.stub(self.method_name) + self.mock.finish_stubbing(self) + return AnswerSelector(self) + + def forget_self(self): + self.mock.forget_stubbed_invocation(self) + + def add_answer(self, answer): + self.answers.add(answer) + + def answer_first(self, *args, **kwargs): + self.used += 1 + return self.answers.answer(*args, **kwargs) + + def should_answer(self, invocation): + # type: (RememberedInvocation) -> None + verification = self.verification + if not verification: + return + + # This check runs before `answer_first`. We add '1' because we want + # to know if the verification passes if this call gets through. + actual_count = self.used + 1 + + if isinstance(verification, verificationModule.Times): + if actual_count > verification.wanted_count: + raise InvocationError( + "\nWanted times: %i, actual times: %i" + % (verification.wanted_count, actual_count)) + elif isinstance(verification, verificationModule.AtMost): + if actual_count > verification.wanted_count: + raise InvocationError( + "\nWanted at most: %i, actual times: %i" + % (verification.wanted_count, actual_count)) + elif isinstance(verification, verificationModule.Between): + if actual_count > verification.wanted_to: + raise InvocationError( + "\nWanted between: [%i, %i], actual times: %i" + % (verification.wanted_from, + verification.wanted_to, + actual_count)) + + # The way mockito's `verify` works is, that it checks off all 'real', + # remembered invocations, if they get verified. This is a simple + # mechanism so that a later `verifyNoMoreInteractions` just has to + # ensure that all invocations have this flag set to ``True``. + # For verifications set up via `expect` we want all invocations + # to get verified 'implicitly', on-the-go, so we set this flag here. + invocation.verified = True + + + def verify(self): + if not self.verification: + return + + actual_count = self.used + self.verification.verify(self, actual_count) + + + +def return_(value, *a, **kw): + return value + +def raise_(exception, *a, **kw): + raise exception + + class AnswerSelector(object): - def __init__(self, invocation): - self.invocation = invocation - self.answer = None - - def thenReturn(self, *return_values): - for return_value in return_values: - self.__then(Return(return_value)) - return self - - def thenRaise(self, *exceptions): - for exception in exceptions: - self.__then(Raise(exception)) - return self - - def __then(self, answer): - if not self.answer: - self.answer = CompositeAnswer(answer) - self.invocation.stub_with(self.answer) - else: - self.answer.add(answer) - - return self + def __init__(self, invocation): + self.invocation = invocation + + def thenReturn(self, *return_values): + for return_value in return_values: + self.__then(functools.partial(return_, return_value)) + return self + + def thenRaise(self, *exceptions): + for exception in exceptions: + self.__then(functools.partial(raise_, exception)) + return self + + def thenAnswer(self, *callables): + for callable in callables: + self.__then(callable) + return self + + def __then(self, answer): + self.invocation.add_answer(answer) + + def __enter__(self): + pass + + def __exit__(self, *exc_info): + self.invocation.forget_self() + class CompositeAnswer(object): - def __init__(self, answer): - self.answers = [answer] - - def add(self, answer): - self.answers.insert(0, answer) - - def answer(self): - if len(self.answers) > 1: - a = self.answers.pop() - else: - a = self.answers[0] - - return a.answer() - -class Raise(object): - def __init__(self, exception): - self.exception = exception - - def answer(self): - raise self.exception - -class Return(object): - def __init__(self, return_value): - self.return_value = return_value - - def answer(self): - return self.return_value + def __init__(self): + #: Container for answers, which are just ordinary callables + self.answers = deque() + + #: Counter for the maximum answers we ever had + self.answer_count = 0 + + def __len__(self): + # The minimum is '1' bc we always have a default answer of 'None' + return max(1, self.answer_count) + + def add(self, answer): + self.answer_count += 1 + self.answers.append(answer) + + def answer(self, *args, **kwargs): + if len(self.answers) == 0: + return None + + if len(self.answers) == 1: + a = self.answers[0] + else: + a = self.answers.popleft() + + return a(*args, **kwargs) + diff --git a/mockito/matchers.py b/mockito/matchers.py index c633762..9a44584 100644 --- a/mockito/matchers.py +++ b/mockito/matchers.py @@ -1,71 +1,387 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -'''Matchers for stubbing and verifications. - -Common matchers for use in stubbing and verifications. -''' - -__all__ = ['any', 'contains', 'times'] - +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Argument matchers for stubbing and verifications. + +In general the call signature you specify when stubbing or verifying in mockito +is as concrete as possible: it consists of values only:: + + when(os.path).exists('/foo/bar.txt').thenReturn(True) + +This is for a reason. In controlled test environments, for the scope of a +single test, you should usually know exactly how you use a function, and what +you expect its outcome to be. In mockito usually (in `strict` mode) all +invocations you did not specify upfront will throw at call time. + +If you reason about your code, the above `when` tirade turns - for the time +of the test - the specific stubbed function into a constant. + +You can use so called argument matchers below if you can't or don't +want to specify a single concrete value for an argument, but a type or class of +possible values. E.g.:: + + when(os.path).exists(...).thenReturn(True) + when(os.path).exists(ANY).thenReturn(True) + when(os.path).exists(ANY(str)).thenReturn(True) + + when(requests).get(ANY(str), **kwargs) + when(requests).get('https://example.com', ...) + + when(math).sqrt(not_(_or(ANY(float), ANY(int)))).thenRaise(TypeError) + +Now what you get each time is a function that up to a degree takes various +arguments and responds with the same outcome each time. Now that's a weird +thing. So use the matchers for a reason, they're powerful. + +The one usage you should not care about is a loose signature when using +:func:`verify`. Since mockito will throw for unexpected calls, a very loose +`verify` should be ok:: + + verify(requests, times=1).get(...) + + +""" + +import re + + +__all__ = [ + 'and_', 'or_', 'not_', + 'eq', 'neq', + 'lt', 'lte', + 'gt', 'gte', + 'any', 'any_', 'ANY', + 'arg_that', + 'contains', + 'matches', + 'captor', + 'times', + 'args', 'ARGS', + 'kwargs', 'KWARGS' +] + +class _ArgsSentinel(object): + def __repr__(self): + return '*args' + + +ARGS_SENTINEL = _ArgsSentinel() +ARGS = args = [ARGS_SENTINEL] +# ARGS.__doc__ = """Matches multiple positional arguments. + +# Note: `args` must match at least one argument. + +# Example:: + +# when(manager).add_tasks(1, 2, *args) + +# """ + +KWARGS_SENTINEL = '**' +KWARGS = kwargs = {KWARGS_SENTINEL: '_'} +# KWARGS.__doc__ = """Matches multiple keyword arguments. + +# Note that `kwargs` must match at least one remaining keyword argument. + +# Example:: + +# when(requests).get('http://myapi/', **KWARGS) + +# """ class Matcher: - def matches(self, arg): - pass - -class Any(Matcher): - def __init__(self, wanted_type=None): - self.wanted_type = wanted_type - - def matches(self, arg): - if self.wanted_type: - return isinstance(arg, self.wanted_type) - else: - return True - - def __repr__(self): - return "" % self.wanted_type + def matches(self, arg): + pass + + +class Any(Matcher): + def __init__(self, wanted_type=None): + self.wanted_type = wanted_type + + def matches(self, arg): + if self.wanted_type: + return isinstance(arg, self.wanted_type) + else: + return True + + def __repr__(self): + return "" % self.wanted_type + + +class ValueMatcher(Matcher): + def __init__(self, value): + self.value = value + + def __repr__(self): + return "<%s: %s>" % (self.__class__.__name__, self.value) + + +class Eq(ValueMatcher): + def matches(self, arg): + return arg == self.value + + +class Neq(ValueMatcher): + def matches(self, arg): + return arg != self.value + + +class Lt(ValueMatcher): + def matches(self, arg): + return arg < self.value + + +class Lte(ValueMatcher): + def matches(self, arg): + return arg <= self.value + + +class Gt(ValueMatcher): + def matches(self, arg): + return arg > self.value + + +class Gte(ValueMatcher): + def matches(self, arg): + return arg >= self.value + + +class And(Matcher): + def __init__(self, matchers): + self.matchers = [ + matcher if isinstance(matcher, Matcher) else Eq(matcher) + for matcher in matchers] + + def matches(self, arg): + return all(matcher.matches(arg) for matcher in self.matchers) + + def __repr__(self): + return "" % self.matchers + + +class Or(Matcher): + def __init__(self, matchers): + self.matchers = [ + matcher if isinstance(matcher, Matcher) else Eq(matcher) + for matcher in matchers] + + def matches(self, arg): + return __builtins__['any']( + [matcher.matches(arg) for matcher in self.matchers] + ) + + def __repr__(self): + return "" % self.matchers + + +class Not(Matcher): + def __init__(self, matcher): + self.matcher = matcher if isinstance(matcher, Matcher) else Eq(matcher) + + def matches(self, arg): + return not self.matcher.matches(arg) + + def __repr__(self): + return "" % self.matcher + + +class ArgThat(Matcher): + def __init__(self, predicate): + self.predicate = predicate + + def matches(self, arg): + return self.predicate(arg) + + def __repr__(self): + return "" + class Contains(Matcher): - def __init__(self, sub): - self.sub = sub - - def matches(self, arg): - if not hasattr(arg, 'find'): - return - return self.sub and len(self.sub) > 0 and arg.find(self.sub) > -1 - - def __repr__(self): - return "" % self.sub - - + def __init__(self, sub): + self.sub = sub + + def matches(self, arg): + if not hasattr(arg, 'find'): + return + return self.sub and len(self.sub) > 0 and arg.find(self.sub) > -1 + + def __repr__(self): + return "" % self.sub + + +class Matches(Matcher): + def __init__(self, regex, flags=0): + self.regex = re.compile(regex, flags) + + def matches(self, arg): + if not isinstance(arg, str): + return + return self.regex.match(arg) is not None + + def __repr__(self): + if self.regex.flags: + return "" % (self.regex.pattern, + self.regex.flags) + else: + return "" % self.regex.pattern + + +class ArgumentCaptor(Matcher): + def __init__(self, matcher=None): + self.matcher = matcher or Any() + self.value = None + + def matches(self, arg): + result = self.matcher.matches(arg) + if not result: + return + self.value = arg + return True + + def __repr__(self): + return "" % ( + repr(self.matcher), self.value, + ) + + def any(wanted_type=None): - """Matches any() argument OR any(SomeClass) argument - Examples: - when(mock).foo(any()).thenReturn(1) - verify(mock).foo(any(int)) - """ - return Any(wanted_type) - + """Matches against type of argument (`isinstance`). + + If you want to match *any* type, use either `ANY` or `ANY()`. + + Examples:: + + when(mock).foo(any).thenReturn(1) + verify(mock).foo(any(int)) + + """ + return Any(wanted_type) + + +ANY = any_ = any + + +def eq(value): + """Matches particular value (`==`)""" + return Eq(value) + + +def neq(value): + """Matches any but given value (`!=`)""" + return Neq(value) + + +def lt(value): + """Matches any value that is less than given value (`<`)""" + return Lt(value) + + +def lte(value): + """Matches any value that is less than or equal to given value (`<=`)""" + return Lte(value) + + +def gt(value): + """Matches any value that is greater than given value (`>`)""" + return Gt(value) + + +def gte(value): + """Matches any value that is greater than or equal to given value (`>=`)""" + return Gte(value) + + +def and_(*matchers): + """Matches if all given matchers match + + Example:: + + when(mock).foo(and_(ANY(str), contains('foo'))) + + """ + return And(matchers) + + +def or_(*matchers): + """Matches if any given matcher match + + Example:: + + when(mock).foo(or_(ANY(int), ANY(float))) + + """ + return Or(matchers) + + +def not_(matcher): + """Matches if given matcher does not match + + Example:: + + when(mock).foo(not_(ANY(str))).thenRaise(TypeError) + + """ + return Not(matcher) + + +def arg_that(predicate): + """Matches any argument for which predicate returns True + + Example:: + + verify(mock).foo(arg_that(lambda arg: arg > 3 and arg < 7)) + + """ + return ArgThat(predicate) + + def contains(sub): - return Contains(sub) + """Matches any string containing given substring + + Example:: + + mock.foo([120, 121, 122, 123]) + verify(mock).foo(contains(123)) + + """ + return Contains(sub) + + +def matches(regex, flags=0): + """Matches any string that matches given regex""" + return Matches(regex, flags) + + +def captor(matcher=None): + """Returns argument captor that captures value for further assertions + + Example:: + + arg_captor = captor(any(int)) + when(mock).do_something(arg_captor) + mock.do_something(123) + assert arg_captor.value == 123 + + """ + return ArgumentCaptor(matcher) + def times(count): - return count + return count diff --git a/mockito/mock_registry.py b/mockito/mock_registry.py index e4cb4b9..f1e69da 100644 --- a/mockito/mock_registry.py +++ b/mockito/mock_registry.py @@ -1,39 +1,90 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + class MockRegistry: - """Registers mock()s, ensures that we only have one mock() per mocked_obj, and - iterates over them to unstub each stubbed method. """ - - def __init__(self): - self.mocks = {} - - def register(self, mock): - self.mocks[mock.mocked_obj] = mock - - def mock_for(self, cls): - return self.mocks.get(cls, None) - - def unstub_all(self): - for mock in self.mocks.itervalues(): - mock.unstub() - self.mocks.clear() + """Registry for mocks -mock_registry = MockRegistry() \ No newline at end of file + Registers mock()s, ensures that we only have one mock() per mocked_obj, and + iterates over them to unstub each stubbed method. + """ + + def __init__(self): + self.mocks = _Dict() + + def register(self, obj, mock): + self.mocks[obj] = mock + + def mock_for(self, obj): + return self.mocks.get(obj, None) + + def unstub(self, obj): + try: + mock = self.mocks.pop(obj) + except KeyError: + pass + else: + mock.unstub() + + def unstub_all(self): + for mock in self.get_registered_mocks(): + mock.unstub() + self.mocks.clear() + + def get_registered_mocks(self): + return self.mocks.values() + + +# We have this dict like because we want non-hashable items in our registry. +# This is just enough to match the invoking code above. TBC +class _Dict(object): + def __init__(self): + self._store = [] + + def __setitem__(self, key, value): + self.remove(key) + self._store.append((key, value)) + + def remove(self, key): + self._store = [(k, v) for k, v in self._store if k != key] + + def pop(self, key): + rv = self.get(key) + if rv is not None: + self.remove(key) + return rv + else: + raise KeyError() + + def get(self, key, default=None): + for k, value in self._store: + if k == key: + return value + return default + + def values(self): + return [v for k, v in self._store] + + def clear(self): + self._store[:] = [] + + +mock_registry = MockRegistry() diff --git a/mockito/mocking.py b/mockito/mocking.py index e6b22c3..90832e4 100644 --- a/mockito/mocking.py +++ b/mockito/mocking.py @@ -1,119 +1,290 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from collections import deque import inspect -import invocation -from mock_registry import mock_registry -import warnings - - -__all__ = ['mock', 'Mock'] - -class _Dummy(object): pass - -class TestDouble(object): pass - -class mock(TestDouble): - def __init__(self, mocked_obj=None, strict=True): - self.invocations = [] - self.stubbed_invocations = [] - self.original_methods = [] - self.stubbing = None - self.verification = None - if mocked_obj is None: - mocked_obj = _Dummy() - strict = False - self.mocked_obj = mocked_obj - self.strict = strict - self.stubbing_real_object = False - - mock_registry.register(self) - - def __getattr__(self, method_name): - if self.stubbing is not None: - return invocation.StubbedInvocation(self, method_name) - - if self.verification is not None: - return invocation.VerifiableInvocation(self, method_name) - - return invocation.RememberedInvocation(self, method_name) - - def remember(self, invocation): - self.invocations.insert(0, invocation) - - def finish_stubbing(self, stubbed_invocation): - self.stubbed_invocations.insert(0, stubbed_invocation) - self.stubbing = None - - def expect_stubbing(self): - self.stubbing = True - - def pull_verification(self): - v = self.verification - self.verification = None - return v - - def has_method(self, method_name): - return hasattr(self.mocked_obj, method_name) - - def get_method(self, method_name): - return self.mocked_obj.__dict__.get(method_name) - - def set_method(self, method_name, new_method): - setattr(self.mocked_obj, method_name, new_method) - - def replace_method(self, method_name, original_method): - - def new_mocked_method(*args, **kwargs): - # we throw away the first argument, if it's either self or cls - if inspect.isclass(self.mocked_obj) and not isinstance(original_method, staticmethod): - args = args[1:] - call = self.__getattr__(method_name) # that is: invocation.RememberedInvocation(self, method_name) - return call(*args, **kwargs) - - if isinstance(original_method, staticmethod): - new_mocked_method = staticmethod(new_mocked_method) - elif isinstance(original_method, classmethod): - new_mocked_method = classmethod(new_mocked_method) - - self.set_method(method_name, new_mocked_method) - - def stub(self, method_name): - original_method = self.get_method(method_name) - original = (method_name, original_method) - self.original_methods.append(original) - - # If we're trying to stub real object(not a generated mock), then we should patch object to use our mock method. - # TODO: Polymorphism was invented long time ago. Refactor this. - if self.stubbing_real_object: - self.replace_method(method_name, original_method) - - def unstub(self): - while self.original_methods: - method_name, original_method = self.original_methods.pop() - self.set_method(method_name, original_method) - -def Mock(*args, **kwargs): - '''A ``mock``() alias. - - Alias for compatibility. To be removed in version 1.0. - ''' - warnings.warn("\n`Mock()` is deprecated, please use `mock()` (lower 'm') instead.", DeprecationWarning) - return mock(*args, **kwargs) +import functools +import operator + +from . import invocation +from . import signature +from . import utils +from .mock_registry import mock_registry + + +__all__ = ['mock'] + +__tracebackhide__ = operator.methodcaller( + "errisinstance", + invocation.InvocationError +) + + + +class _Dummy(object): + # We spell out `__call__` here for convenience. All other magic methods + # must be configured before use, but we want `mock`s to be callable by + # default. + def __call__(self, *args, **kwargs): + return self.__getattr__('__call__')(*args, **kwargs) + + +def remembered_invocation_builder(mock, method_name, *args, **kwargs): + invoc = invocation.RememberedInvocation(mock, method_name) + return invoc(*args, **kwargs) + + +class Mock(object): + def __init__(self, mocked_obj, strict=True, spec=None): + self.mocked_obj = mocked_obj + self.strict = strict + self.spec = spec + + self.invocations = deque() + self.stubbed_invocations = deque() + + self.original_methods = {} + self._signatures_store = {} + + def remember(self, invocation): + self.invocations.appendleft(invocation) + + def finish_stubbing(self, stubbed_invocation): + self.stubbed_invocations.appendleft(stubbed_invocation) + + def clear_invocations(self): + self.invocations = deque() + + # STUBBING + + def get_original_method(self, method_name): + if self.spec is None: + return None + + try: + return self.spec.__dict__.get(method_name) + except AttributeError: + return getattr(self.spec, method_name, None) + + def set_method(self, method_name, new_method): + setattr(self.mocked_obj, method_name, new_method) + + def replace_method(self, method_name, original_method): + + def new_mocked_method(*args, **kwargs): + # we throw away the first argument, if it's either self or cls + if ( + inspect.ismethod(new_mocked_method) + or inspect.isclass(self.mocked_obj) + and not isinstance(new_mocked_method, staticmethod) + ): + args = args[1:] + + return remembered_invocation_builder( + self, method_name, *args, **kwargs) + + new_mocked_method.__name__ = method_name + if original_method: + new_mocked_method.__doc__ = original_method.__doc__ + new_mocked_method.__wrapped__ = original_method + try: + new_mocked_method.__module__ = original_method.__module__ + except AttributeError: + pass + + if inspect.ismethod(original_method): + new_mocked_method = utils.newmethod( + new_mocked_method, self.mocked_obj + ) + + if isinstance(original_method, staticmethod): + new_mocked_method = staticmethod(new_mocked_method) + elif isinstance(original_method, classmethod): + new_mocked_method = classmethod(new_mocked_method) + elif ( + inspect.isclass(self.mocked_obj) + and inspect.isclass(original_method) # TBC: Inner classes + ): + new_mocked_method = staticmethod(new_mocked_method) + + self.set_method(method_name, new_mocked_method) + + def stub(self, method_name): + try: + self.original_methods[method_name] + except KeyError: + original_method = self.get_original_method(method_name) + self.original_methods[method_name] = original_method + + self.replace_method(method_name, original_method) + + def forget_stubbed_invocation(self, invocation): + assert invocation in self.stubbed_invocations + + if len(self.stubbed_invocations) == 1: + mock_registry.unstub(self.mocked_obj) + return + + self.stubbed_invocations.remove(invocation) + + if not any( + inv.method_name == invocation.method_name + for inv in self.stubbed_invocations + ): + original_method = self.original_methods.pop(invocation.method_name) + self.restore_method(invocation.method_name, original_method) + + def restore_method(self, method_name, original_method): + # If original_method is None, we *added* it to mocked_obj, so we + # must delete it here. + # If we mocked an instance, our mocked function will actually hide + # the one on its class, so we delete as well. + if ( + not original_method + or not inspect.isclass(self.mocked_obj) + and inspect.ismethod(original_method) + ): + delattr(self.mocked_obj, method_name) + else: + self.set_method(method_name, original_method) + + def unstub(self): + while self.original_methods: + method_name, original_method = self.original_methods.popitem() + self.restore_method(method_name, original_method) + + # SPECCING + + def has_method(self, method_name): + if self.spec is None: + return True + + return hasattr(self.spec, method_name) + + def get_signature(self, method_name): + if self.spec is None: + return None + + try: + return self._signatures_store[method_name] + except KeyError: + sig = signature.get_signature(self.spec, method_name) + self._signatures_store[method_name] = sig + return sig + + +class _OMITTED(object): + def __repr__(self): + return 'OMITTED' + + +OMITTED = _OMITTED() + +def mock(config_or_spec=None, spec=None, strict=OMITTED): + """Create 'empty' objects ('Mocks'). + + Will create an empty unconfigured object, that you can pass + around. All interactions (method calls) will be recorded and can be + verified using :func:`verify` et.al. + + A plain `mock()` will be not `strict`, and thus all methods regardless + of the arguments will return ``None``. + + .. note:: Technically all attributes will return an internal interface. + Because of that a simple ``if mock().foo:`` will surprisingly pass. + + If you set strict to ``True``: ``mock(strict=True)`` all unexpected + interactions will raise an error instead. + + You configure a mock using :func:`when`, :func:`when2` or :func:`expect`. + You can also very conveniently just pass in a dict here:: + + response = mock({'text': 'ok', 'raise_for_status': lambda: None}) + + You can also create an empty Mock which is specced against a given + `spec`: ``mock(requests.Response)``. These mock are by default strict, + thus they raise if you want to stub a method, the spec does not implement. + Mockito will also match the function signature. + + You can pre-configure a specced mock as well:: + + response = mock({'json': lambda: {'status': 'Ok'}}, + spec=requests.Response) + + Mocks are by default callable. Configure the callable behavior using + `when`:: + + dummy = mock() + when(dummy).__call_(1).thenReturn(2) + + All other magic methods must be configured this way or they will raise an + AttributeError. + + + See :func:`verify` to verify your interactions after usage. + + """ + + if type(config_or_spec) is dict: + config = config_or_spec + else: + config = {} + spec = config_or_spec + + if strict is OMITTED: + strict = False if spec is None else True + + + class Dummy(_Dummy): + if spec: + __class__ = spec # make isinstance work + + def __getattr__(self, method_name): + if strict: + __tracebackhide__ = operator.methodcaller( + "errisinstance", AttributeError + ) + + raise AttributeError( + "'Dummy' has no attribute %r configured" % method_name) + return functools.partial( + remembered_invocation_builder, theMock, method_name) + + def __repr__(self): + name = 'Dummy' + if spec: + name += spec.__name__ + return "<%s id=%s>" % (name, id(self)) + + + # That's a tricky one: The object we will return is an *instance* of our + # Dummy class, but the mock we register will point and patch the class. + # T.i. so that magic methods (`__call__` etc.) can be configured. + obj = Dummy() + theMock = Mock(Dummy, strict=strict, spec=spec) + + for n, v in config.items(): + if inspect.isfunction(v): + invocation.StubbedInvocation(theMock, n)(Ellipsis).thenAnswer(v) + else: + setattr(Dummy, n, v) + + mock_registry.register(obj, theMock) + return obj diff --git a/mockito/mockito.py b/mockito/mockito.py index ded0143..53854fe 100644 --- a/mockito/mockito.py +++ b/mockito/mockito.py @@ -1,106 +1,433 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import verification -from mocking import mock, TestDouble -from mock_registry import mock_registry -from verification import VerificationError +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import operator + +from . import invocation +from . import verification + +from .utils import get_obj, get_obj_attr_tuple +from .mocking import Mock +from .mock_registry import mock_registry +from .verification import VerificationError class ArgumentError(Exception): - pass + pass + + +__tracebackhide__ = operator.methodcaller( + "errisinstance", (ArgumentError, VerificationError) +) + def _multiple_arguments_in_use(*args): - return len(filter(lambda x: x, args)) > 1 + return len([x for x in args if x]) > 1 + def _invalid_argument(value): - return (value is not None and value < 1) or value == 0 + return (value is not None and value < 1) or value == 0 + def _invalid_between(between): - if between is not None: - start, end = between - if start > end or start < 0: - return True - return False - -def verify(obj, times=1, atleast=None, atmost=None, between=None, inorder=False): - if times < 0: - raise ArgumentError("""'times' argument has invalid value. - It should be at least 0. You wanted to set it to: %i""" % times) - if _multiple_arguments_in_use(atleast, atmost, between): - raise ArgumentError("""Sure you know what you are doing? - You can set only one of the arguments: 'atleast', 'atmost' or 'between'.""") - if _invalid_argument(atleast): - raise ArgumentError("""'atleast' argument has invalid value. - It should be at least 1. You wanted to set it to: %i""" % atleast) - if _invalid_argument(atmost): - raise ArgumentError("""'atmost' argument has invalid value. - It should be at least 1. You wanted to set it to: %i""" % atmost) - if _invalid_between(between): - raise ArgumentError("""'between' argument has invalid value. - It should consist of positive values with second number not greater than first - e.g. [1, 4] or [0, 3] or [2, 2] - You wanted to set it to: %s""" % between) - - if isinstance(obj, TestDouble): - mocked_object = obj - else: - mocked_object = mock_registry.mock_for(obj) - - if atleast: - mocked_object.verification = verification.AtLeast(atleast) - elif atmost: - mocked_object.verification = verification.AtMost(atmost) - elif between: - mocked_object.verification = verification.Between(*between) - else: - mocked_object.verification = verification.Times(times) - - if inorder: - mocked_object.verification = verification.InOrder(mocked_object.verification) - - return mocked_object - -def when(obj, strict=True): - if isinstance(obj, mock): - theMock = obj - else: + if between is not None: + try: + start, end = between + except Exception: + return True + + if start > end or start < 0: + return True + return False + +def _get_wanted_verification( + times=None, atleast=None, atmost=None, between=None): + if times is not None and times < 0: + raise ArgumentError("'times' argument has invalid value.\n" + "It should be at least 0. You wanted to set it to:" + " %i" % times) + if _multiple_arguments_in_use(atleast, atmost, between): + raise ArgumentError( + "You can set only one of the arguments: 'atleast', " + "'atmost' or 'between'.") + if _invalid_argument(atleast): + raise ArgumentError("'atleast' argument has invalid value.\n" + "It should be at least 1. You wanted to set it " + "to: %i" % atleast) + if _invalid_argument(atmost): + raise ArgumentError("'atmost' argument has invalid value.\n" + "It should be at least 1. You wanted to set it " + "to: %i" % atmost) + if _invalid_between(between): + raise ArgumentError( + """'between' argument has invalid value. +It should consist of positive values with second number not greater +than first e.g. (1, 4) or (0, 3) or (2, 2). +You wanted to set it to: %s""" % (between,)) + + if atleast: + return verification.AtLeast(atleast) + elif atmost: + return verification.AtMost(atmost) + elif between: + return verification.Between(*between) + elif times is not None: + return verification.Times(times) + +def _get_mock(obj, strict=True): theMock = mock_registry.mock_for(obj) if theMock is None: - theMock = mock(obj, strict=strict) - # If we call when on something that is not TestDouble that means we're trying to stub real object, - # (class, module etc.). Not to be confused with generating stubs from real classes. - theMock.stubbing_real_object = True - - theMock.expect_stubbing() - return theMock - -def unstub(): - """Unstubs all stubbed methods and functions""" - mock_registry.unstub_all() - -def verifyNoMoreInteractions(*mocks): - for mock in mocks: - for i in mock.invocations: - if not i.verified: - raise VerificationError("\nUnwanted interaction: " + str(i)) - -def verifyZeroInteractions(*mocks): - verifyNoMoreInteractions(*mocks) + theMock = Mock(obj, strict=strict, spec=obj) + mock_registry.register(obj, theMock) + return theMock + +def _get_mock_or_raise(obj): + theMock = mock_registry.mock_for(obj) + if theMock is None: + raise ArgumentError("obj '%s' is not registered" % obj) + return theMock + +def verify(obj, times=1, atleast=None, atmost=None, between=None, + inorder=False): + """Central interface to verify interactions. + + `verify` uses a fluent interface:: + + verify(, times=2).() + + `args` can be as concrete as necessary. Often a catch-all is enough, + especially if you're working with strict mocks, bc they throw at call + time on unwanted, unconfigured arguments:: + + from mockito import ANY, ARGS, KWARGS + when(manager).add_tasks(1, 2, 3) + ... + # no need to duplicate the specification; every other argument pattern + # would have raised anyway. + verify(manager).add_tasks(1, 2, 3) # duplicates `when`call + verify(manager).add_tasks(*ARGS) + verify(manager).add_tasks(...) # Py3 + verify(manager).add_tasks(Ellipsis) # Py2 + + """ + + if isinstance(obj, str): + obj = get_obj(obj) + + verification_fn = _get_wanted_verification( + times=times, atleast=atleast, atmost=atmost, between=between) + if inorder: + verification_fn = verification.InOrder(verification_fn) + + theMock = _get_mock_or_raise(obj) + + class Verify(object): + def __getattr__(self, method_name): + return invocation.VerifiableInvocation( + theMock, method_name, verification_fn) + + return Verify() + + +class _OMITTED(object): + def __repr__(self): + return 'OMITTED' + + +OMITTED = _OMITTED() + + +def when(obj, strict=True): + """Central interface to stub functions on a given `obj` + + `obj` should be a module, a class or an instance of a class; it can be + a Dummy you created with :func:`mock`. ``when`` exposes a fluent interface + where you configure a stub in three steps:: + + when().().thenReturn() + + Compared to simple *patching*, stubbing in mockito requires you to specify + conrete `args` for which the stub will answer with a concrete ``. + All invocations that do not match this specific call signature will be + rejected. They usually throw at call time. + + Stubbing in mockito's sense thus means not only to get rid of unwanted + side effects, but effectively to turn function calls into constants. + + E.g.:: + + # Given ``dog`` is an instance of a ``Dog`` + when(dog).bark('Grrr').thenReturn('Wuff') + when(dog).bark('Miau').thenRaise(TypeError()) + + # With this configuration set up: + assert dog.bark('Grrr') == 'Wuff' + dog.bark('Miau') # will throw TypeError + dog.bark('Wuff') # will throw unwanted interaction + + Stubbing can effectively be used as monkeypatching; usage shown with + the `with` context managing:: + + with when(os.path).exists('/foo').thenReturn(True): + ... + + Most of the time verifying your interactions is not necessary, because + your code under tests implicitly verifies the return value by evaluating + it. See :func:`verify` if you need to, see also :func:`expect` to setup + expected call counts up front. + + If your function is pure side effect and does not return something, you + can omit the specific answer. The default then is `None`:: + + when(manager).do_work() + + `when` verifies the method name, the expected argument signature, and the + actual, factual arguments your code under test uses against the original + object and its function so its easier to spot changing interfaces. + + Sometimes it's tedious to spell out all arguments:: + + from mockito import ANY, ARGS, KWARGS + when(requests).get('http://example.com/', **KWARGS).thenReturn(...) + when(os.path).exists(ANY) + when(os.path).exists(ANY(str)) + + .. note:: You must :func:`unstub` after stubbing, or use `with` + statement. + + Set ``strict=False`` to bypass the function signature checks. + + See related :func:`when2` which has a more pythonic interface. + + """ + + if isinstance(obj, str): + obj = get_obj(obj) + + theMock = _get_mock(obj, strict=strict) + + class When(object): + def __getattr__(self, method_name): + return invocation.StubbedInvocation( + theMock, method_name, strict=strict) + + return When() + + +def when2(fn, *args, **kwargs): + """Stub a function call with the given arguments + + Exposes a more pythonic interface than :func:`when`. See :func:`when` for + more documentation. + + Returns `AnswerSelector` interface which exposes `thenReturn`, + `thenRaise`, and `thenAnswer` as usual. Always `strict`. + + Usage:: + + # Given `dog` is an instance of a `Dog` + when2(dog.bark, 'Miau').thenReturn('Wuff') + + .. note:: You must :func:`unstub` after stubbing, or use `with` + statement. + + """ + obj, name = get_obj_attr_tuple(fn) + theMock = _get_mock(obj, strict=True) + return invocation.StubbedInvocation(theMock, name)(*args, **kwargs) + + +def patch(fn, attr_or_replacement, replacement=None): + """Patch/Replace a function. + + This is really like monkeypatching, but *note* that all interactions + will be recorded and can be verified. That is, using `patch` you stay in + the domain of mockito. + + Two ways to call this. Either:: + + patch(os.path.exists, lambda str: True) # two arguments + # OR + patch(os.path, 'exists', lambda str: True) # three arguments + + If called with three arguments, the mode is *not* strict to allow *adding* + methods. If called with two arguments, mode is always `strict`. + + .. note:: You must :func:`unstub` after stubbing, or use `with` + statement. + + """ + if replacement is None: + replacement = attr_or_replacement + return when2(fn, Ellipsis).thenAnswer(replacement) + else: + obj, name = fn, attr_or_replacement + theMock = _get_mock(obj, strict=True) + return invocation.StubbedInvocation( + theMock, name, strict=False)(Ellipsis).thenAnswer(replacement) + + + +def expect(obj, strict=None, + times=None, atleast=None, atmost=None, between=None): + """Stub a function call, and set up an expected call count. + + Usage:: + + # Given `dog` is an instance of a `Dog` + expect(dog, times=1).bark('Wuff').thenReturn('Miau') + dog.bark('Wuff') + dog.bark('Wuff') # will throw at call time: too many invocations + + # maybe if you need to ensure that `dog.bark()` was called at all + verifyNoUnwantedInteractions() + + .. note:: You must :func:`unstub` after stubbing, or use `with` + statement. + + See :func:`when`, :func:`when2`, :func:`verifyNoUnwantedInteractions` + + """ + if strict is None: + strict = True + theMock = _get_mock(obj, strict=strict) + + verification_fn = _get_wanted_verification( + times=times, atleast=atleast, atmost=atmost, between=between) + + class Expect(object): + def __getattr__(self, method_name): + return invocation.StubbedInvocation( + theMock, method_name, verification=verification_fn, + strict=strict) + + return Expect() + + + +def unstub(*objs): + """Unstubs all stubbed methods and functions + + If you don't pass in any argument, *all* registered mocks and + patched modules, classes etc. will be unstubbed. + + Note that additionally, the underlying registry will be cleaned. + After an `unstub` you can't :func:`verify` anymore because all + interactions will be forgotten. + """ + + if objs: + for obj in objs: + mock_registry.unstub(obj) + else: + mock_registry.unstub_all() + + +def forget_invocations(*objs): + """Forget all invocations of given objs. + + If you already *call* mocks during your setup routine, you can now call + ``forget_invocations`` at the end of your setup, and have a clean + 'recording' for your actual test code. T.i. you don't have + to count the invocations from your setup code anymore when using + :func:`verify` afterwards. + """ + for obj in objs: + theMock = _get_mock_or_raise(obj) + theMock.clear_invocations() + + +def verifyNoMoreInteractions(*objs): + verifyNoUnwantedInteractions(*objs) + + for obj in objs: + theMock = _get_mock_or_raise(obj) + + for i in theMock.invocations: + if not i.verified: + raise VerificationError("\nUnwanted interaction: %s" % i) + + +def verifyZeroInteractions(*objs): + """Verify that no methods have been called on given objs. + + Note that strict mocks usually throw early on unexpected, unstubbed + invocations. Partial mocks ('monkeypatched' objects or modules) do not + support this functionality at all, bc only for the stubbed invocations + the actual usage gets recorded. So this function is of limited use, + nowadays. + + """ + for obj in objs: + theMock = _get_mock_or_raise(obj) + + if len(theMock.invocations) > 0: + raise VerificationError( + "\nUnwanted interaction: %s" % theMock.invocations[0]) + + + +def verifyNoUnwantedInteractions(*objs): + """Verifies that expectations set via `expect` are met + + E.g.:: + + expect(os.path, times=1).exists(...).thenReturn(True) + os.path('/foo') + verifyNoUnwantedInteractions(os.path) # ok, called once + + If you leave out the argument *all* registered objects will + be checked. + + .. note:: **DANGERZONE**: If you did not :func:`unstub` correctly, + it is possible that old registered mocks, from other tests + leak. + + See related :func:`expect` + """ + + if objs: + theMocks = map(_get_mock_or_raise, objs) + else: + theMocks = mock_registry.get_registered_mocks() + + for mock in theMocks: + for i in mock.stubbed_invocations: + i.verify() + +def verifyStubbedInvocationsAreUsed(*objs): + """Ensure stubs are actually used. + + This functions just ensures that stubbed methods are actually used. Its + purpose is to detect interface changes after refactorings. It is meant + to be invoked usually without arguments just before :func:`unstub`. + + """ + if objs: + theMocks = map(_get_mock_or_raise, objs) + else: + theMocks = mock_registry.get_registered_mocks() + + + for mock in theMocks: + for i in mock.stubbed_invocations: + if not i.allow_zero_invocations and i.used < len(i.answers): + raise VerificationError("\nUnused stub: %s" % i) + diff --git a/mockito/signature.py b/mockito/signature.py new file mode 100644 index 0000000..fabef5f --- /dev/null +++ b/mockito/signature.py @@ -0,0 +1,128 @@ + +from . import matchers +from .utils import contains_strict + +import functools +import inspect +import sys +import types + +try: + from inspect import signature, Parameter +except ImportError: + from funcsigs import signature, Parameter + + +PY3 = sys.version_info >= (3,) + + +def get_signature(obj, method_name): + method = getattr(obj, method_name) + + # Eat self for unbound methods bc signature doesn't do it + if PY3: + if ( + inspect.isclass(obj) + and not inspect.ismethod(method) + and not isinstance(obj.__dict__.get(method_name), staticmethod) + ): + method = functools.partial(method, None) + else: + if ( + isinstance(method, types.UnboundMethodType) + and method.__self__ is None + ): + method = functools.partial(method, None) + + try: + return signature(method) + except Exception: + return None + + +def match_signature(sig, args, kwargs): + sig.bind(*args, **kwargs) + return sig + + +def match_signature_allowing_placeholders(sig, args, kwargs): # noqa: C901 + # Let's face it. If this doesn't work out, we have to do it the hard + # way and reimplement something like `sig.bind` with our specific + # need for `...`, `*args`, and `**kwargs` support. + + if contains_strict(args, Ellipsis): + # Invariant: Ellipsis as the sole argument should just pass, regardless + # if it actually can consume an arg or the function does not take any + # arguments at all + if len(args) == 1: + return + + has_kwargs = has_var_keyword(sig) + # Ellipsis is always the last arg in args; it matches all keyword + # arguments as well. So the strategy here is to strip off all + # the keyword arguments from the signature, and do a partial + # bind with the rest. + params = [p for n, p in sig.parameters.items() + if p.kind not in (Parameter.KEYWORD_ONLY, + Parameter.VAR_KEYWORD)] + sig = sig.replace(parameters=params) + # Ellipsis should fill at least one argument. We strip it off if + # it can stand for a `kwargs` argument. + sig.bind_partial(*(args[:-1] if has_kwargs else args)) + else: + # `*args` should at least match one arg (t.i. not `*[]`), so we + # keep it here. The value and its type is irrelevant in python. + args_provided = contains_strict(args, matchers.ARGS_SENTINEL) + + # If we find the `**kwargs` sentinel we must remove it, bc its + # name cannot be matched against the sig. + kwargs_provided = matchers.KWARGS_SENTINEL in kwargs + if kwargs_provided: + kwargs = kwargs.copy() + kwargs.pop(matchers.KWARGS_SENTINEL) + + + if args_provided or kwargs_provided: + try: + sig.bind(*args, **kwargs) + except TypeError as e: + error = str(e) + if 'too many positional arguments' in error: + raise TypeError('no argument for *args left') + if 'multiple values for argument' in error: + raise + if 'too many keyword arguments' in error: # PY<3.5 + raise + if 'got an unexpected keyword argument' in error: # PY>3.5 + raise + + else: + if kwargs_provided and not has_var_keyword(sig): + pos_args = positional_arguments(sig) + len_args = len(args) - int(args_provided) + len_kwargs = len(kwargs) + provided_args = len_args + len_kwargs + # Substitute at least one argument for the `**kwargs`, + # the user provided; t.i. do not allow kwargs to + # satisfy an empty `{}`. + if provided_args + 1 > pos_args: + raise TypeError( + 'no keyword argument for **kwargs left') + + else: + # Without Ellipsis and the other stuff this would really be + # straight forward. + sig.bind(*args, **kwargs) + + return sig + + +def positional_arguments(sig): + return len([p for n, p in sig.parameters.items() + if p.kind in (Parameter.POSITIONAL_ONLY, + Parameter.POSITIONAL_OR_KEYWORD)]) + +def has_var_keyword(sig): + return any(p for n, p in sig.parameters.items() + if p.kind is Parameter.VAR_KEYWORD) + diff --git a/mockito/spying.py b/mockito/spying.py index ec056db..b2a5ae7 100644 --- a/mockito/spying.py +++ b/mockito/spying.py @@ -1,54 +1,105 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. '''Spying on real objects.''' -from invocation import RememberedProxyInvocation, VerifiableInvocation -from mocking import TestDouble +import inspect -__author__ = "Serhiy Oplakanets " +from .mockito import when2 +from .invocation import RememberedProxyInvocation +from .mocking import Mock, _Dummy, mock_registry +from .utils import get_obj __all__ = ['spy'] -def spy(original_object): - return Spy(original_object) -class Spy(TestDouble): - strict = True # spies always have to check if method exists - - def __init__(self, original_object): - self.original_object = original_object - self.invocations = [] - self.verification = None - - def __getattr__(self, name): - if self.verification: - return VerifiableInvocation(self, name) +def spy(object): + """Spy an object. + + Spying means that all functions will behave as before, so they will + be side effects, but the interactions can be verified afterwards. + + Returns Dummy-like, almost empty object as proxy to `object`. + + The *returned* object must be injected and used by the code under test; + after that all interactions can be verified as usual. + T.i. the original object **will not be patched**, and has no further + knowledge as before. + + E.g.:: + + import time + time = spy(time) + # inject time + do_work(..., time) + verify(time).time() + + """ + if inspect.isclass(object) or inspect.ismodule(object): + class_ = None else: - return RememberedProxyInvocation(self, name) - - def remember(self, invocation): - self.invocations.insert(0, invocation) - - def pull_verification(self): - v = self.verification - self.verification = None - return v - \ No newline at end of file + class_ = object.__class__ + + class Spy(_Dummy): + if class_: + __class__ = class_ + + def __getattr__(self, method_name): + return RememberedProxyInvocation(theMock, method_name) + + def __repr__(self): + name = 'Spied' + if class_: + name += class_.__name__ + return "<%s id=%s>" % (name, id(self)) + + + obj = Spy() + theMock = Mock(obj, strict=True, spec=object) + + mock_registry.register(obj, theMock) + return obj + + +def spy2(fn): # type: (...) -> None + """Spy usage of given `fn`. + + Patches the module, class or object `fn` lives in, so that all + interactions can be recorded; otherwise executes `fn` as before, so + that all side effects happen as before. + + E.g.:: + + import time + spy(time.time) + do_work(...) # nothing injected, uses global patched `time` module + verify(time).time() + + Note that builtins often cannot be patched because they're read-only. + + + """ + if isinstance(fn, str): + answer = get_obj(fn) + else: + answer = fn + + when2(fn, Ellipsis).thenAnswer(answer) + diff --git a/mockito/utils.py b/mockito/utils.py new file mode 100644 index 0000000..ed8e7c3 --- /dev/null +++ b/mockito/utils.py @@ -0,0 +1,176 @@ + +import importlib +import inspect +import sys +import types +import re + + +PY3 = sys.version_info >= (3,) + + +def contains_strict(seq, element): + return any(item is element for item in seq) + + +def newmethod(fn, obj): + if PY3: + return types.MethodType(fn, obj) + else: + return types.MethodType(fn, obj, obj.__class__) + + +def get_function_host(fn): + """Destructure a given function into its host and its name. + + The 'host' of a function is a module, for methods it is usually its + instance or its class. This is safe only for methods, for module wide, + globally declared names it must be considered experimental. + + For all reasonable fn: ``getattr(*get_function_host(fn)) == fn`` + + Returns tuple (host, fn-name) + Otherwise should raise TypeError + """ + + obj = None + try: + name = fn.__name__ + obj = fn.__self__ + except AttributeError: + pass + + if obj is None: + # Due to how python imports work, everything that is global on a module + # level must be regarded as not safe here. For now, we go for the extra + # mile, TBC, because just specifying `os.path.exists` would be 'cool'. + # + # TLDR;: + # E.g. `inspect.getmodule(os.path.exists)` returns `genericpath` bc + # that's where `exists` is defined and comes from. But from the point + # of view of the user `exists` always comes and is used from `os.path` + # which points e.g. to `ntpath`. We thus must patch `ntpath`. + # But that's the same for most imports:: + # + # # b.py + # from a import foo + # + # Now asking `getmodule(b.foo)` it tells you `a`, but we access and use + # `b.foo` and we therefore must patch `b`. + + obj, name = find_invoking_frame_and_try_parse() + # safety check! + assert getattr(obj, name) == fn + + + return obj, name + + +FIND_ID = re.compile(r'.*\s*.*(?:when2|patch|spy2)\(\s*(.+?)[,\)]', re.M) + + +def find_invoking_frame_and_try_parse(): + # Actually we just want the first frame in user land; we're open for + # refactorings here and don't yet decide on which frame exactly we hit + # that user land. + stack = inspect.stack(3)[2:10] + for frame_info in stack: + # Within `patch` and `spy2` we delegate to `when2` but that's not + # user land code + if frame_info[3] in ('patch', 'spy2'): + continue + + source = ''.join(frame_info[4]) + m = FIND_ID.match(source) + if m: + # id should be something like `os.path.exists` etc. + id = m.group(1) + parts = id.split('.') + if len(parts) < 2: + raise TypeError("can't guess origin of '%s'" % id) + + frame = frame_info[0] + vars = frame.f_globals.copy() + vars.update(frame.f_locals) + + # Now that's a simple reduce; we get the initial value from the + # locally available `vars`, and then reduce the middle parts via + # `getattr`. The last path component gets not resolved, but is + # returned as plain string value. + obj = vars.get(parts[0]) + for part in parts[1:-1]: + obj = getattr(obj, part) + return obj, parts[-1] + + raise TypeError('could not destructure first argument') + +def get_obj(path): + """Return obj for given dotted path. + + Typical inputs for `path` are 'os' or 'os.path' in which case you get a + module; or 'os.path.exists' in which case you get a function from that + module. + + Just returns the given input in case it is not a str. + + Note: Relative imports not supported. + Raises ImportError or AttributeError as appropriate. + + """ + # Since we usually pass in mocks here; duck typing is not appropriate + # (mocks respond to every attribute). + if not isinstance(path, str): + return path + + if path.startswith('.'): + raise TypeError('relative imports are not supported') + + parts = path.split('.') + head, tail = parts[0], parts[1:] + + obj = importlib.import_module(head) + + # Normally a simple reduce, but we go the extra mile + # for good exception messages. + for i, name in enumerate(tail): + try: + obj = getattr(obj, name) + except AttributeError: + # Note the [:i] instead of [:i+1], so we get the path just + # *before* the AttributeError, t.i. the part of it that went ok. + module = '.'.join([head] + tail[:i]) + try: + importlib.import_module(module) + except ImportError: + raise AttributeError( + "object '%s' has no attribute '%s'" % (module, name)) + else: + raise AttributeError( + "module '%s' has no attribute '%s'" % (module, name)) + return obj + +def get_obj_attr_tuple(path): + """Split path into (obj, attribute) tuple. + + Given `path` is 'os.path.exists' will thus return `(os.path, 'exists')` + + If path is not a str, delegates to `get_function_host(path)` + + """ + if not isinstance(path, str): + return get_function_host(path) + + if path.startswith('.'): + raise TypeError('relative imports are not supported') + + try: + leading, end = path.rsplit('.', 1) + except ValueError: + raise TypeError('path must have dots') + + return get_obj(leading), end + + + + + diff --git a/mockito/verification.py b/mockito/verification.py index 04357d5..e7f766e 100644 --- a/mockito/verification.py +++ b/mockito/verification.py @@ -1,93 +1,152 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import operator __all__ = ['never', 'VerificationError'] class VerificationError(AssertionError): - '''Indicates error during verification of invocations. - - Raised if verification fails. Error message contains the cause. - ''' - pass + '''Indicates error during verification of invocations. + + Raised if verification fails. Error message contains the cause. + ''' + pass + + +__tracebackhide__ = operator.methodcaller("errisinstance", VerificationError) + class AtLeast(object): - def __init__(self, wanted_count): - self.wanted_count = wanted_count - - def verify(self, invocation, actual_count): - if actual_count < self.wanted_count: - raise VerificationError("\nWanted at least: %i, actual times: %i" % (self.wanted_count, actual_count)) - + def __init__(self, wanted_count): + self.wanted_count = wanted_count + + def verify(self, invocation, actual_count): + if actual_count < self.wanted_count: + raise VerificationError("\nWanted at least: %i, actual times: %i" + % (self.wanted_count, actual_count)) + + def __repr__(self): + return "<%s wanted=%s>" % (type(self).__name__, self.wanted_count) + class AtMost(object): - def __init__(self, wanted_count): - self.wanted_count = wanted_count - - def verify(self, invocation, actual_count): - if actual_count > self.wanted_count: - raise VerificationError("\nWanted at most: %i, actual times: %i" % (self.wanted_count, actual_count)) + def __init__(self, wanted_count): + self.wanted_count = wanted_count + + def verify(self, invocation, actual_count): + if actual_count > self.wanted_count: + raise VerificationError("\nWanted at most: %i, actual times: %i" + % (self.wanted_count, actual_count)) + + def __repr__(self): + return "<%s wanted=%s>" % (type(self).__name__, self.wanted_count) class Between(object): - def __init__(self, wanted_from, wanted_to): - self.wanted_from = wanted_from - self.wanted_to = wanted_to - - def verify(self, invocation, actual_count): - if actual_count < self.wanted_from or actual_count > self.wanted_to: - raise VerificationError("\nWanted between: [%i, %i], actual times: %i" % (self.wanted_from, self.wanted_to, actual_count)) - + def __init__(self, wanted_from, wanted_to): + self.wanted_from = wanted_from + self.wanted_to = wanted_to + + def verify(self, invocation, actual_count): + if actual_count < self.wanted_from or actual_count > self.wanted_to: + raise VerificationError( + "\nWanted between: [%i, %i], actual times: %i" + % (self.wanted_from, self.wanted_to, actual_count)) + + def __repr__(self): + return "<%s [%s, %s]>" % ( + type(self).__name__, self.wanted_from, self.wanted_to) + class Times(object): - def __init__(self, wanted_count): - self.wanted_count = wanted_count - - def verify(self, invocation, actual_count): - if actual_count == self.wanted_count: - return - if actual_count == 0: - raise VerificationError("\nWanted but not invoked: %s" % (invocation)) - else: - if self.wanted_count == 0: - raise VerificationError("\nUnwanted invocation of %s, times: %i" % (invocation, actual_count)) - else: - raise VerificationError("\nWanted times: %i, actual times: %i" % (self.wanted_count, actual_count)) - + def __init__(self, wanted_count): + self.wanted_count = wanted_count + + def verify(self, invocation, actual_count): + if actual_count == self.wanted_count: + return + + if actual_count == 0: + invocations = ( + [ + invoc + for invoc in invocation.mock.invocations + if invoc.method_name == invocation.method_name + ] + or invocation.mock.invocations + or ['Nothing'] + ) + raise VerificationError( + """ +Wanted but not invoked: + + %s + +Instead got: + + %s + +""" + % ( + invocation, + "\n ".join( + str(invoc) for invoc in reversed(invocations) + ) + ) + ) + else: + if self.wanted_count == 0: + raise VerificationError( + "\nUnwanted invocation of %s, times: %i" + % (invocation, actual_count)) + else: + raise VerificationError("\nWanted times: %i, actual times: %i" + % (self.wanted_count, actual_count)) + + def __repr__(self): + return "<%s wanted=%s>" % (type(self).__name__, self.wanted_count) + class InOrder(object): - ''' - Verifies invocations in order. - - Verifies if invocation was in expected order, and if yes -- degrades to original Verifier (AtLeast, Times, Between, ...). - ''' - - def __init__(self, original_verification): - ''' - @param original_verification: Original verifiaction to degrade to if order of invocation was ok. + '''Verifies invocations in order. + + Verifies if invocation was in expected order, and if yes -- degrades to + original Verifier (AtLeast, Times, Between, ...). ''' - self.original_verification = original_verification - - def verify(self, wanted_invocation, count): - for invocation in reversed(wanted_invocation.mock.invocations): - if not invocation.verified_inorder: - if not wanted_invocation.matches(invocation): - raise VerificationError("\nWanted %s to be invoked, got %s instead" % (wanted_invocation, invocation)) - invocation.verified_inorder = True - break - # proceed with original verification - self.original_verification.verify(wanted_invocation, count) - + + def __init__(self, original_verification): + ''' + + @param original_verification: Original verifiaction to degrade to if + order of invocation was ok. + ''' + self.original_verification = original_verification + + def verify(self, wanted_invocation, count): + for invocation in reversed(wanted_invocation.mock.invocations): + if not invocation.verified_inorder: + if not wanted_invocation.matches(invocation): + raise VerificationError( + '\nWanted %s to be invoked,' + '\ngot %s instead.' % + (wanted_invocation, invocation)) + invocation.verified_inorder = True + break + # proceed with original verification + self.original_verification.verify(wanted_invocation, count) + + never = 0 diff --git a/mockito.egg-info/PKG-INFO b/mockito.egg-info/PKG-INFO deleted file mode 100644 index 25926ea..0000000 --- a/mockito.egg-info/PKG-INFO +++ /dev/null @@ -1,16 +0,0 @@ -Metadata-Version: 1.0 -Name: mockito -Version: 0.5.2 -Summary: Spying framework -Home-page: http://code.google.com/p/mockito-python -Author: Justin Hopper -Author-email: mockito-python@googlegroups.com -License: MIT -Download-URL: http://code.google.com/p/mockito-python/downloads/list -Description: Mockito is a spying framework based on Java library with the same name. -Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Topic :: Software Development :: Testing -Classifier: Programming Language :: Python :: 3 diff --git a/mockito.egg-info/SOURCES.txt b/mockito.egg-info/SOURCES.txt deleted file mode 100644 index d75abf9..0000000 --- a/mockito.egg-info/SOURCES.txt +++ /dev/null @@ -1,36 +0,0 @@ -AUTHORS -LICENSE -MANIFEST.in -README.rst -distribute_setup.py -setup.cfg -setup.py -mockito/__init__.py -mockito/inorder.py -mockito/invocation.py -mockito/matchers.py -mockito/mock_registry.py -mockito/mocking.py -mockito/mockito.py -mockito/spying.py -mockito/verification.py -mockito.egg-info/PKG-INFO -mockito.egg-info/SOURCES.txt -mockito.egg-info/dependency_links.txt -mockito.egg-info/top_level.txt -mockito_test/__init__.py -mockito_test/classmethods_test.py -mockito_test/demo_test.py -mockito_test/instancemethods_test.py -mockito_test/matchers_test.py -mockito_test/mockingexacttypes_test.py -mockito_test/modulefunctions_test.py -mockito_test/spying_test.py -mockito_test/staticmethods_test.py -mockito_test/stubbing_test.py -mockito_test/test_base.py -mockito_test/verification_errors_test.py -mockito_test/verifications_test.py -mockito_util/__init__.py -mockito_util/test.py -mockito_util/write_readme.py \ No newline at end of file diff --git a/mockito.egg-info/dependency_links.txt b/mockito.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/mockito.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/mockito.egg-info/top_level.txt b/mockito.egg-info/top_level.txt deleted file mode 100644 index 9aae942..0000000 --- a/mockito.egg-info/top_level.txt +++ /dev/null @@ -1,4 +0,0 @@ -mockito_util -distribute_setup -mockito -mockito_test diff --git a/mockito_test/__init__.py b/mockito_test/__init__.py deleted file mode 100644 index 2000e67..0000000 --- a/mockito_test/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. diff --git a/mockito_test/classmethods_test.py b/mockito_test/classmethods_test.py deleted file mode 100644 index 47132f5..0000000 --- a/mockito_test/classmethods_test.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from test_base import * -from mockito import * - -class Dog: - @classmethod - def bark(cls): - return "woof!" - -class Cat: - @classmethod - def meow(cls, m): - return cls.__name__ + " " + str(m) - -class Lion(object): - @classmethod - def roar(cls): - return "Rrrrr!" - -class ClassMethodsTest(TestBase): - - def tearDown(self): - unstub() - - def testUnstubs(self): - when(Dog).bark().thenReturn("miau!") - unstub() - self.assertEquals("woof!", Dog.bark()) - - #TODO decent test case please :) without testing irrelevant implementation details - def testUnstubShouldPreserveMethodType(self): - when(Dog).bark().thenReturn("miau!") - unstub() - self.assertTrue(isinstance(Dog.__dict__.get("bark"), classmethod)) - - def testStubs(self): - self.assertEquals("woof!", Dog.bark()) - - when(Dog).bark().thenReturn("miau!") - - self.assertEquals("miau!", Dog.bark()) - - def testStubsClassesDerivedFromTheObjectClass(self): - self.assertEquals("Rrrrr!", Lion.roar()) - - when(Lion).roar().thenReturn("miau!") - - self.assertEquals("miau!", Lion.roar()) - - def testVerifiesMultipleCallsOnClassmethod(self): - when(Dog).bark().thenReturn("miau!") - - Dog.bark() - Dog.bark() - - verify(Dog, times(2)).bark() - - def testFailsVerificationOfMultipleCallsOnClassmethod(self): - when(Dog).bark().thenReturn("miau!") - - Dog.bark() - - self.assertRaises(VerificationError, verify(Dog, times(2)).bark) - - def testStubsAndVerifiesClassmethod(self): - when(Dog).bark().thenReturn("miau!") - - self.assertEquals("miau!", Dog.bark()) - - verify(Dog).bark() - - def testPreservesClassArgumentAfterUnstub(self): - self.assertEquals("Cat foo", Cat.meow("foo")) - - when(Cat).meow("foo").thenReturn("bar") - - self.assertEquals("bar", Cat.meow("foo")) - - unstub() - - self.assertEquals("Cat foo", Cat.meow("foo")) - -if __name__ == '__main__': - unittest.main() - diff --git a/mockito_test/demo_test.py b/mockito_test/demo_test.py deleted file mode 100644 index 38467e5..0000000 --- a/mockito_test/demo_test.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import mockito_util.write_readme - -#all below code will be merged with README -#DELIMINATOR -import unittest -from mockito import mock, when, verify - -class DemoTest(unittest.TestCase): - def testStubbing(self): - # create a mock - ourMock = mock() - - # stub it - when(ourMock).getStuff("cool").thenReturn("cool stuff") - - # use the mock - self.assertEqual("cool stuff", ourMock.getStuff("cool")) - - # what happens when you pass different argument? - self.assertEqual(None, ourMock.getStuff("different argument")) - - def testVerification(self): - # create a mock - theMock = mock() - - # use the mock - theMock.doStuff("cool") - - # verify the interactions. Method and parameters must match. Otherwise verification error. - verify(theMock).doStuff("cool") - -#DELIMINATOR -#all above code will be merged with README -if __name__ == '__main__': - unittest.main() - diff --git a/mockito_test/instancemethods_test.py b/mockito_test/instancemethods_test.py deleted file mode 100644 index db12ea3..0000000 --- a/mockito_test/instancemethods_test.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from test_base import * -from mockito import * -from mockito.invocation import InvocationError - -class Dog(object): - def waggle(self): - return "Wuff!" - - def bark(self, sound): - return "%s!" % sound - - def do_default_bark(self): - return self.bark('Wau') - -class InstanceMethodsTest(TestBase): - def tearDown(self): - unstub() - - def testUnstubAnInstanceMethod(self): - original_method = Dog.waggle - when(Dog).waggle().thenReturn('Nope!') - - unstub() - - rex = Dog() - self.assertEquals('Wuff!', rex.waggle()) - self.assertEquals(original_method, Dog.waggle) - - def testStubAnInstanceMethod(self): - when(Dog).waggle().thenReturn('Boing!') - - rex = Dog() - self.assertEquals('Boing!', rex.waggle()) - - def testStubsAnInstanceMethodWithAnArgument(self): - when(Dog).bark('Miau').thenReturn('Wuff') - - rex = Dog() - self.assertEquals('Wuff', rex.bark('Miau')) - #self.assertEquals('Wuff', rex.bark('Wuff')) - - def testInvocateAStubbedMethodFromAnotherMethod(self): - when(Dog).bark('Wau').thenReturn('Wuff') - - rex = Dog() - self.assertEquals('Wuff', rex.do_default_bark()) - verify(Dog).bark('Wau') - - def testYouCantStubAnUnknownMethodInStrictMode(self): - try: - when(Dog).barks('Wau').thenReturn('Wuff') - self.fail('Stubbing an unknown method should have thrown a exception') - except InvocationError: - pass - - def testCallingAStubbedMethodWithUnexpectedArgumentsShouldReturnNone(self): - when(Dog).bark('Miau').thenReturn('Wuff') - rex = Dog() - self.assertEquals(None, rex.bark('Shhh')) - - - def testStubInstancesInsteadOfClasses(self): - rex = Dog() - when(rex).bark('Miau').thenReturn('Wuff') - - self.assertEquals('Wuff', rex.bark('Miau')) - verify(rex, times=1).bark(any()) - - max = Dog() - self.assertEquals('Miau!', max.bark('Miau')) - - -if __name__ == '__main__': - unittest.main() diff --git a/mockito_test/matchers_test.py b/mockito_test/matchers_test.py deleted file mode 100644 index 3ab5d81..0000000 --- a/mockito_test/matchers_test.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from test_base import * -from mockito import mock, verify, contains - -class MatchersTest(TestBase): - def testVerifiesUsingContainsMatcher(self): - ourMock = mock() - ourMock.foo("foobar") - - verify(ourMock).foo(contains("foo")) - verify(ourMock).foo(contains("bar")) - -class ContainsMatcherTest(TestBase): - def testShouldSatisfiySubstringOfGivenString(self): - self.assertTrue(contains("foo").matches("foobar")) - - def testShouldSatisfySameString(self): - self.assertTrue(contains("foobar").matches("foobar")) - - def testShouldNotSatisfiyStringWhichIsNotSubstringOfGivenString(self): - self.assertFalse(contains("barfoo").matches("foobar")) - - def testShouldNotSatisfiyEmptyString(self): - self.assertFalse(contains("").matches("foobar")) - - def testShouldNotSatisfiyNone(self): - self.assertFalse(contains(None).matches("foobar")) - -if __name__ == '__main__': - unittest.main() diff --git a/mockito_test/mockingexacttypes_test.py b/mockito_test/mockingexacttypes_test.py deleted file mode 100644 index e6288cb..0000000 --- a/mockito_test/mockingexacttypes_test.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from test_base import * -from mockito.invocation import InvocationError -from mockito import mock, when - -class Foo(object): - - def bar(self): - pass - -class MockingExactTypesTest(TestBase): - - def testShouldScreamWhenUnknownMethodStubbed(self): - ourMock = mock(Foo) - - when(ourMock).bar().thenReturn("grr"); - - try: - when(ourMock).unknownMethod().thenReturn("grr"); - self.fail() - except InvocationError: - pass - - def testShouldReturnNoneWhenCallingExistingButUnstubbedMethod(self): - ourMock = mock(Foo) - self.assertEquals(None, ourMock.bar()) - -if __name__ == '__main__': - unittest.main() diff --git a/mockito_test/modulefunctions_test.py b/mockito_test/modulefunctions_test.py deleted file mode 100644 index 3e6a2d3..0000000 --- a/mockito_test/modulefunctions_test.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from test_base import * -from mockito import * -from mockito.invocation import InvocationError -import os - -class ModuleFunctionsTest(TestBase): - def tearDown(self): - unstub() - - def testUnstubs(self): - when(os.path).exists("test").thenReturn(True) - unstub() - self.assertEquals(False, os.path.exists("test")) - - def testStubs(self): - when(os.path).exists("test").thenReturn(True) - - self.assertEquals(True, os.path.exists("test")) - - def testStubsConsecutiveCalls(self): - when(os.path).exists("test").thenReturn(False).thenReturn(True) - - self.assertEquals(False, os.path.exists("test")) - self.assertEquals(True, os.path.exists("test")) - - def testStubsMultipleClasses(self): - when(os.path).exists("test").thenReturn(True) - when(os.path).dirname(any(str)).thenReturn("mocked") - - self.assertEquals(True, os.path.exists("test")) - self.assertEquals("mocked", os.path.dirname("whoah!")) - - def testVerifiesSuccesfully(self): - when(os.path).exists("test").thenReturn(True) - - os.path.exists("test") - - verify(os.path).exists("test") - - def testFailsVerification(self): - when(os.path).exists("test").thenReturn(True) - - self.assertRaises(VerificationError, verify(os.path).exists, "test") - - def testFailsOnNumberOfCalls(self): - when(os.path).exists("test").thenReturn(True) - - os.path.exists("test") - - self.assertRaises(VerificationError, verify(os.path, times(2)).exists, "test") - - def testStubsTwiceAndUnstubs(self): - when(os.path).exists("test").thenReturn(False) - when(os.path).exists("test").thenReturn(True) - - self.assertEquals(True, os.path.exists("test")) - - unstub() - - self.assertEquals(False, os.path.exists("test")) - - def testStubsTwiceWithDifferentArguments(self): - when(os.path).exists("Foo").thenReturn(False) - when(os.path).exists("Bar").thenReturn(True) - - self.assertEquals(False, os.path.exists("Foo")) - self.assertEquals(True, os.path.exists("Bar")) - - def testShouldThrowIfWeStubAFunctionNotDefinedInTheModule(self): - self.assertRaises(InvocationError, lambda:when(os).walk_the_line().thenReturn(None)) - - -if __name__ == '__main__': - unittest.main() diff --git a/mockito_test/spying_test.py b/mockito_test/spying_test.py deleted file mode 100644 index b445d8b..0000000 --- a/mockito_test/spying_test.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -#!/usr/bin/env python -# coding: utf-8 - -from mockito_test.test_base import TestBase -from mockito import spy, verify, VerificationError - -class Dummy: - def foo(self): - return "foo" - - def bar(self): - raise TypeError - - def return_args(self, *args, **kwargs): - return (args, kwargs) - -class SpyingTest(TestBase): - def testPreservesReturnValues(self): - dummy = Dummy() - spiedDummy = spy(dummy) - self.assertEquals(dummy.foo(), spiedDummy.foo()) - - def testPreservesSideEffects(self): - dummy = spy(Dummy()) - self.assertRaises(TypeError, dummy.bar) - - def testPassesArgumentsCorrectly(self): - dummy = spy(Dummy()) - self.assertEquals((('foo', 1), {'bar': 'baz'}), dummy.return_args('foo', 1, bar='baz')) - - def testIsVerifiable(self): - dummy = spy(Dummy()) - dummy.foo() - verify(dummy).foo() - self.assertRaises(VerificationError, verify(dummy).bar) - - def testRaisesAttributeErrorIfNoSuchMethod(self): - dummy = spy(Dummy()) - try: - dummy.lol() - self.fail("Should fail if no such method.") - except AttributeError, e: - self.assertEquals("You tried to call method 'lol' which 'Dummy' instance does not have.", str(e)) - \ No newline at end of file diff --git a/mockito_test/staticmethods_test.py b/mockito_test/staticmethods_test.py deleted file mode 100644 index cdda278..0000000 --- a/mockito_test/staticmethods_test.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from test_base import * -from mockito import * - -class Dog: - @staticmethod - def bark(): - return "woof" - - @staticmethod - def barkHardly(*args): - return "woof woof" - -class Cat: - @staticmethod - def meow(): - return "miau" - -class StaticMethodsTest(TestBase): - - def tearDown(self): - unstub() - - def testUnstubs(self): - when(Dog).bark().thenReturn("miau") - unstub() - self.assertEquals("woof", Dog.bark()) - -#TODO decent test case please :) without testing irrelevant implementation details - def testUnstubShouldPreserveMethodType(self): - when(Dog).bark().thenReturn("miau!") - unstub() - self.assertTrue(isinstance(Dog.__dict__.get("bark"), staticmethod)) - - def testStubs(self): - self.assertEquals("woof", Dog.bark()) - - when(Dog).bark().thenReturn("miau") - - self.assertEquals("miau", Dog.bark()) - - def testStubsConsecutiveCalls(self): - when(Dog).bark().thenReturn(1).thenReturn(2) - - self.assertEquals(1, Dog.bark()) - self.assertEquals(2, Dog.bark()) - self.assertEquals(2, Dog.bark()) - - def testStubsWithArgs(self): - self.assertEquals("woof woof", Dog.barkHardly(1, 2)) - - when(Dog).barkHardly(1, 2).thenReturn("miau") - - self.assertEquals("miau", Dog.barkHardly(1, 2)) - - def testStubsButDoesNotMachArguments(self): - self.assertEquals("woof woof", Dog.barkHardly(1, "anything")) - - when(Dog, strict=False).barkHardly(1, 2).thenReturn("miau") - - self.assertEquals(None, Dog.barkHardly(1)) - - def testStubsMultipleClasses(self): - when(Dog).barkHardly(1, 2).thenReturn(1) - when(Dog).bark().thenReturn(2) - when(Cat).meow().thenReturn(3) - - self.assertEquals(1, Dog.barkHardly(1, 2)) - self.assertEquals(2, Dog.bark()) - self.assertEquals(3, Cat.meow()) - - unstub() - - self.assertEquals("woof", Dog.bark()) - self.assertEquals("miau", Cat.meow()) - - def testVerifiesSuccesfully(self): - when(Dog).bark().thenReturn("boo") - - Dog.bark() - - verify(Dog).bark() - - def testVerifiesWithArguments(self): - when(Dog).barkHardly(1, 2).thenReturn("boo") - - Dog.barkHardly(1, 2) - - verify(Dog).barkHardly(1, any()) - - def testFailsVerification(self): - when(Dog).bark().thenReturn("boo") - - Dog.bark() - - self.assertRaises(VerificationError, verify(Dog).barkHardly, (1,2)) - - def testFailsOnInvalidArguments(self): - when(Dog).bark().thenReturn("boo") - - Dog.barkHardly(1, 2) - - self.assertRaises(VerificationError, verify(Dog).barkHardly, (1,20)) - - def testFailsOnNumberOfCalls(self): - when(Dog).bark().thenReturn("boo") - - Dog.bark() - - self.assertRaises(VerificationError, verify(Dog, times(2)).bark) - - def testStubsAndVerifies(self): - when(Dog).bark().thenReturn("boo") - - self.assertEquals("boo", Dog.bark()) - - verify(Dog).bark() - - def testStubsTwiceAndUnstubs(self): - when(Dog).bark().thenReturn(1) - when(Dog).bark().thenReturn(2) - - self.assertEquals(2, Dog.bark()) - - unstub() - - self.assertEquals("woof", Dog.bark()) - - def testDoesNotVerifyStubbedCalls(self): - when(Dog).bark().thenReturn(1) - - verify(Dog, times=0).bark() - -if __name__ == '__main__': - unittest.main() diff --git a/mockito_test/stubbing_test.py b/mockito_test/stubbing_test.py deleted file mode 100644 index b34ba52..0000000 --- a/mockito_test/stubbing_test.py +++ /dev/null @@ -1,255 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from test_base import * -from mockito import mock, when, verify, times, any - -class StubbingTest(TestBase): - def testStubsWithReturnValue(self): - theMock = mock() - when(theMock).getStuff().thenReturn("foo") - when(theMock).getMoreStuff(1, 2).thenReturn(10) - - self.assertEquals("foo", theMock.getStuff()) - self.assertEquals(10, theMock.getMoreStuff(1, 2)) - self.assertEquals(None, theMock.getMoreStuff(1, 3)) - - def testStubsWhenNoArgsGiven(self): - theMock = mock() - when(theMock).getStuff().thenReturn("foo") - when(theMock).getWidget().thenReturn("bar") - - self.assertEquals("foo", theMock.getStuff()) - self.assertEquals("bar", theMock.getWidget()) - - def testStubsConsecutivelyWhenNoArgsGiven(self): - theMock = mock() - when(theMock).getStuff().thenReturn("foo").thenReturn("bar") - when(theMock).getWidget().thenReturn("baz").thenReturn("baz2") - - self.assertEquals("foo", theMock.getStuff()) - self.assertEquals("bar", theMock.getStuff()) - self.assertEquals("bar", theMock.getStuff()) - self.assertEquals("baz", theMock.getWidget()) - self.assertEquals("baz2", theMock.getWidget()) - self.assertEquals("baz2", theMock.getWidget()) - - def testStubsWithException(self): - theMock = mock() - when(theMock).someMethod().thenRaise(Exception("foo")) - - self.assertRaisesMessage("foo", theMock.someMethod) - - def testStubsAndVerifies(self): - theMock = mock() - when(theMock).foo().thenReturn("foo") - - self.assertEquals("foo", theMock.foo()) - verify(theMock).foo() - - def testStubsVerifiesAndStubsAgain(self): - theMock = mock() - - when(theMock).foo().thenReturn("foo") - self.assertEquals("foo", theMock.foo()) - verify(theMock).foo() - - when(theMock).foo().thenReturn("next foo") - self.assertEquals("next foo", theMock.foo()) - verify(theMock, times(2)).foo() - - def testOverridesStubbing(self): - theMock = mock() - - when(theMock).foo().thenReturn("foo") - when(theMock).foo().thenReturn("bar") - - self.assertEquals("bar", theMock.foo()) - - def testStubsAndInvokesTwiceAndVerifies(self): - theMock = mock() - - when(theMock).foo().thenReturn("foo") - - self.assertEquals("foo", theMock.foo()) - self.assertEquals("foo", theMock.foo()) - - verify(theMock, times(2)).foo() - - def testStubsAndReturnValuesForMethodWithSameNameAndDifferentArguments(self): - theMock = mock() - when(theMock).getStuff(1).thenReturn("foo") - when(theMock).getStuff(1, 2).thenReturn("bar") - - self.assertEquals("foo", theMock.getStuff(1)) - self.assertEquals("bar", theMock.getStuff(1, 2)) - - def testStubsAndReturnValuesForMethodWithSameNameAndDifferentNamedArguments(self): - repo = mock() - when(repo).findby(id=6).thenReturn("John May") - when(repo).findby(name="John").thenReturn(["John May", "John Smith"]) - - self.assertEquals("John May", repo.findby(id=6)) - self.assertEquals(["John May", "John Smith"], repo.findby(name="John")) - - def testStubsForMethodWithSameNameAndNamedArgumentsInArbitraryOrder(self): - theMock = mock() - - when(theMock).foo(first=1, second=2, third=3).thenReturn(True) - - self.assertEquals(True, theMock.foo(third=3, first=1, second=2)) - - def testStubsMethodWithSameNameAndMixedArguments(self): - repo = mock() - when(repo).findby(1).thenReturn("John May") - when(repo).findby(1, active_only=True).thenReturn(None) - when(repo).findby(name="Sarah").thenReturn(["Sarah Connor"]) - when(repo).findby(name="Sarah", active_only=True).thenReturn([]) - - self.assertEquals("John May", repo.findby(1)) - self.assertEquals(None, repo.findby(1, active_only=True)) - self.assertEquals(["Sarah Connor"], repo.findby(name="Sarah")) - self.assertEquals([], repo.findby(name="Sarah", active_only=True)) - - def testStubsWithChainedReturnValues(self): - theMock = mock() - when(theMock).getStuff().thenReturn("foo").thenReturn("bar").thenReturn("foobar") - - self.assertEquals("foo", theMock.getStuff()) - self.assertEquals("bar", theMock.getStuff()) - self.assertEquals("foobar", theMock.getStuff()) - - def testStubsWithChainedReturnValuesAndException(self): - theMock = mock() - when(theMock).getStuff().thenReturn("foo").thenReturn("bar").thenRaise(Exception("foobar")) - - self.assertEquals("foo", theMock.getStuff()) - self.assertEquals("bar", theMock.getStuff()) - self.assertRaisesMessage("foobar", theMock.getStuff) - - def testStubsWithChainedExceptionAndReturnValue(self): - theMock = mock() - when(theMock).getStuff().thenRaise(Exception("foo")).thenReturn("bar") - - self.assertRaisesMessage("foo", theMock.getStuff) - self.assertEquals("bar", theMock.getStuff()) - - def testStubsWithChainedExceptions(self): - theMock = mock() - when(theMock).getStuff().thenRaise(Exception("foo")).thenRaise(Exception("bar")) - - self.assertRaisesMessage("foo", theMock.getStuff) - self.assertRaisesMessage("bar", theMock.getStuff) - - def testStubsWithReturnValueBeingException(self): - theMock = mock() - exception = Exception("foo") - when(theMock).getStuff().thenReturn(exception) - - self.assertEquals(exception, theMock.getStuff()) - - def testLastStubbingWins(self): - theMock = mock() - when(theMock).foo().thenReturn(1) - when(theMock).foo().thenReturn(2) - - self.assertEquals(2, theMock.foo()) - - def testStubbingOverrides(self): - theMock = mock() - when(theMock).foo().thenReturn(1) - when(theMock).foo().thenReturn(2).thenReturn(3) - - self.assertEquals(2, theMock.foo()) - self.assertEquals(3, theMock.foo()) - self.assertEquals(3, theMock.foo()) - - def testStubsWithMatchers(self): - theMock = mock() - when(theMock).foo(any()).thenReturn(1) - - self.assertEquals(1, theMock.foo(1)) - self.assertEquals(1, theMock.foo(100)) - - def testStubbingOverrides2(self): - theMock = mock() - when(theMock).foo(any()).thenReturn(1) - when(theMock).foo("oh").thenReturn(2) - - self.assertEquals(2, theMock.foo("oh")) - self.assertEquals(1, theMock.foo("xxx")) - - def testDoesNotVerifyStubbedCalls(self): - theMock = mock() - when(theMock).foo().thenReturn(1) - - verify(theMock, times=0).foo() - - def testStubsWithMultipleReturnValues(self): - theMock = mock() - when(theMock).getStuff().thenReturn("foo", "bar", "foobar") - - self.assertEquals("foo", theMock.getStuff()) - self.assertEquals("bar", theMock.getStuff()) - self.assertEquals("foobar", theMock.getStuff()) - - def testStubsWithChainedMultipleReturnValues(self): - theMock = mock() - when(theMock).getStuff().thenReturn("foo", "bar").thenReturn("foobar") - - self.assertEquals("foo", theMock.getStuff()) - self.assertEquals("bar", theMock.getStuff()) - self.assertEquals("foobar", theMock.getStuff()) - - def testStubsWithMultipleExceptions(self): - theMock = mock() - when(theMock).getStuff().thenRaise(Exception("foo"), Exception("bar")) - - self.assertRaisesMessage("foo", theMock.getStuff) - self.assertRaisesMessage("bar", theMock.getStuff) - - def testStubsWithMultipleChainedExceptions(self): - theMock = mock() - when(theMock).getStuff().thenRaise(Exception("foo"), Exception("bar")).thenRaise(Exception("foobar")) - - self.assertRaisesMessage("foo", theMock.getStuff) - self.assertRaisesMessage("bar", theMock.getStuff) - self.assertRaisesMessage("foobar", theMock.getStuff) - - def testLeavesOriginalMethodUntouchedWhenCreatingStubFromRealClass(self): - class Person: - def get_name(self): - return "original name" - - # given - person = Person() - mockPerson = mock(Person) - - # when - when(mockPerson).get_name().thenReturn("stubbed name") - - # then - self.assertEquals("stubbed name", mockPerson.get_name()) - self.assertEquals("original name", person.get_name(), 'Original method should not be replaced.') - -# TODO: verify after stubbing and vice versa - -if __name__ == '__main__': - unittest.main() diff --git a/mockito_test/test_base.py b/mockito_test/test_base.py deleted file mode 100644 index 634f18c..0000000 --- a/mockito_test/test_base.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import unittest - -class TestBase(unittest.TestCase): - - def __init__(self, *args, **kwargs): - unittest.TestCase.__init__(self, *args, **kwargs) - - def assertRaisesMessage(self, message, callable, *params): - try: - if (params): - callable(params) - else: - callable() - self.fail('Exception with message "%s" expected, but never raised' % (message)) - except Exception, e: - # TODO: self.fail() raises AssertionError which is caught here and error message becomes hardly understadable - self.assertEquals(message, str(e)) - -main = unittest.main diff --git a/mockito_test/verification_errors_test.py b/mockito_test/verification_errors_test.py deleted file mode 100644 index 02a0bfd..0000000 --- a/mockito_test/verification_errors_test.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from test_base import * -from mockito import mock, when, verify, VerificationError, verifyNoMoreInteractions -from mockito.verification import never - -class VerificationErrorsTest(TestBase): - - def testPrintsNicely(self): - theMock = mock() - try: - verify(theMock).foo() - except VerificationError, e: - self.assertEquals("\nWanted but not invoked: foo()", str(e)) - - def testPrintsNicelyOneArgument(self): - theMock = mock() - try: - verify(theMock).foo("bar") - except VerificationError, e: - self.assertEquals("\nWanted but not invoked: foo('bar')", str(e)) - - def testPrintsNicelyArguments(self): - theMock = mock() - try: - verify(theMock).foo(1, 2) - except VerificationError, e: - self.assertEquals("\nWanted but not invoked: foo(1, 2)", str(e)) - - def testPrintsNicelyStringArguments(self): - theMock = mock() - try: - verify(theMock).foo(1, 'foo') - except VerificationError, e: - self.assertEquals("\nWanted but not invoked: foo(1, 'foo')", str(e)) - - def testPrintsOutThatTheActualAndExpectedInvocationCountDiffers(self): - theMock = mock() - when(theMock).foo().thenReturn(0) - - theMock.foo() - theMock.foo() - - try: - verify(theMock).foo() - except VerificationError, e: - self.assertEquals("\nWanted times: 1, actual times: 2", str(e)) - - - # TODO: implement - def disabled_PrintsNicelyWhenArgumentsDifferent(self): - theMock = mock() - theMock.foo('foo', 1) - try: - verify(theMock).foo(1, 'foo') - except VerificationError, e: - self.assertEquals( -"""Arguments are different. -Wanted: foo(1, 'foo') -Actual: foo('foo', 1)""", str(e)) - - def testPrintsUnwantedInteraction(self): - theMock = mock() - theMock.foo(1, 'foo') - try: - verifyNoMoreInteractions(theMock) - except VerificationError, e: - self.assertEquals("\nUnwanted interaction: foo(1, 'foo')", str(e)) - - def testPrintsNeverWantedInteractionsNicely(self): - theMock = mock() - theMock.foo() - self.assertRaisesMessage("\nUnwanted invocation of foo(), times: 1", verify(theMock, never).foo) - -if __name__ == '__main__': - unittest.main() diff --git a/mockito_test/verifications_test.py b/mockito_test/verifications_test.py deleted file mode 100644 index 92ebab4..0000000 --- a/mockito_test/verifications_test.py +++ /dev/null @@ -1,275 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from test_base import TestBase, main -from mockito import mock, verify, inorder, VerificationError , ArgumentError, verifyNoMoreInteractions, verifyZeroInteractions, any -from mockito.verification import never - -class VerificationTestBase(TestBase): - def __init__(self, verification_function, *args, **kwargs): - self.verification_function = verification_function - TestBase.__init__(self, *args, **kwargs) - - def setUp(self): - self.mock = mock() - - def testVerifies(self): - self.mock.foo() - self.mock.someOtherMethod(1, "foo", "bar") - - self.verification_function(self.mock).foo() - self.verification_function(self.mock).someOtherMethod(1, "foo", "bar") - - def testVerifiesWhenMethodIsUsingKeywordArguments(self): - self.mock.foo() - self.mock.someOtherMethod(1, fooarg="foo", bararg="bar") - - self.verification_function(self.mock).foo() - self.verification_function(self.mock).someOtherMethod(1, bararg="bar", fooarg="foo") - - def testVerifiesDetectsNamedArguments(self): - self.mock.foo(fooarg="foo", bararg="bar") - - self.verification_function(self.mock).foo(bararg="bar", fooarg="foo") - try: - self.verification_function(self.mock).foo(bararg="foo", fooarg="bar") - self.fail(); - except VerificationError: - pass - - def testFailsVerification(self): - self.mock.foo("boo") - - self.assertRaises(VerificationError, self.verification_function(self.mock).foo, "not boo") - - def testVerifiesAnyTimes(self): - self.mock = mock() - self.mock.foo() - - self.verification_function(self.mock).foo() - self.verification_function(self.mock).foo() - self.verification_function(self.mock).foo() - - def testVerifiesMultipleCalls(self): - self.mock = mock() - self.mock.foo() - self.mock.foo() - self.mock.foo() - - self.verification_function(self.mock, times=3).foo() - - def testFailsVerificationOfMultipleCalls(self): - self.mock = mock() - self.mock.foo() - self.mock.foo() - self.mock.foo() - - self.assertRaises(VerificationError, self.verification_function(self.mock, times=2).foo) - - def testVerifiesUsingAnyMatcher(self): - self.mock.foo(1, "bar") - - self.verification_function(self.mock).foo(1, any()) - self.verification_function(self.mock).foo(any(), "bar") - self.verification_function(self.mock).foo(any(), any()) - - def testVerifiesUsingAnyIntMatcher(self): - self.mock.foo(1, "bar") - - self.verification_function(self.mock).foo(any(int), "bar") - - def testFailsVerificationUsingAnyIntMatcher(self): - self.mock.foo(1, "bar") - - self.assertRaises(VerificationError, self.verification_function(self.mock).foo, 1, any(int)) - self.assertRaises(VerificationError, self.verification_function(self.mock).foo, any(int)) - - def testNumberOfTimesDefinedDirectlyInVerify(self): - self.mock.foo("bar") - - self.verification_function(self.mock, times=1).foo("bar") - - def testFailsWhenTimesIsLessThanZero(self): - self.assertRaises(ArgumentError, self.verification_function, None, -1) - - def testVerifiesAtLeastTwoWhenMethodInvokedTwice(self): - self.mock.foo() - self.mock.foo() - - self.verification_function(self.mock, atleast=2).foo() - - def testVerifiesAtLeastTwoWhenMethodInvokedFourTimes(self): - self.mock.foo() - self.mock.foo() - self.mock.foo() - self.mock.foo() - - self.verification_function(self.mock, atleast=2).foo() - - def testFailsWhenMethodInvokedOnceForAtLeastTwoVerification(self): - self.mock.foo() - self.assertRaises(VerificationError, self.verification_function(self.mock, atleast=2).foo) - - def testVerifiesAtMostTwoWhenMethodInvokedTwice(self): - self.mock.foo() - self.mock.foo() - - self.verification_function(self.mock, atmost=2).foo() - - def testVerifiesAtMostTwoWhenMethodInvokedOnce(self): - self.mock.foo() - - self.verification_function(self.mock, atmost=2).foo() - - def testFailsWhenMethodInvokedFourTimesForAtMostTwoVerification(self): - self.mock.foo() - self.mock.foo() - self.mock.foo() - self.mock.foo() - - self.assertRaises(VerificationError, self.verification_function(self.mock, atmost=2).foo) - - def testVerifiesBetween(self): - self.mock.foo() - self.mock.foo() - - self.verification_function(self.mock, between=[1, 2]).foo() - self.verification_function(self.mock, between=[2, 3]).foo() - self.verification_function(self.mock, between=[1, 5]).foo() - self.verification_function(self.mock, between=[2, 2]).foo() - - def testFailsVerificationWithBetween(self): - self.mock.foo() - self.mock.foo() - self.mock.foo() - - self.assertRaises(VerificationError, self.verification_function(self.mock, between=[1, 2]).foo) - self.assertRaises(VerificationError, self.verification_function(self.mock, between=[4, 9]).foo) - - def testFailsAtMostAtLeastAndBetweenVerificationWithWrongArguments(self): - self.assertRaises(ArgumentError, self.verification_function, self.mock, atleast=0) - self.assertRaises(ArgumentError, self.verification_function, self.mock, atleast=-5) - self.assertRaises(ArgumentError, self.verification_function, self.mock, atmost=0) - self.assertRaises(ArgumentError, self.verification_function, self.mock, atmost=-5) - self.assertRaises(ArgumentError, self.verification_function, self.mock, between=[5, 1]) - self.assertRaises(ArgumentError, self.verification_function, self.mock, between=[-1, 1]) - self.assertRaises(ArgumentError, self.verification_function, self.mock, atleast=5, atmost=5) - self.assertRaises(ArgumentError, self.verification_function, self.mock, atleast=5, between=[1, 2]) - self.assertRaises(ArgumentError, self.verification_function, self.mock, atmost=5, between=[1, 2]) - self.assertRaises(ArgumentError, self.verification_function, self.mock, atleast=5, atmost=5, between=[1, 2]) - - def runTest(self): - pass - -class VerifyTest(VerificationTestBase): - def __init__(self, *args, **kwargs): - VerificationTestBase.__init__(self, verify, *args, **kwargs) - - def testVerifyNeverCalled(self): - verify(self.mock, never).someMethod() - - def testVerifyNeverCalledRaisesError(self): - self.mock.foo() - self.assertRaises(VerificationError, verify(self.mock, never).foo) - -class InorderVerifyTest(VerificationTestBase): - def __init__(self, *args, **kwargs): - VerificationTestBase.__init__(self, inorder.verify, *args, **kwargs) - - def setUp(self): - self.mock = mock() - - def testPassesIfOneIteraction(self): - self.mock.first() - inorder.verify(self.mock).first() - - def testPassesIfMultipleInteractions(self): - self.mock.first() - self.mock.second() - self.mock.third() - - inorder.verify(self.mock).first() - inorder.verify(self.mock).second() - inorder.verify(self.mock).third() - - def testFailsIfNoInteractions(self): - self.assertRaises(VerificationError, inorder.verify(self.mock).first) - - def testFailsIfWrongOrderOfInteractions(self): - self.mock.first() - self.mock.second() - - self.assertRaises(VerificationError, inorder.verify(self.mock).second) - - def testErrorMessage(self): - self.mock.second() - self.mock.first() - self.assertRaisesMessage("\nWanted first() to be invoked, got second() instead", inorder.verify(self.mock).first) - - - def testPassesMixedVerifications(self): - self.mock.first() - self.mock.second() - - verify(self.mock).first() - verify(self.mock).second() - - inorder.verify(self.mock).first() - inorder.verify(self.mock).second() - - def testFailsMixedVerifications(self): - self.mock.second() - self.mock.first() - - # first - normal verifications, they should pass - verify(self.mock).first() - verify(self.mock).second() - - # but, inorder verification should fail - self.assertRaises(VerificationError, inorder.verify(self.mock).first) - - -class VerifyNoMoreInteractionsTest(TestBase): - def testVerifies(self): - mockOne, mockTwo = mock(), mock() - mockOne.foo() - mockTwo.bar() - - verify(mockOne).foo() - verify(mockTwo).bar() - verifyNoMoreInteractions(mockOne, mockTwo) - - def testFails(self): - theMock = mock() - theMock.foo() - self.assertRaises(VerificationError, verifyNoMoreInteractions, theMock) - - -class VerifyZeroInteractionsTest(TestBase): - def testVerifies(self): - theMock = mock() - verifyZeroInteractions(theMock) - theMock.foo() - self.assertRaises(VerificationError, verifyNoMoreInteractions, theMock) - - -if __name__ == '__main__': - main() - diff --git a/mockito_util/__init__.py b/mockito_util/__init__.py deleted file mode 100644 index 2000e67..0000000 --- a/mockito_util/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. diff --git a/mockito_util/test.py b/mockito_util/test.py deleted file mode 100644 index 576a0a6..0000000 --- a/mockito_util/test.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from unittest import TestLoader as BaseTestLoader, TestSuite -import sys - -class TestLoader(BaseTestLoader): - def loadTestsFromName(self, name, module=None): - suite = TestSuite() - for test in findTests(name): - sys.path.insert(0, name) # python3 compatibility - suite.addTests(super(TestLoader, self).loadTestsFromName(test)) - del sys.path[0] # python3 compatibility - return suite - - def loadTestsFromNames(self, names, module=None): - suite = TestSuite() - for name in names: - suite.addTests(self.loadTestsFromName(name)) - return suite - -def findTests(dir): - import os, re - pattern = re.compile('([a-z]+_)+test\.py$') - for fileName in os.listdir(dir): - if pattern.match(fileName): - yield os.path.join(dir, fileName).replace('.py', '').replace(os.sep, '.') - -__all__ = [TestLoader] - diff --git a/mockito_util/write_readme.py b/mockito_util/write_readme.py deleted file mode 100644 index 01ee5cc..0000000 --- a/mockito_util/write_readme.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2008-2013 Szczepan Faber, Serhiy Oplakanets, Herr Kaste -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import os -import re - -def openFile(f, m='r'): - if (os.path.exists(f)): - return open(f, m) - else: - return open('../' + f, m) - -demo_test = ' '.join(openFile('mockito_test/demo_test.py').readlines()) -demo_test = demo_test.split('#DELIMINATOR')[1] - -readme_before = ''.join(openFile('README.rst').readlines()) -token = 'Basic usage:' -readme_after = re.compile(token + '.*', re.S).sub(token + '\n' + demo_test, readme_before) - -if (readme_before != readme_after): - readme_file = openFile('README.rst', 'w') - readme_file.write(readme_after) - print "README updated" -else: - print "README update not required" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 373068c..0000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[nosetests] -where = mockito_test -detailed-errors = 1 - -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 - diff --git a/setup.py b/setup.py index 46cf175..db955cf 100755 --- a/setup.py +++ b/setup.py @@ -1,37 +1,40 @@ -#!/usr/bin/env python -# coding: utf-8 - -from distribute_setup import use_setuptools -use_setuptools() - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup +from setuptools import setup import sys -extra = {} -if sys.version_info >= (3,): - extra['use_2to3'] = True +import re +import ast + + +_version_re = re.compile(r'__version__\s+=\s+(.*)') + +with open('mockito/__init__.py', 'rb') as f: + version = str(ast.literal_eval(_version_re.search( + f.read().decode('utf-8')).group(1))) + + +install_requires = ['funcsigs'] if sys.version_info < (3,) else [] setup(name='mockito', - version='0.5.2', - packages=['mockito', 'mockito_test', 'mockito_util'], - url='http://code.google.com/p/mockito-python', - download_url='http://code.google.com/p/mockito-python/downloads/list', - maintainer='Justin Hopper', - maintainer_email='mockito-python@googlegroups.com', + version=version, + packages=['mockito'], + url='https://github.com/kaste/mockito-python', + maintainer='herr.kaste', + maintainer_email='herr.kaste@gmail.com', license='MIT', description='Spying framework', - long_description='Mockito is a spying framework based on Java library with the same name.', - classifiers=['Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Topic :: Software Development :: Testing', - 'Programming Language :: Python :: 3' - ], - test_suite='nose.collector', - py_modules=['distribute_setup'], - setup_requires=['nose'], - **extra) - + long_description=open('README.rst').read(), + install_requires=install_requires, + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Topic :: Software Development :: Testing', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ]) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/classmethods_test.py b/tests/classmethods_test.py new file mode 100644 index 0000000..69d8cf7 --- /dev/null +++ b/tests/classmethods_test.py @@ -0,0 +1,107 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from .test_base import TestBase +from mockito import when, unstub, verify +from mockito.verification import VerificationError + +class Dog: + @classmethod + def bark(cls): + return "woof!" + + +class Cat: + @classmethod + def meow(cls, m): + return cls.__name__ + " " + str(m) + + +class Lion(object): + @classmethod + def roar(cls): + return "Rrrrr!" + + +class ClassMethodsTest(TestBase): + + def tearDown(self): + unstub() + + def testUnstubs(self): + when(Dog).bark().thenReturn("miau!") + unstub() + self.assertEqual("woof!", Dog.bark()) + + # TODO decent test case please :) without testing irrelevant implementation + # details + def testUnstubShouldPreserveMethodType(self): + when(Dog).bark().thenReturn("miau!") + unstub() + self.assertTrue(isinstance(Dog.__dict__.get("bark"), classmethod)) + + def testStubs(self): + self.assertEqual("woof!", Dog.bark()) + + when(Dog).bark().thenReturn("miau!") + + self.assertEqual("miau!", Dog.bark()) + + def testStubsClassesDerivedFromTheObjectClass(self): + self.assertEqual("Rrrrr!", Lion.roar()) + + when(Lion).roar().thenReturn("miau!") + + self.assertEqual("miau!", Lion.roar()) + + def testVerifiesMultipleCallsOnClassmethod(self): + when(Dog).bark().thenReturn("miau!") + + Dog.bark() + Dog.bark() + + verify(Dog, times=2).bark() + + def testFailsVerificationOfMultipleCallsOnClassmethod(self): + when(Dog).bark().thenReturn("miau!") + + Dog.bark() + + self.assertRaises(VerificationError, verify(Dog, times=2).bark) + + def testStubsAndVerifiesClassmethod(self): + when(Dog).bark().thenReturn("miau!") + + self.assertEqual("miau!", Dog.bark()) + + verify(Dog).bark() + + def testPreservesClassArgumentAfterUnstub(self): + self.assertEqual("Cat foo", Cat.meow("foo")) + + when(Cat).meow("foo").thenReturn("bar") + + self.assertEqual("bar", Cat.meow("foo")) + + unstub() + + self.assertEqual("Cat foo", Cat.meow("foo")) + + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0dea3a4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ + +import pytest + + +@pytest.fixture +def unstub(): + from mockito import unstub + yield + unstub() diff --git a/tests/ellipsis_test.py b/tests/ellipsis_test.py new file mode 100644 index 0000000..e4e5860 --- /dev/null +++ b/tests/ellipsis_test.py @@ -0,0 +1,398 @@ + +import pytest + +from collections import namedtuple + +from mockito import when, args, kwargs, invocation, mock + + +class Dog(object): + def bark(self, sound): + return "%s!" % sound + + def waggle(self): + return 'waggle' + +class CallSignature(namedtuple('CallSignature', 'args kwargs')): + def raises(self, reason): + return pytest.mark.xfail(self, raises=reason, strict=True) + +def sig(*args, **kwargs): + return CallSignature(args, kwargs) + + +class TestCallMethodWithSignature: + def testNoArg(self): + rex = Dog() + when(rex).waggle().thenReturn('wuff') + + assert rex.waggle() == 'wuff' + + with pytest.raises(TypeError): + rex.waggle(1) + with pytest.raises(TypeError): + rex.waggle(Ellipsis) + with pytest.raises(TypeError): + rex.waggle(args) + with pytest.raises(TypeError): + rex.waggle(kwargs) + with pytest.raises(TypeError): + rex.waggle(*args) + with pytest.raises(TypeError): + rex.waggle(**kwargs) + + def testExpectingSpecificInputAsPositionalArgument(self): + rex = Dog() + when(rex).bark(1).thenReturn('wuff') + + assert rex.bark(1) == 'wuff' + + with pytest.raises(invocation.InvocationError): + rex.bark(sound=1) + with pytest.raises(invocation.InvocationError): + rex.bark(Ellipsis) + with pytest.raises(invocation.InvocationError): + rex.bark(args) + with pytest.raises(invocation.InvocationError): + rex.bark(*args) + with pytest.raises(invocation.InvocationError): + rex.bark(kwargs) + + with pytest.raises(TypeError): + rex.bark(1, 2) + with pytest.raises(TypeError): + rex.bark(wuff=1) + with pytest.raises(TypeError): + rex.bark(**kwargs) + + def testExpectingSpecificInputAsKeyword(self): + rex = Dog() + when(rex).bark(sound=1).thenReturn('wuff') + + assert rex.bark(sound=1) == 'wuff' + + with pytest.raises(invocation.InvocationError): + rex.bark(1) + with pytest.raises(invocation.InvocationError): + rex.bark(Ellipsis) + with pytest.raises(invocation.InvocationError): + rex.bark(args) + with pytest.raises(invocation.InvocationError): + rex.bark(*args) + with pytest.raises(invocation.InvocationError): + rex.bark(kwargs) + + with pytest.raises(TypeError): + rex.bark(1, 2) + with pytest.raises(TypeError): + rex.bark(wuff=1) + with pytest.raises(TypeError): + rex.bark(**kwargs) + + def testExpectingStarKwargs(self): + rex = Dog() + when(rex).bark(**kwargs).thenReturn('wuff') + + assert rex.bark(sound='miau') == 'wuff' + + with pytest.raises(invocation.InvocationError): + rex.bark('miau') + with pytest.raises(invocation.InvocationError): + rex.bark(Ellipsis) + with pytest.raises(invocation.InvocationError): + rex.bark(kwargs) + with pytest.raises(invocation.InvocationError): + rex.bark(args) + + with pytest.raises(TypeError): + rex.bark(wuff='miau') + with pytest.raises(TypeError): + rex.bark(**kwargs) + + def testExpectingEllipsis(self): + rex = Dog() + when(rex).bark(Ellipsis).thenReturn('wuff') + + assert rex.bark('miau') == 'wuff' + with pytest.raises(TypeError): + rex.bark('miau', 'miau') + + assert rex.bark(sound='miau') == 'wuff' + with pytest.raises(TypeError): + rex.bark(wuff='miau') + + assert rex.bark(Ellipsis) == 'wuff' + assert rex.bark(args) == 'wuff' + assert rex.bark(*args) == 'wuff' + assert rex.bark(kwargs) == 'wuff' + + with pytest.raises(TypeError): + rex.bark(**kwargs) == 'wuff' + + def testExpectingStarArgs(self): + rex = Dog() + when(rex).bark(*args).thenReturn('wuff') + + assert rex.bark('miau') == 'wuff' + + with pytest.raises(invocation.InvocationError): + rex.bark(sound='miau') + with pytest.raises(TypeError): + rex.bark(wuff='miau') + + assert rex.bark(*args) == 'wuff' + assert rex.bark(Ellipsis) == 'wuff' + + with pytest.raises(TypeError): + rex.bark(**kwargs) + + +class TestEllipsises: + + # In python3 `bark(...)` is actually valid, but the tests must + # be downwards compatible to python 2 + + @pytest.mark.parametrize('call', [ + sig(), + sig('Wuff'), + sig('Wuff', 'Wuff'), + sig('Wuff', then='Wuff'), + sig(then='Wuff'), + ]) + def testEllipsisAsSoleArgumentAlwaysPasses(self, call): + rex = mock() + when(rex).bark(Ellipsis).thenReturn('Miau') + + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + + @pytest.mark.parametrize('call', [ + sig('Wuff'), + sig('Wuff', 'Wuff'), + sig('Wuff', then='Wuff'), + ]) + def testEllipsisAsSecondArgumentPasses(self, call): + rex = mock() + when(rex).bark('Wuff', Ellipsis).thenReturn('Miau') + + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + @pytest.mark.parametrize('call', [ + sig(), + sig(then='Wuff'), + ]) + def testEllipsisAsSecondArgumentRejections(self, call): + rex = mock() + when(rex).bark('Wuff', Ellipsis).thenReturn('Miau') + + with pytest.raises(AssertionError): + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + + @pytest.mark.parametrize('call', [ + sig(), + sig('Wuff'), + sig('Wuff', 'Wuff'), + ]) + def testArgsAsSoleArgumentPasses(self, call): + rex = mock() + when(rex).bark(*args).thenReturn('Miau') + + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + @pytest.mark.parametrize('call', [ + sig('Wuff', then='Wuff'), + sig(then='Wuff'), + ]) + def testArgsAsSoleArgumentRejections(self, call): + rex = mock() + when(rex).bark(*args).thenReturn('Miau') + + with pytest.raises(AssertionError): + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + + @pytest.mark.parametrize('call', [ + sig('Wuff'), + sig('Wuff', 'Wuff'), + ]) + def testArgsAsSecondArgumentPasses(self, call): + rex = mock() + when(rex).bark('Wuff', *args).thenReturn('Miau') + + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + @pytest.mark.parametrize('call', [ + sig(), + sig('Wuff', then='Wuff'), + sig(then='Wuff'), + ]) + def testArgsAsSecondArgumentRejections(self, call): + rex = mock() + when(rex).bark('Wuff', *args).thenReturn('Miau') + + with pytest.raises(AssertionError): + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + + @pytest.mark.parametrize('call', [ + sig('Wuff', then='Wuff'), + sig('Wuff', 'Wuff', then='Wuff'), + + ]) + def testArgsBeforeConcreteKwargPasses(self, call): + rex = mock() + when(rex).bark('Wuff', *args, then='Wuff').thenReturn('Miau') + + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + @pytest.mark.parametrize('call', [ + sig(), + sig('Wuff'), + sig('Wuff', 'Wuff'), + sig(then='Wuff'), + + ]) + def testArgsBeforeConcreteKwargRejections(self, call): + rex = mock() + when(rex).bark('Wuff', *args, then='Wuff').thenReturn('Miau') + + with pytest.raises(AssertionError): + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + + @pytest.mark.parametrize('call', [ + sig(), + sig(then='Wuff'), + sig(then='Wuff', later='Waff') + ]) + def testKwargsAsSoleArgumentPasses(self, call): + rex = mock() + when(rex).bark(**kwargs).thenReturn('Miau') + + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + @pytest.mark.parametrize('call', [ + sig('Wuff'), + sig('Wuff', 'Wuff'), + sig('Wuff', then='Wuff'), + sig('Wuff', 'Wuff', then='Wuff'), + ]) + def testKwargsAsSoleArgumentRejections(self, call): + rex = mock() + when(rex).bark(**kwargs).thenReturn('Miau') + + with pytest.raises(AssertionError): + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + + @pytest.mark.parametrize('call', [ + sig(then='Wuff'), + sig(then='Wuff', later='Waff'), + sig(later='Waff', then='Wuff'), + ]) + def testKwargsAsSecondKwargPasses(self, call): + rex = mock() + when(rex).bark(then='Wuff', **kwargs).thenReturn('Miau') + + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + @pytest.mark.parametrize('call', [ + sig(), + sig('Wuff'), + sig('Wuff', 'Wuff'), + sig('Wuff', then='Wuff'), + sig('Wuff', 'Wuff', then='Wuff'), + sig(first='Wuff', later='Waff') + ]) + def testKwargsAsSecondKwargRejections(self, call): + rex = mock() + when(rex).bark(then='Wuff', **kwargs).thenReturn('Miau') + + with pytest.raises(AssertionError): + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + + @pytest.mark.parametrize('call', [ + sig('Wuff', then='Waff'), + sig('Wuff', 'Wuff', then='Waff'), + sig('Wuff', then='Waff', later='Woff'), + sig('Wuff', first="Wiff", then='Waff', later='Woff'), + sig('Wuff', 'Wuff', then='Waff', later="Woff"), + ]) + def testCombinedArgsAndKwargsPasses(self, call): + rex = mock() + when(rex).bark('Wuff', *args, then='Waff', **kwargs).thenReturn('Miau') + + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + @pytest.mark.parametrize('call', [ + sig(), + sig('Wuff'), + sig('Wuff', 'Wuff'), + sig(later='Woff'), + sig('Wuff', later='Woff'), + ]) + def testCombinedArgsAndKwargsRejections(self, call): + rex = mock() + when(rex).bark('Wuff', *args, then='Waff', **kwargs).thenReturn('Miau') + + with pytest.raises(AssertionError): + assert rex.bark(*call.args, **call.kwargs) == 'Miau' + + + @pytest.mark.parametrize('call', [ + sig(Ellipsis), + ]) + def testEllipsisMustBeLastThing(self, call): + rex = mock() + when(rex).bark(*call.args, **call.kwargs).thenReturn('Miau') + + @pytest.mark.parametrize('call', [ + sig(Ellipsis, 'Wuff'), + sig(Ellipsis, then='Wuff'), + sig(Ellipsis, 'Wuff', then='Waff'), + ]) + def testEllipsisMustBeLastThingRejections(self, call): + rex = mock() + with pytest.raises(TypeError): + when(rex).bark(*call.args, **call.kwargs).thenReturn('Miau') + + + def testArgsMustUsedAsStarArg(self): + rex = mock() + with pytest.raises(TypeError): + when(rex).bark(args).thenReturn('Miau') + + def testKwargsMustBeUsedAsStarKwarg(self): + rex = mock() + with pytest.raises(TypeError): + when(rex).bark(kwargs).thenReturn('Miau') + + with pytest.raises(TypeError): + when(rex).bark(*kwargs).thenReturn('Miau') + + def testNiceFormattingForEllipsis(self): + m = mock() + m.strict = False + inv = invocation.StubbedInvocation(m, 'bark', None) + inv(Ellipsis) + + assert repr(inv) == 'bark(...)' + + def testNiceFormattingForArgs(self): + m = mock() + m.strict = False + inv = invocation.StubbedInvocation(m, 'bark', None) + inv(*args) + + assert repr(inv) == 'bark(*args)' + + def testNiceFormattingForKwargs(self): + m = mock() + m.strict = False + inv = invocation.StubbedInvocation(m, 'bark', None) + inv(**kwargs) + + assert repr(inv) == 'bark(**kwargs)' + diff --git a/tests/instancemethods_test.py b/tests/instancemethods_test.py new file mode 100644 index 0000000..860d31d --- /dev/null +++ b/tests/instancemethods_test.py @@ -0,0 +1,462 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +from .test_base import TestBase +from mockito import ( + mock, when, expect, unstub, ANY, verify, verifyNoMoreInteractions, + verifyZeroInteractions, verifyNoUnwantedInteractions, + verifyStubbedInvocationsAreUsed) +from mockito.invocation import InvocationError +from mockito.verification import VerificationError + +pytestmark = pytest.mark.usefixtures("unstub") + +class Dog(object): + def waggle(self): + return "Wuff!" + + def bark(self, sound): + return "%s!" % sound + + def do_default_bark(self): + return self.bark('Wau') + + def __call__(self): + pass + +class InstanceMethodsTest(TestBase): + def tearDown(self): + unstub() + + def testUnstubClassMethod(self): + original_method = Dog.waggle + when(Dog).waggle().thenReturn('Nope!') + + unstub() + + rex = Dog() + self.assertEqual('Wuff!', rex.waggle()) + self.assertEqual(original_method, Dog.waggle) + + def testUnstubMockedInstanceMethod(self): + rex = Dog() + when(rex).waggle().thenReturn('Nope!') + assert rex.waggle() == 'Nope!' + unstub() + assert rex.waggle() == 'Wuff!' + + def testUnstubMockedInstanceDoesNotHideTheClass(self): + when(Dog).waggle().thenReturn('Nope!') + rex = Dog() + when(rex).waggle().thenReturn('Sure!') + assert rex.waggle() == 'Sure!' + + unstub() + assert rex.waggle() == 'Wuff!' + + + def testStubAnInstanceMethod(self): + when(Dog).waggle().thenReturn('Boing!') + + rex = Dog() + self.assertEqual('Boing!', rex.waggle()) + + def testStubsAnInstanceMethodWithAnArgument(self): + when(Dog).bark('Miau').thenReturn('Wuff') + + rex = Dog() + self.assertEqual('Wuff', rex.bark('Miau')) + + def testInvokeAStubbedMethodFromAnotherMethod(self): + when(Dog).bark('Wau').thenReturn('Wuff') + + rex = Dog() + self.assertEqual('Wuff', rex.do_default_bark()) + verify(Dog).bark('Wau') + + def testYouCantStubAnUnknownMethodInStrictMode(self): + try: + when(Dog).barks('Wau').thenReturn('Wuff') + self.fail( + 'Stubbing an unknown method should have thrown a exception') + except InvocationError: + pass + + def testStubUnknownMethodInLooseMode(self): + when(Dog, strict=False).walk() + + rex = Dog() + rex.walk() + + unstub() + with pytest.raises(AttributeError): + rex.walk + with pytest.raises(AttributeError): + Dog.walk + + def testAddNewMethodOnInstanceInLooseMode(self): + rex = Dog() + when(rex, strict=False).walk() + rex.walk() + + unstub() + with pytest.raises(AttributeError): + rex.walk + + def testThrowEarlyIfCallingWithUnexpectedArgumentsInStrictMode(self): + rex = Dog() + when(rex).bark('Miau').thenReturn('Wuff') + + with pytest.raises(InvocationError): + rex.bark('Shhh') + + def testNiceErrorMessageOnUnexpectedCall(self): + theMock = mock(strict=True) + when(theMock).foo('bar') + when(theMock).foo(12, baz='boz') + when(theMock).bar('foo') # <==== omitted from output! + + with pytest.raises(InvocationError) as exc: + theMock.foo(True, None) + + assert str(exc.value) == ''' +Called but not expected: + + foo(True, None) + +Stubbed invocations are: + + foo('bar') + foo(12, baz='boz') + +''' + + def testStubCallableObject(self): + when(Dog).__call__().thenReturn('done') + + rex = Dog() # <= important. not stubbed + assert rex() == 'done' + + def testReturnNoneIfCallingWithUnexpectedArgumentsIfNotStrict(self): + when(Dog, strict=False).bark('Miau').thenReturn('Wuff') + rex = Dog() + self.assertEqual(None, rex.bark('Shhh')) + + def testStubInstancesInsteadOfClasses(self): + rex = Dog() + when(rex).bark('Miau').thenReturn('Wuff') + + self.assertEqual('Wuff', rex.bark('Miau')) + verify(rex, times=1).bark(ANY) + + max = Dog() + self.assertEqual('Miau!', max.bark('Miau')) + + def testUnstubInstance(self): + rex = Dog() + when(rex).bark('Miau').thenReturn('Wuff') + + unstub() + + assert rex.bark('Miau') == 'Miau!' + + + def testNoExplicitReturnValueMeansNone(self): + when(Dog).bark('Miau').thenReturn() + rex = Dog() + + self.assertEqual(None, rex.bark('Miau')) + + def testForgottenThenReturnMeansReturnNone(self): + when(Dog).bark('Miau') + when(Dog).waggle() + rex = Dog() + + self.assertEqual(None, rex.bark('Miau')) + self.assertEqual(None, rex.waggle()) + +class TestVerifyInteractions: + class TestZeroInteractions: + def testVerifyNoMoreInteractionsWorks(self): + when(Dog).bark('Miau') + verifyNoMoreInteractions(Dog) + + def testVerifyZeroInteractionsWorks(self): + when(Dog).bark('Miau') + verifyZeroInteractions(Dog) + + class TestOneInteraction: + def testNothingVerifiedVerifyNoMoreInteractionsRaises(self): + when(Dog).bark('Miau') + rex = Dog() + rex.bark('Miau') + with pytest.raises(VerificationError): + verifyNoMoreInteractions(Dog) + + def testIfVerifiedVerifyNoMoreInteractionsPasses(self): + when(Dog).bark('Miau') + rex = Dog() + rex.bark('Miau') + verify(Dog).bark('Miau') + verifyNoMoreInteractions(Dog) + + def testNothingVerifiedVerifyZeroInteractionsRaises(self): + when(Dog).bark('Miau') + rex = Dog() + rex.bark('Miau') + with pytest.raises(VerificationError): + verifyZeroInteractions(Dog) + + def testIfVerifiedVerifyZeroInteractionsStillRaises(self): + when(Dog).bark('Miau') + rex = Dog() + rex.bark('Miau') + verify(Dog).bark('Miau') + with pytest.raises(VerificationError): + verifyZeroInteractions(Dog) + +class TestEnsureStubsAreUsed: + def testBarkOnUnusedStub(self): + when(Dog).bark('Miau') + with pytest.raises(VerificationError): + verifyStubbedInvocationsAreUsed(Dog) + + class TestPassIfExplicitlyVerified: + @pytest.mark.parametrize('verification', [ + {'times': 0}, + {'between': [0, 3]} + ]) + def testPassIfExplicitlyVerified(self, verification): + dog = mock() + when(dog).waggle().thenReturn('Sure') + verify(dog, **verification).waggle() + + verifyStubbedInvocationsAreUsed(dog) + + def testWildcardCallSignatureOnVerify(self): + dog = mock() + when(dog).waggle(1).thenReturn('Sure') + verify(dog, times=0).waggle(Ellipsis) + + verifyStubbedInvocationsAreUsed(dog) + + @pytest.mark.xfail(reason='Not implemented.') + def testPassIfVerifiedZeroInteractions(self): + dog = mock() + when(dog).waggle(1).thenReturn('Sure') + verifyZeroInteractions(dog) + + verifyStubbedInvocationsAreUsed(dog) + + @pytest.mark.xfail(reason='Not implemented.') + def testPassIfVerifiedNoMoreInteractions(self): + dog = mock() + when(dog).waggle(1).thenReturn('Sure') + verifyNoMoreInteractions(dog) + + verifyStubbedInvocationsAreUsed(dog) + + def testWildacardCallSignatureOnStub(self): + dog = mock() + when(dog).waggle(Ellipsis).thenReturn('Sure') + verify(dog, times=0).waggle(1) + + verifyStubbedInvocationsAreUsed(dog) + + def testPassIfExplicitlyVerified4(self): + dog = mock() + when(dog).waggle(1).thenReturn('Sure') + when(dog).waggle(2).thenReturn('Sure') + verify(dog, times=0).waggle(Ellipsis) + + verifyStubbedInvocationsAreUsed(dog) + + class TestPassIfImplicitlyVerifiedViaExpect: + @pytest.mark.parametrize('verification', [ + {'times': 0}, + {'between': [0, 3]} + ]) + def testPassIfImplicitlyVerified(self, verification): + dog = mock() + expect(dog, **verification).waggle().thenReturn('Sure') + + verifyStubbedInvocationsAreUsed(dog) + + def testPassUsedOnceImplicitAnswer(self): + when(Dog).bark('Miau') + rex = Dog() + rex.bark('Miau') + verifyStubbedInvocationsAreUsed(Dog) + + def testPassUsedOnce(self): + dog = mock() + when(dog).waggle().thenReturn('Sure') + + dog.waggle() + verifyStubbedInvocationsAreUsed(dog) + + def testFailSecondStubNotUsed(self): + when(Dog).bark('Miau') + when(Dog).waggle() + rex = Dog() + rex.bark('Miau') + with pytest.raises(VerificationError): + verifyStubbedInvocationsAreUsed(Dog) + + def testFailSecondStubSameMethodUnused(self): + when(Dog).bark('Miau') + when(Dog).bark('Grrr') + rex = Dog() + rex.bark('Miau') + with pytest.raises(VerificationError): + verifyStubbedInvocationsAreUsed(Dog) + + def testPassTwoStubsOnSameMethodUsed(self): + when(Dog).bark('Miau') + when(Dog).bark('Grrr') + rex = Dog() + rex.bark('Miau') + rex.bark('Grrr') + verifyStubbedInvocationsAreUsed(Dog) + + def testPassOneCatchAllOneSpecificStubBothUsed(self): + when(Dog).bark(Ellipsis) + when(Dog).bark('Miau') + rex = Dog() + rex.bark('Miau') + rex.bark('Grrr') + verifyStubbedInvocationsAreUsed(Dog) + + def testFailSecondAnswerUnused(self): + when(Dog).bark('Miau').thenReturn('Yep').thenReturn('Nop') + rex = Dog() + rex.bark('Miau') + with pytest.raises(VerificationError): + verifyStubbedInvocationsAreUsed(Dog) + + +@pytest.mark.usefixtures('unstub') +class TestImplicitVerificationsUsingExpect: + + @pytest.fixture(params=[ + {'times': 2}, + {'atmost': 2}, + {'between': [1, 2]} + ], ids=['times', 'atmost', 'between']) + def verification(self, request): + return request.param + + def testFailImmediatelyIfWantedCountExceeds(self, verification): + rex = Dog() + expect(rex, **verification).bark('Miau').thenReturn('Wuff') + rex.bark('Miau') + rex.bark('Miau') + + with pytest.raises(InvocationError): + rex.bark('Miau') + + def testVerifyNoMoreInteractionsWorks(self, verification): + rex = Dog() + expect(rex, **verification).bark('Miau').thenReturn('Wuff') + rex.bark('Miau') + rex.bark('Miau') + + verifyNoMoreInteractions(rex) + + def testNoUnwantedInteractionsWorks(self, verification): + rex = Dog() + expect(rex, **verification).bark('Miau').thenReturn('Wuff') + rex.bark('Miau') + rex.bark('Miau') + + verifyNoUnwantedInteractions(rex) + + @pytest.mark.parametrize('verification', [ + {'times': 2}, + {'atleast': 2}, + {'between': [1, 2]} + ], ids=['times', 'atleast', 'between']) + def testVerifyNoMoreInteractionsBarksIfUnsatisfied(self, verification): + rex = Dog() + expect(rex, **verification).bark('Miau').thenReturn('Wuff') + + with pytest.raises(VerificationError): + verifyNoMoreInteractions(rex) + + @pytest.mark.parametrize('verification', [ + {'times': 2}, + {'atleast': 2}, + {'between': [1, 2]} + ], ids=['times', 'atleast', 'between']) + def testNoUnwantedInteractionsBarksIfUnsatisfied(self, verification): + rex = Dog() + expect(rex, **verification).bark('Miau').thenReturn('Wuff') + + with pytest.raises(VerificationError): + verifyNoUnwantedInteractions(rex) + + def testNoUnwantedInteractionsForAllRegisteredObjects(self): + rex = Dog() + mox = Dog() + + expect(rex, times=1).bark('Miau') + expect(mox, times=1).bark('Miau') + + rex.bark('Miau') + mox.bark('Miau') + + verifyNoUnwantedInteractions() + + def testUseWhenAndExpectTogetherVerifyNoUnwatedInteractions(self): + rex = Dog() + when(rex).waggle() + expect(rex, times=1).bark('Miau') + + rex.waggle() + rex.bark('Miau') + + verifyNoUnwantedInteractions() + + def testExpectWitoutVerification(self): + rex = Dog() + expect(rex).bark('Miau').thenReturn('Wuff') + verifyNoMoreInteractions(rex) + + rex.bark('Miau') + with pytest.raises(VerificationError): + verifyNoMoreInteractions(rex) + + # Where to put this test? During first implementation I broke this + def testEnsureWhenGetsNotConfused(self): + m = mock() + when(m).foo(1).thenReturn() + m.foo(1) + with pytest.raises(VerificationError): + verifyNoMoreInteractions(m) + + def testEnsureMultipleExpectsArentConfused(self): + rex = Dog() + expect(rex, times=1).bark('Miau').thenReturn('Wuff') + expect(rex, times=1).waggle().thenReturn('Wuff') + rex.bark('Miau') + rex.waggle() + diff --git a/tests/late_imports_test.py b/tests/late_imports_test.py new file mode 100644 index 0000000..e844fef --- /dev/null +++ b/tests/late_imports_test.py @@ -0,0 +1,97 @@ + +import pytest + +from mockito.utils import get_obj, get_obj_attr_tuple + +import sys + + +PY3 = sys.version_info >= (3,) + + +def foo(): + pass + +class TestLateImports: + + def testOs(self): + import os + assert get_obj('os') is os + + def testOsPath(self): + import os.path + assert get_obj('os.path') is os.path + + def testOsPathExists(self): + import os.path + assert get_obj('os.path.exists') is os.path.exists + + def testOsPathWhatever(self): + with pytest.raises(AttributeError) as exc: + get_obj('os.path.whatever') + + assert str(exc.value) == "module 'os.path' has no attribute 'whatever'" + + def testOsPathExistsForever(self): + with pytest.raises(AttributeError) as exc: + get_obj('os.path.exists.forever') + + assert str(exc.value) == \ + "object 'os.path.exists' has no attribute 'forever'" + + def testOsPathExistsForeverAndEver(self): + with pytest.raises(AttributeError) as exc: + get_obj('os.path.exists.forever.and.ever') + + assert str(exc.value) == \ + "object 'os.path.exists' has no attribute 'forever'" + + def testUnknownMum(self): + with pytest.raises(ImportError) as exc: + assert get_obj('mum') is foo + if PY3: + assert str(exc.value) == "No module named 'mum'" + else: + assert str(exc.value) == "No module named mum" + + def testUnknownMumFoo(self): + with pytest.raises(ImportError) as exc: + assert get_obj('mum.foo') is foo + if PY3: + assert str(exc.value) == "No module named 'mum'" + else: + assert str(exc.value) == "No module named mum" + + def testReturnGivenObject(self): + import os + assert get_obj(os) == os + assert get_obj(os.path) == os.path + assert get_obj(2) == 2 + + def testDisallowRelativeImports(self): + with pytest.raises(TypeError): + get_obj('.mum') + +class TestReturnTuple: + def testOs(self): + with pytest.raises(TypeError): + get_obj_attr_tuple('os') + + def testOsPath(self): + import os + assert get_obj_attr_tuple('os.path') == (os, 'path') + + def testOsPathExists(self): + import os + assert get_obj_attr_tuple('os.path.exists') == (os.path, 'exists') + + def testOsPathExistsNot(self): + import os + assert get_obj_attr_tuple('os.path.exists.not') == ( + os.path.exists, 'not') + + def testDisallowRelativeImports(self): + with pytest.raises(TypeError): + get_obj('.mum') + + diff --git a/tests/matchers_test.py b/tests/matchers_test.py new file mode 100644 index 0000000..0ffcbc3 --- /dev/null +++ b/tests/matchers_test.py @@ -0,0 +1,240 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from .test_base import TestBase +from mockito import mock, verify +from mockito.matchers import and_, or_, not_, eq, neq, lt, lte, gt, gte, \ + any_, arg_that, contains, matches, captor, ANY, ARGS, KWARGS +import re + + +class TestConvenienceMatchers: + def testBuiltinAnyStandsForOurAny(self): + dummy = mock() + dummy.foo(1) + dummy.foo('a') + dummy.foo(True) + + verify(dummy, times=3).foo(any) + + dummy.foo(a=12) + verify(dummy).foo(a=any) + + def testOurAnyCanBeUsedAsAType(self): + dummy = mock() + dummy.foo(1) + dummy.foo('a') + dummy.foo(True) + verify(dummy, times=3).foo(any_) + + +class TestAliases: + def testANY(self): + dummy = mock() + dummy.foo(1) + verify(dummy).foo(ANY) + + def testARGS(self): + dummy = mock() + dummy.foo(1) + verify(dummy).foo(*ARGS) + + def testKWARGS(self): + dummy = mock() + dummy.foo(a=1) + verify(dummy).foo(**KWARGS) + + +class MatchersTest(TestBase): + def testVerifiesUsingContainsMatcher(self): + ourMock = mock() + ourMock.foo("foobar") + + verify(ourMock).foo(contains("foo")) + verify(ourMock).foo(contains("bar")) + + +class AndMatcherTest(TestBase): + def testShouldSatisfyIfAllMatchersAreSatisfied(self): + self.assertTrue( + and_(contains("foo"), contains("bar")).matches("foobar")) + + def testShouldNotSatisfyIfOneOfMatchersIsNotSatisfied(self): + self.assertFalse( + and_(contains("foo"), contains("bam")).matches("foobar")) + + def testShouldTreatNonMatchersAsEqMatcher(self): + self.assertTrue(and_("foo", any_(str)).matches("foo")) + self.assertFalse(and_("foo", any_(int)).matches("foo")) + + +class OrMatcherTest(TestBase): + def testShouldSatisfyIfAnyOfMatchersIsSatisfied(self): + self.assertTrue( + or_(contains("foo"), contains("bam")).matches("foobar")) + + def testShouldNotSatisfyIfAllOfMatchersAreNotSatisfied(self): + self.assertFalse( + or_(contains("bam"), contains("baz")).matches("foobar")) + + def testShouldTreatNonMatchersAsEqMatcher(self): + self.assertTrue(or_("foo", "bar").matches("foo")) + self.assertFalse(or_("foo", "bar").matches("bam")) + + +class NotMatcherTest(TestBase): + def testShouldSatisfyIfInnerMatcherIsNotSatisfied(self): + self.assertTrue(not_(contains("foo")).matches("bar")) + + def testShouldNotSatisfyIfInnerMatcherIsSatisfied(self): + self.assertFalse(not_(contains("foo")).matches("foo")) + + def testShouldTreatNonMatchersAsEqMatcher(self): + self.assertTrue(or_("foo", "bar").matches("foo")) + + +class EqMatcherTest(TestBase): + def testShouldSatisfyIfArgMatchesGivenValue(self): + self.assertTrue(eq("foo").matches("foo")) + + def testShouldNotSatisfyIfArgDoesNotMatchGivenValue(self): + self.assertFalse(eq("foo").matches("bar")) + + +class NeqMatcherTest(TestBase): + def testShouldSatisfyIfArgDoesNotMatchGivenValue(self): + self.assertTrue(neq("foo").matches("bar")) + + def testShouldNotSatisfyIfArgMatchesGivenValue(self): + self.assertFalse(neq("foo").matches("foo")) + + +class LtMatcherTest(TestBase): + def testShouldSatisfyIfArgIsLessThanGivenValue(self): + self.assertTrue(lt(5).matches(4)) + + def testShouldNotSatisfyIfArgIsEqualToGivenValue(self): + self.assertFalse(lt(5).matches(5)) + + def testShouldNotSatisfyIfArgIsGreaterThanGivenValue(self): + self.assertFalse(lt(5).matches(6)) + + +class LteMatcherTest(TestBase): + def testShouldSatisfyIfArgIsLessThanGivenValue(self): + self.assertTrue(lte(5).matches(4)) + + def testShouldSatisfyIfArgIsEqualToGivenValue(self): + self.assertTrue(lte(5).matches(5)) + + def testShouldNotSatisfyIfArgIsGreaterThanGivenValue(self): + self.assertFalse(lte(5).matches(6)) + + +class GtMatcherTest(TestBase): + def testShouldNotSatisfyIfArgIsLessThanGivenValue(self): + self.assertFalse(gt(5).matches(4)) + + def testShouldNotSatisfyIfArgIsEqualToGivenValue(self): + self.assertFalse(gt(5).matches(5)) + + def testShouldSatisfyIfArgIsGreaterThanGivenValue(self): + self.assertTrue(gt(5).matches(6)) + + +class GteMatcherTest(TestBase): + def testShouldNotSatisfyIfArgIsLessThanGivenValue(self): + self.assertFalse(gte(5).matches(4)) + + def testShouldSatisfyIfArgIsEqualToGivenValue(self): + self.assertTrue(gte(5).matches(5)) + + def testShouldSatisfyIfArgIsGreaterThanGivenValue(self): + self.assertTrue(gte(5).matches(6)) + + +class ArgThatMatcherTest(TestBase): + def testShouldSatisfyIfPredicateReturnsTrue(self): + self.assertTrue(arg_that(lambda arg: arg > 5).matches(10)) + + def testShouldNotSatisfyIfPredicateReturnsFalse(self): + self.assertFalse(arg_that(lambda arg: arg > 5).matches(1)) + + +class ContainsMatcherTest(TestBase): + def testShouldSatisfiySubstringOfGivenString(self): + self.assertTrue(contains("foo").matches("foobar")) + + def testShouldSatisfySameString(self): + self.assertTrue(contains("foobar").matches("foobar")) + + def testShouldNotSatisfiyStringWhichIsNotSubstringOfGivenString(self): + self.assertFalse(contains("barfoo").matches("foobar")) + + def testShouldNotSatisfiyEmptyString(self): + self.assertFalse(contains("").matches("foobar")) + + def testShouldNotSatisfiyNone(self): + self.assertFalse(contains(None).matches("foobar")) + + +class MatchesMatcherTest(TestBase): + def testShouldSatisfyIfRegexMatchesGivenString(self): + self.assertTrue(matches('f..').matches('foo')) + + def testShouldAllowSpecifyingRegexFlags(self): + self.assertFalse(matches('f..').matches('Foo')) + self.assertTrue(matches('f..', re.IGNORECASE).matches('Foo')) + + def testShouldNotSatisfyIfRegexIsNotMatchedByGivenString(self): + self.assertFalse(matches('f..').matches('bar')) + + +class ArgumentCaptorTest(TestBase): + def testShouldSatisfyIfInnerMatcherIsSatisfied(self): + c = captor(contains("foo")) + self.assertTrue(c.matches("foobar")) + + def testShouldNotSatisfyIfInnerMatcherIsNotSatisfied(self): + c = captor(contains("foo")) + self.assertFalse(c.matches("barbam")) + + def testShouldReturnNoneValueByDefault(self): + c = captor(contains("foo")) + self.assertEqual(None, c.value) + + def testShouldReturnNoneValueIfDidntMatch(self): + c = captor(contains("foo")) + c.matches("bar") + self.assertEqual(None, c.value) + + def testShouldReturnLastMatchedValue(self): + c = captor(contains("foo")) + c.matches("foobar") + c.matches("foobam") + c.matches("bambaz") + self.assertEqual("foobam", c.value) + + def testShouldDefaultMatcherToAny(self): + c = captor() + c.matches("foo") + c.matches(123) + self.assertEqual(123, c.value) + diff --git a/tests/mocking_properties_test.py b/tests/mocking_properties_test.py new file mode 100644 index 0000000..3411bba --- /dev/null +++ b/tests/mocking_properties_test.py @@ -0,0 +1,49 @@ +import pytest +from mockito import mock, when + +def test_deprecated_a(unstub): + # Setting on `__class__` is confusing for users + m = mock() + + prop = mock() + when(prop).__get__(Ellipsis).thenRaise(ValueError) + m.__class__.tx = prop + + with pytest.raises(ValueError): + m.tx + + +def test_deprecated_b(unstub): + # Setting on `__class__` is confusing for users + m = mock() + + def _raise(*a): + print(a) + raise ValueError('Boom') + + m.__class__.tx = property(_raise) + + with pytest.raises(ValueError): + m.tx + + +def test_deprecated_c(unstub): + # Setting on `__class__` is confusing for users + # Wrapping explicitly with `property` as well + m = mock() + + prop = mock(strict=True) + when(prop).__call__(Ellipsis).thenRaise(ValueError) + m.__class__.tx = property(prop) + + with pytest.raises(ValueError): + m.tx + + +def test_recommended_approach(): + prop = mock(strict=True) + when(prop).__get__(Ellipsis).thenRaise(ValueError) + + m = mock({'tx': prop}) + with pytest.raises(ValueError): + m.tx diff --git a/tests/module.py b/tests/module.py new file mode 100644 index 0000000..1d11c56 --- /dev/null +++ b/tests/module.py @@ -0,0 +1,8 @@ + +class Foo(object): + def no_arg(self): + pass + + +def one_arg(arg): + return arg diff --git a/tests/modulefunctions_test.py b/tests/modulefunctions_test.py new file mode 100644 index 0000000..6d8728f --- /dev/null +++ b/tests/modulefunctions_test.py @@ -0,0 +1,100 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os + +from .test_base import TestBase +from mockito import when, unstub, verify, any +from mockito.invocation import InvocationError +from mockito.verification import VerificationError + + +class ModuleFunctionsTest(TestBase): + def tearDown(self): + unstub() + + def testUnstubs(self): + when(os.path).exists("test").thenReturn(True) + unstub() + self.assertEqual(False, os.path.exists("test")) + + def testStubs(self): + when(os.path).exists("test").thenReturn(True) + + self.assertEqual(True, os.path.exists("test")) + + def testStubsConsecutiveCalls(self): + when(os.path).exists("test").thenReturn(False).thenReturn(True) + + self.assertEqual(False, os.path.exists("test")) + self.assertEqual(True, os.path.exists("test")) + + def testStubsMultipleClasses(self): + when(os.path).exists("test").thenReturn(True) + when(os.path).dirname(any(str)).thenReturn("mocked") + + self.assertEqual(True, os.path.exists("test")) + self.assertEqual("mocked", os.path.dirname("whoah!")) + + def testVerifiesSuccesfully(self): + when(os.path).exists("test").thenReturn(True) + + os.path.exists("test") + + verify(os.path).exists("test") + + def testFailsVerification(self): + when(os.path).exists("test").thenReturn(True) + + self.assertRaises(VerificationError, verify(os.path).exists, "test") + + def testFailsOnNumberOfCalls(self): + when(os.path).exists("test").thenReturn(True) + + os.path.exists("test") + + self.assertRaises(VerificationError, verify(os.path, times=2).exists, + "test") + + def testStubsTwiceAndUnstubs(self): + when(os.path).exists("test").thenReturn(False) + when(os.path).exists("test").thenReturn(True) + + self.assertEqual(True, os.path.exists("test")) + + unstub() + + self.assertEqual(False, os.path.exists("test")) + + def testStubsTwiceWithDifferentArguments(self): + when(os.path).exists("Foo").thenReturn(False) + when(os.path).exists("Bar").thenReturn(True) + + self.assertEqual(False, os.path.exists("Foo")) + self.assertEqual(True, os.path.exists("Bar")) + + def testShouldThrowIfWeStubAFunctionNotDefinedInTheModule(self): + self.assertRaises(InvocationError, + lambda: when(os).walk_the_line().thenReturn(None)) + + def testEnsureWeCanMockTheClassOnAModule(self): + from . import module + when(module).Foo().thenReturn('mocked') + assert module.Foo() == 'mocked' diff --git a/tests/my_dict_test.py b/tests/my_dict_test.py new file mode 100644 index 0000000..bfab349 --- /dev/null +++ b/tests/my_dict_test.py @@ -0,0 +1,62 @@ + +import pytest + +from mockito.mock_registry import _Dict +from mockito.mocking import Mock + + +class TestCustomDictLike: + + def testAssignKeyValuePair(self): + td = _Dict() + obj = {} + mock = Mock(None) + + td[obj] = mock + + def testGetValueForKey(self): + td = _Dict() + obj = {} + mock = Mock(None) + td[obj] = mock + + assert td.get(obj) == mock + + def testReplaceValueForSameKey(self): + td = _Dict() + obj = {} + mock1 = Mock(None) + mock2 = Mock(None) + td[obj] = mock1 + td[obj] = mock2 + + assert td.pop(obj) == mock2 + with pytest.raises(KeyError): + td.pop(obj) + + def testPopKey(self): + td = _Dict() + obj = {} + mock = Mock(None) + td[obj] = mock + + assert td.pop(obj) == mock + assert td.get(obj) is None + + def testIterValues(self): + td = _Dict() + obj = {} + mock = Mock(None) + td[obj] = mock + + assert td.values() == [mock] + + def testClear(self): + td = _Dict() + obj = {} + mock = Mock(None) + td[obj] = mock + + td.clear() + assert td.get(obj) is None + diff --git a/tests/numpy_test.py b/tests/numpy_test.py new file mode 100644 index 0000000..b88c28e --- /dev/null +++ b/tests/numpy_test.py @@ -0,0 +1,31 @@ +import mockito +from mockito import when, patch +import pytest + +import numpy as np +from . import module + + +pytestmark = pytest.mark.usefixtures("unstub") + + +def xcompare(a, b): + if isinstance(a, mockito.matchers.Matcher): + return a.matches(b) + + return np.array_equal(a, b) + + +class TestEnsureNumpyWorks: + def testEnsureNumpyArrayAllowedWhenStubbing(self): + array = np.array([1, 2, 3]) + when(module).one_arg(array).thenReturn('yep') + + with patch(mockito.invocation.MatchingInvocation.compare, xcompare): + assert module.one_arg(array) == 'yep' + + def testEnsureNumpyArrayAllowedWhenCalling(self): + array = np.array([1, 2, 3]) + when(module).one_arg(Ellipsis).thenReturn('yep') + assert module.one_arg(array) == 'yep' + diff --git a/tests/signatures_test.py b/tests/signatures_test.py new file mode 100644 index 0000000..d7c93c6 --- /dev/null +++ b/tests/signatures_test.py @@ -0,0 +1,459 @@ + +import pytest + +from mockito import when, args, kwargs, unstub + +from collections import namedtuple + + +class CallSignature(namedtuple('CallSignature', 'args kwargs')): + def raises(self, reason): + return pytest.mark.xfail(self, raises=reason, strict=True) + +def sig(*a, **kw): + return CallSignature(a, kw) + + +class SUT(object): + def none_args(self): + pass + + def one_arg(self, a): + pass + + def two_args(self, a, b): + pass + + def star_arg(self, *args): + pass + + def star_kwarg(self, **kwargs): + pass + + def arg_plus_star_arg(self, a, *b): + pass + + def arg_plus_star_kwarg(self, a, **b): + pass + + def two_args_wt_default(self, a, b=None): + pass + + def combination(self, a, b=None, *c, **d): + pass + + +class ClassMethods(object): + @classmethod + def none_args(cls): + pass + + @classmethod + def one_arg(cls, a): + pass + + @classmethod + def two_args(cls, a, b): + pass + + @classmethod + def star_arg(cls, *a): + pass + + @classmethod + def star_kwarg(cls, **kw): + pass + + @classmethod + def arg_plus_star_arg(cls, a, *b): + pass + + @classmethod + def arg_plus_star_kwarg(cls, a, **b): + pass + + @classmethod + def two_args_wt_default(cls, a, b=None): + pass + + @classmethod + def combination(cls, a, b=None, *c, **d): + pass + + +class StaticMethods(object): + @staticmethod + def none_args(): + pass + + @staticmethod + def one_arg(a): + pass + + @staticmethod + def two_args(a, b): + pass + + @staticmethod + def star_arg(*a): + pass + + @staticmethod + def star_kwarg(**kw): + pass + + @staticmethod + def arg_plus_star_arg(a, *b): + pass + + @staticmethod + def arg_plus_star_kwarg(a, **b): + pass + + @staticmethod + def two_args_wt_default(a, b=None): + pass + + @staticmethod + def combination(a, b=None, *c, **d): + pass + + +@pytest.fixture(params=[ + 'instance', + 'class', + 'classmethods', + 'staticmethods', + 'staticmethods_2', +]) +def sut(request): + if request.param == 'instance': + yield SUT() + elif request.param == 'class': + yield SUT + elif request.param == 'classmethods': + yield ClassMethods + elif request.param == 'staticmethods': + yield StaticMethods + elif request.param == 'staticmethods_2': + yield StaticMethods() + + unstub() + + +class TestSignatures: + + class TestNoneArg: + + @pytest.mark.parametrize('call', [ + sig(), + sig(Ellipsis), + ]) + def test_passing(self, sut, call): + when(sut).none_args(*call.args, **call.kwargs).thenReturn('stub') + + + @pytest.mark.parametrize('call', [ + sig(12), + sig(*args), + sig(**kwargs), + sig(*args, **kwargs) + ]) + def test_failing(self, sut, call): + with pytest.raises(TypeError): + when(sut).none_args(*call.args, **call.kwargs) + + + class TestOneArg: + + @pytest.mark.parametrize('call', [ + sig(12), + sig(a=12), + + sig(Ellipsis), + + sig(*args), + sig(*args, **kwargs), + sig(**kwargs), + ]) + def test_passing(self, sut, call): + when(sut).one_arg(*call.args, **call.kwargs).thenReturn('stub') + + @pytest.mark.parametrize('call', [ + sig(12, 13), + sig(12, b=2), + sig(12, 13, 14), + sig(b=2), + sig(12, c=2), + sig(12, b=2, c=2), + + sig(12, Ellipsis), + + sig(1, *args), + sig(*args, a=1), + sig(*args, b=1), + sig(1, **kwargs), + ]) + def test_failing(self, sut, call): + with pytest.raises(TypeError): + when(sut).one_arg(*call.args, **call.kwargs) + + + class TestTwoArgs: + + # def two_args(a, b) + @pytest.mark.parametrize('call', [ + sig(12, 13), + sig(12, b=2), + + sig(Ellipsis), + sig(12, Ellipsis), + + sig(*args), + sig(*args, **kwargs), + sig(12, *args), + sig(**kwargs), + sig(12, **kwargs), + sig(b=13, **kwargs), + + ]) + def test_passing(self, sut, call): + when(sut).two_args(*call.args, **call.kwargs) + + @pytest.mark.parametrize('call', [ + sig(12), + sig(12, 13, 14), + sig(b=2), + sig(12, c=2), + sig(12, b=2, c=2), + + sig(12, 13, Ellipsis), + sig(12, 13, *args), + sig(12, b=13, *args), + sig(12, 13, **kwargs), + sig(12, b=13, **kwargs), + ]) + def test_failing(self, sut, call): + with pytest.raises(TypeError): + when(sut).two_args(*call.args, **call.kwargs) + + class TestStarArg: + # def star_arg(*args) + @pytest.mark.parametrize('call', [ + sig(), + sig(12), + sig(12, 13), + + sig(Ellipsis), + sig(12, Ellipsis), + sig(12, 13, Ellipsis), + + sig(*args), + sig(12, *args), + sig(12, 13, *args) + ], ids=lambda i: str(i)) + def test_passing(self, sut, call): + when(sut).star_arg(*call.args, **call.kwargs) + + @pytest.mark.parametrize('call', [ + sig(**kwargs), + sig(12, **kwargs), + sig(Ellipsis, **kwargs), + sig(a=12), + sig(args=12) + ], ids=lambda i: str(i)) + def test_failing(self, sut, call): + with pytest.raises(TypeError): + when(sut).star_arg(*call.args, **call.kwargs) + + + class TestStarKwarg: + # def star_kwarg(**kwargs) + @pytest.mark.parametrize('call', [ + sig(), + sig(a=1), + sig(a=1, b=2), + + sig(Ellipsis), + + sig(**kwargs), + sig(a=1, **kwargs) + + ], ids=lambda i: str(i)) + def test_passing(self, sut, call): + when(sut).star_kwarg(*call.args, **call.kwargs) + + @pytest.mark.parametrize('call', [ + sig(12), + sig(*args), + sig(*args, **kwargs), + sig(12, a=1) + ], ids=lambda i: str(i)) + def test_failing(self, sut, call): + with pytest.raises(TypeError): + when(sut).star_kwarg(*call.args, **call.kwargs) + + + class TestArgPlusStarArg: + # def arg_plus_star_arg(a, *args) + @pytest.mark.parametrize('call', [ + sig(12), + sig(a=12), + sig(12, 13), + + sig(Ellipsis), + sig(12, Ellipsis), + sig(12, 13, Ellipsis), + + sig(*args), + sig(12, *args), + + sig(**kwargs), + ], ids=lambda i: str(i)) + def test_passing(self, sut, call): + when(sut).arg_plus_star_arg(*call.args, **call.kwargs) + + @pytest.mark.parametrize('call', [ + sig(), + sig(13, a=12), + + sig(b=13), + sig(12, b=13, *args), + sig(a=12, b=13, *args), + + sig(12, **kwargs), + sig(a=12, **kwargs), + ], ids=lambda i: str(i)) + def test_failing(self, sut, call): + with pytest.raises(TypeError): + when(sut).arg_plus_star_arg(*call.args, **call.kwargs) + + + class TestArgPlusStarKwarg: + # def arg_plus_star_kwarg(a, **kwargs) + @pytest.mark.parametrize('call', [ + sig(12), + sig(a=12), + sig(12, b=1), + + sig(Ellipsis), + sig(12, Ellipsis), + + sig(**kwargs), + sig(12, **kwargs), + sig(a=12, **kwargs), + sig(12, b=1, **kwargs), + sig(a=12, b=1, **kwargs), + + sig(*args), + sig(*args, b=1), + + sig(*args, **kwargs) + ], ids=lambda i: str(i)) + def test_passing(self, sut, call): + when(sut).arg_plus_star_kwarg(*call.args, **call.kwargs) + + @pytest.mark.parametrize('call', [ + sig(), + sig(12, 13), + sig(b=1), + sig(12, a=1), + sig(12, 13, Ellipsis), + sig(*args, a=1), + sig(12, *args) + ], ids=lambda i: str(i)) + def test_failing(self, sut, call): + with pytest.raises(TypeError): + when(sut).arg_plus_star_kwarg(*call.args, **call.kwargs) + + + + + class TestTwoArgsWtDefault: + + @pytest.mark.parametrize('call', [ + sig(12), + sig(12, 13), + sig(12, b=2), + + sig(Ellipsis), + sig(12, Ellipsis), + + sig(*args), + sig(*args, **kwargs), + sig(12, *args), + sig(*args, b=2), + sig(**kwargs), + sig(12, **kwargs), + ], ids=lambda i: str(i)) + def test_passing(self, sut, call): + when(sut).two_args_wt_default( + *call.args, **call.kwargs).thenReturn() + + + @pytest.mark.parametrize('call', [ + sig(12, 13, 14), + sig(b=2), + sig(12, c=2), + sig(12, b=2, c=2), + + sig(12, 13, Ellipsis), + + sig(12, 13, *args), + sig(12, b=13, *args), + sig(12, c=13, *args), + sig(12, *args, b=2), + sig(*args, a=2), + sig(*args, c=2), + sig(12, 13, **kwargs), + sig(12, b=13, **kwargs), + sig(12, c=13, **kwargs), + ], ids=lambda i: str(i)) + def test_failing(self, sut, call): + with pytest.raises(TypeError): + when(sut).two_args_wt_default( + *call.args, **call.kwargs).thenReturn() + + class TestCombination: + + # def combination(self, a, b=None, *c, **d) + @pytest.mark.parametrize('call', [ + sig(12), + sig(12, 13), + sig(12, 13, 14), + sig(12, 13, 14, 15), + + sig(Ellipsis), + sig(12, Ellipsis), + sig(12, 13, Ellipsis), + sig(12, 13, 14, Ellipsis), + sig(12, 13, 14, 15, Ellipsis) + + ], ids=lambda i: str(i)) + def test_passing(self, sut, call): + when(sut).combination( + *call.args, **call.kwargs).thenReturn() + + + @pytest.mark.parametrize('call', [ + sig(12, 13, b=16), + ], ids=lambda i: str(i)) + def test_failing(self, sut, call): + with pytest.raises(TypeError): + when(sut).combination( + *call.args, **call.kwargs).thenReturn() + + + class TestBuiltin: + + def testBuiltinOpen(self): + try: + import builtins + except ImportError: + import __builtin__ as builtins + + try: + when(builtins).open('foo') + finally: # just to be sure + unstub() + diff --git a/tests/speccing_test.py b/tests/speccing_test.py new file mode 100644 index 0000000..0ef5e70 --- /dev/null +++ b/tests/speccing_test.py @@ -0,0 +1,116 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +from mockito.invocation import InvocationError +from mockito import mock, when, verify + + +class Foo(object): + def bar(self): + pass + +class Action(object): + def no_arg(self): + pass + + def run(self, arg): + return arg + + def __call__(self, task): + return task + + +class TestSpeccing: + def testStubCallAndVerify(self): + action = mock(Action) + + when(action).run(11).thenReturn(12) + assert action.run(11) == 12 + verify(action).run(11) + + + def testShouldScreamWhenStubbingUnknownMethod(self): + action = mock(Action) + + with pytest.raises(InvocationError): + when(action).unknownMethod() + + def testShouldScreamWhenCallingUnknownMethod(self): + action = mock(Action) + + with pytest.raises(AttributeError): + action.unknownMethod() + + def testShouldScreamWhenCallingUnexpectedMethod(self): + action = mock(Action) + + with pytest.raises(AttributeError): + action.run(11) + + def testPreconfigureMockWithAttributes(self): + action = mock({'foo': 'bar'}, spec=Action) + + assert action.foo == 'bar' + with pytest.raises(InvocationError): + when(action).remember() + + def testPreconfigureWithFunction(self): + action = mock({ + 'run': lambda _: 12 + }, spec=Action) + + assert action.run(11) == 12 + + verify(action).run(11) + + def testPreconfigureWithFunctionThatTakesNoArgs(self): + action = mock({ + 'no_arg': lambda: 12 + }, spec=Action) + + assert action.no_arg() == 12 + + verify(action).no_arg() + + def testShouldScreamOnUnknownAttribute(self): + action = mock(Action) + + with pytest.raises(AttributeError): + action.cam + + def testShouldPassIsInstanceChecks(self): + action = mock(Action) + + assert isinstance(action, Action) + + def testHasANiceName(self): + action = mock(Action) + + assert repr(action) == "" % id(action) + + +class TestSpeccingLoose: + def testReturnNoneForEveryMethod(self): + action = mock(Action, strict=False) + assert action.unknownMethod() is None + assert action.run(11) is None + diff --git a/tests/spying_test.py b/tests/spying_test.py new file mode 100644 index 0000000..f2fa87d --- /dev/null +++ b/tests/spying_test.py @@ -0,0 +1,156 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import sys + +from .test_base import TestBase +from mockito import ( + when, spy, spy2, verify, VerificationError, verifyZeroInteractions) + +import time + +class Dummy(object): + def foo(self): + return "foo" + + def bar(self): + raise TypeError + + def return_args(self, *args, **kwargs): + return (args, kwargs) + + @classmethod + def class_method(cls, arg): + return arg + + +class SpyingTest(TestBase): + def testPreservesReturnValues(self): + dummy = Dummy() + spiedDummy = spy(dummy) + self.assertEqual(dummy.foo(), spiedDummy.foo()) + + def testPreservesSideEffects(self): + dummy = spy(Dummy()) + self.assertRaises(TypeError, dummy.bar) + + def testPassesArgumentsCorrectly(self): + dummy = spy(Dummy()) + self.assertEqual((('foo', 1), {'bar': 'baz'}), + dummy.return_args('foo', 1, bar='baz')) + + def testIsVerifiable(self): + dummy = spy(Dummy()) + dummy.foo() + verify(dummy).foo() + self.assertRaises(VerificationError, verify(dummy).bar) + + def testVerifyZeroInteractionsWorks(self): + dummy = spy(Dummy()) + verifyZeroInteractions(dummy) + + def testRaisesAttributeErrorIfNoSuchMethod(self): + original = Dummy() + dummy = spy(original) + try: + dummy.lol() + self.fail("Should fail if no such method.") + except AttributeError as e: + self.assertEqual("You tried to call method 'lol' which '%s' " + "instance does not have." % original, str(e)) + + def testIsInstanceFakesOriginalClass(self): + dummy = spy(Dummy()) + + assert isinstance(dummy, Dummy) + + def testHasNiceRepr(self): + dummy = spy(Dummy()) + + assert repr(dummy) == "" % id(dummy) + + + + def testCallClassmethod(self): + dummy = spy(Dummy) + + assert dummy.class_method('foo') == 'foo' + verify(dummy).class_method('foo') + + + @pytest.mark.xfail( + sys.version_info >= (3,), + reason="python3 allows any value for self" + ) + def testCantCallInstanceMethodWhenSpyingClass(self): + dummy = spy(Dummy) + with pytest.raises(TypeError): + dummy.return_args('foo') + + + def testModuleFunction(self): + import time + dummy = spy(time) + + assert dummy.time() is not None + + verify(dummy).time() + + +class TestSpy2: + + def testA(self): + dummy = Dummy() + spy2(dummy.foo) + + assert dummy.foo() == 'foo' + verify(dummy).foo() + + def testB(self): + spy2(Dummy.class_method) + + assert Dummy.class_method('foo') == 'foo' + verify(Dummy).class_method('foo') + + def testModule(self): + spy2(time.time) + + assert time.time() is not None + verify(time).time() + + def testEnsureStubbedResponseForSpecificInvocation(self): + dummy = Dummy() + spy2(dummy.return_args) + when(dummy).return_args('foo').thenReturn('fox') + + assert dummy.return_args('bar') == (('bar',), {}) + assert dummy.return_args('box') == (('box',), {}) + assert dummy.return_args('foo') == 'fox' + + def testEnsureStubOrder(self): + dummy = Dummy() + when(dummy).return_args(Ellipsis).thenReturn('foo') + when(dummy).return_args('fox').thenReturn('fix') + + assert dummy.return_args('bar') == 'foo' + assert dummy.return_args('box') == 'foo' + assert dummy.return_args('fox') == 'fix' + diff --git a/tests/staticmethods_test.py b/tests/staticmethods_test.py new file mode 100644 index 0000000..ee9a0f5 --- /dev/null +++ b/tests/staticmethods_test.py @@ -0,0 +1,156 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from .test_base import TestBase +from mockito import when, verify, unstub, any +from mockito.verification import VerificationError + +class Dog: + @staticmethod + def bark(): + return "woof" + + @staticmethod + def barkHardly(*args): + return "woof woof" + + +class Cat: + @staticmethod + def meow(): + return "miau" + + +class StaticMethodsTest(TestBase): + + def tearDown(self): + unstub() + + def testUnstubs(self): + when(Dog).bark().thenReturn("miau") + unstub() + self.assertEqual("woof", Dog.bark()) + + # TODO decent test case please :) without testing irrelevant implementation + # details + def testUnstubShouldPreserveMethodType(self): + when(Dog).bark().thenReturn("miau!") + unstub() + self.assertTrue(isinstance(Dog.__dict__.get("bark"), staticmethod)) + + def testStubs(self): + self.assertEqual("woof", Dog.bark()) + + when(Dog).bark().thenReturn("miau") + + self.assertEqual("miau", Dog.bark()) + + def testStubsConsecutiveCalls(self): + when(Dog).bark().thenReturn(1).thenReturn(2) + + self.assertEqual(1, Dog.bark()) + self.assertEqual(2, Dog.bark()) + self.assertEqual(2, Dog.bark()) + + def testStubsWithArgs(self): + self.assertEqual("woof woof", Dog.barkHardly(1, 2)) + + when(Dog).barkHardly(1, 2).thenReturn("miau") + + self.assertEqual("miau", Dog.barkHardly(1, 2)) + + def testStubsButDoesNotMachArguments(self): + self.assertEqual("woof woof", Dog.barkHardly(1, "anything")) + + when(Dog, strict=False).barkHardly(1, 2).thenReturn("miau") + + self.assertEqual(None, Dog.barkHardly(1)) + + def testStubsMultipleClasses(self): + when(Dog).barkHardly(1, 2).thenReturn(1) + when(Dog).bark().thenReturn(2) + when(Cat).meow().thenReturn(3) + + self.assertEqual(1, Dog.barkHardly(1, 2)) + self.assertEqual(2, Dog.bark()) + self.assertEqual(3, Cat.meow()) + + unstub() + + self.assertEqual("woof", Dog.bark()) + self.assertEqual("miau", Cat.meow()) + + def testVerifiesSuccesfully(self): + when(Dog).bark().thenReturn("boo") + + Dog.bark() + + verify(Dog).bark() + + def testVerifiesWithArguments(self): + when(Dog).barkHardly(1, 2).thenReturn("boo") + + Dog.barkHardly(1, 2) + + verify(Dog).barkHardly(1, any()) + + def testFailsVerification(self): + when(Dog).bark().thenReturn("boo") + + Dog.bark() + + self.assertRaises(VerificationError, verify(Dog).barkHardly, (1, 2)) + + def testFailsOnInvalidArguments(self): + when(Dog).bark().thenReturn("boo") + + Dog.barkHardly(1, 2) + + self.assertRaises(VerificationError, verify(Dog).barkHardly, (1, 20)) + + def testFailsOnNumberOfCalls(self): + when(Dog).bark().thenReturn("boo") + + Dog.bark() + + self.assertRaises(VerificationError, verify(Dog, times=2).bark) + + def testStubsAndVerifies(self): + when(Dog).bark().thenReturn("boo") + + self.assertEqual("boo", Dog.bark()) + + verify(Dog).bark() + + def testStubsTwiceAndUnstubs(self): + when(Dog).bark().thenReturn(1) + when(Dog).bark().thenReturn(2) + + self.assertEqual(2, Dog.bark()) + + unstub() + + self.assertEqual("woof", Dog.bark()) + + def testDoesNotVerifyStubbedCalls(self): + when(Dog).bark().thenReturn(1) + + verify(Dog, times=0).bark() + diff --git a/tests/stubbing_test.py b/tests/stubbing_test.py new file mode 100644 index 0000000..6c520d3 --- /dev/null +++ b/tests/stubbing_test.py @@ -0,0 +1,401 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +from .test_base import TestBase +from mockito import mock, when, verify, times, any + + +class TestEmptyMocks: + def testAllMethodsReturnNone(self): + dummy = mock() + + assert dummy.foo() is None + assert dummy.foo(1, 2) is None + + + def testConfigureDummy(self): + dummy = mock({'foo': 'bar'}) + assert dummy.foo == 'bar' + + def testConfigureDummyWithFunction(self): + dummy = mock({ + 'getStuff': lambda s: s + ' thing' + }) + + assert dummy.getStuff('da') == 'da thing' + verify(dummy).getStuff('da') + + def testDummiesAreCallable(self): + dummy = mock() + assert dummy() is None + assert dummy(1, 2) is None + + def testCallsAreVerifiable(self): + dummy = mock() + dummy(1, 2) + + verify(dummy).__call__(1, 2) + + def testConfigureCallBehavior(self): + dummy = mock() + when(dummy).__call__(1).thenReturn(2) + + assert dummy(1) == 2 + verify(dummy).__call__(1) + + def testCheckIsInstanceAgainstItself(self): + dummy = mock() + assert isinstance(dummy, dummy.__class__) + + + def testConfigureMagicMethod(self): + dummy = mock() + when(dummy).__getitem__(1).thenReturn(2) + + assert dummy[1] == 2 + +class TestStrictEmptyMocks: + def testScream(self): + dummy = mock(strict=True) + + with pytest.raises(AttributeError): + dummy.foo() + + def testAllowStubbing(self): + dummy = mock(strict=True) + when(dummy).foo() + dummy.foo() + verify(dummy).foo() + + def testCanConfigureCall(self): + dummy = mock(strict=True) + when(dummy).__call__(1).thenReturn(2) + + assert dummy(1) == 2 + + def testScreamOnUnconfiguredCall(self): + dummy = mock(strict=True) + + with pytest.raises(AttributeError): + dummy(1) + + def testConfigureMagicMethod(self): + dummy = mock(strict=True) + when(dummy).__getitem__(1).thenReturn(2) + + assert dummy[1] == 2 + + +class StubbingTest(TestBase): + def testStubsWithReturnValue(self): + theMock = mock() + when(theMock).getStuff().thenReturn("foo") + when(theMock).getMoreStuff(1, 2).thenReturn(10) + + self.assertEqual("foo", theMock.getStuff()) + self.assertEqual(10, theMock.getMoreStuff(1, 2)) + self.assertEqual(None, theMock.getMoreStuff(1, 3)) + + def testStubsWhenNoArgsGiven(self): + theMock = mock() + when(theMock).getStuff().thenReturn("foo") + when(theMock).getWidget().thenReturn("bar") + + self.assertEqual("foo", theMock.getStuff()) + self.assertEqual("bar", theMock.getWidget()) + + def testStubsConsecutivelyWhenNoArgsGiven(self): + theMock = mock() + when(theMock).getStuff().thenReturn("foo").thenReturn("bar") + when(theMock).getWidget().thenReturn("baz").thenReturn("baz2") + + self.assertEqual("foo", theMock.getStuff()) + self.assertEqual("bar", theMock.getStuff()) + self.assertEqual("bar", theMock.getStuff()) + self.assertEqual("baz", theMock.getWidget()) + self.assertEqual("baz2", theMock.getWidget()) + self.assertEqual("baz2", theMock.getWidget()) + + def testStubsWithException(self): + theMock = mock() + when(theMock).someMethod().thenRaise(Exception("foo")) + + self.assertRaisesMessage("foo", theMock.someMethod) + + def testStubsAndVerifies(self): + theMock = mock() + when(theMock).foo().thenReturn("foo") + + self.assertEqual("foo", theMock.foo()) + verify(theMock).foo() + + def testStubsVerifiesAndStubsAgain(self): + theMock = mock() + + when(theMock).foo().thenReturn("foo") + self.assertEqual("foo", theMock.foo()) + verify(theMock).foo() + + when(theMock).foo().thenReturn("next foo") + self.assertEqual("next foo", theMock.foo()) + verify(theMock, times(2)).foo() + + def testOverridesStubbing(self): + theMock = mock() + + when(theMock).foo().thenReturn("foo") + when(theMock).foo().thenReturn("bar") + + self.assertEqual("bar", theMock.foo()) + + def testStubsAndInvokesTwiceAndVerifies(self): + theMock = mock() + + when(theMock).foo().thenReturn("foo") + + self.assertEqual("foo", theMock.foo()) + self.assertEqual("foo", theMock.foo()) + + verify(theMock, times(2)).foo() + + def testStubsAndReturnValuesForSameMethodWithDifferentArguments(self): + theMock = mock() + when(theMock).getStuff(1).thenReturn("foo") + when(theMock).getStuff(1, 2).thenReturn("bar") + + self.assertEqual("foo", theMock.getStuff(1)) + self.assertEqual("bar", theMock.getStuff(1, 2)) + + def testStubsAndReturnValuesForSameMethodWithDifferentNamedArguments(self): + repo = mock() + when(repo).findby(id=6).thenReturn("John May") + when(repo).findby(name="John").thenReturn(["John May", "John Smith"]) + + self.assertEqual("John May", repo.findby(id=6)) + self.assertEqual(["John May", "John Smith"], repo.findby(name="John")) + + def testStubsForMethodWithSameNameAndNamedArgumentsInArbitraryOrder(self): + theMock = mock() + + when(theMock).foo(first=1, second=2, third=3).thenReturn(True) + + self.assertEqual(True, theMock.foo(third=3, first=1, second=2)) + + def testStubsMethodWithSameNameAndMixedArguments(self): + repo = mock() + when(repo).findby(1).thenReturn("John May") + when(repo).findby(1, active_only=True).thenReturn(None) + when(repo).findby(name="Sarah").thenReturn(["Sarah Connor"]) + when(repo).findby(name="Sarah", active_only=True).thenReturn([]) + + self.assertEqual("John May", repo.findby(1)) + self.assertEqual(None, repo.findby(1, active_only=True)) + self.assertEqual(["Sarah Connor"], repo.findby(name="Sarah")) + self.assertEqual([], repo.findby(name="Sarah", active_only=True)) + + def testStubsWithChainedReturnValues(self): + theMock = mock() + when(theMock).getStuff().thenReturn("foo") \ + .thenReturn("bar") \ + .thenReturn("foobar") + + self.assertEqual("foo", theMock.getStuff()) + self.assertEqual("bar", theMock.getStuff()) + self.assertEqual("foobar", theMock.getStuff()) + + def testStubsWithChainedReturnValuesAndException(self): + theMock = mock() + when(theMock).getStuff().thenReturn("foo") \ + .thenReturn("bar") \ + .thenRaise(Exception("foobar")) + + self.assertEqual("foo", theMock.getStuff()) + self.assertEqual("bar", theMock.getStuff()) + self.assertRaisesMessage("foobar", theMock.getStuff) + + def testStubsWithChainedExceptionAndReturnValue(self): + theMock = mock() + when(theMock).getStuff().thenRaise(Exception("foo")) \ + .thenReturn("bar") + + self.assertRaisesMessage("foo", theMock.getStuff) + self.assertEqual("bar", theMock.getStuff()) + + def testStubsWithChainedExceptions(self): + theMock = mock() + when(theMock).getStuff().thenRaise(Exception("foo")) \ + .thenRaise(Exception("bar")) + + self.assertRaisesMessage("foo", theMock.getStuff) + self.assertRaisesMessage("bar", theMock.getStuff) + + def testStubsWithReturnValueBeingException(self): + theMock = mock() + exception = Exception("foo") + when(theMock).getStuff().thenReturn(exception) + + self.assertEqual(exception, theMock.getStuff()) + + def testLastStubbingWins(self): + theMock = mock() + when(theMock).foo().thenReturn(1) + when(theMock).foo().thenReturn(2) + + self.assertEqual(2, theMock.foo()) + + def testStubbingOverrides(self): + theMock = mock() + when(theMock).foo().thenReturn(1) + when(theMock).foo().thenReturn(2).thenReturn(3) + + self.assertEqual(2, theMock.foo()) + self.assertEqual(3, theMock.foo()) + self.assertEqual(3, theMock.foo()) + + def testStubsWithMatchers(self): + theMock = mock() + when(theMock).foo(any()).thenReturn(1) + + self.assertEqual(1, theMock.foo(1)) + self.assertEqual(1, theMock.foo(100)) + + def testStubbingOverrides2(self): + theMock = mock() + when(theMock).foo(any()).thenReturn(1) + when(theMock).foo("oh").thenReturn(2) + + self.assertEqual(2, theMock.foo("oh")) + self.assertEqual(1, theMock.foo("xxx")) + + def testDoesNotVerifyStubbedCalls(self): + theMock = mock() + when(theMock).foo().thenReturn(1) + + verify(theMock, times=0).foo() + + def testStubsWithMultipleReturnValues(self): + theMock = mock() + when(theMock).getStuff().thenReturn("foo", "bar", "foobar") + + self.assertEqual("foo", theMock.getStuff()) + self.assertEqual("bar", theMock.getStuff()) + self.assertEqual("foobar", theMock.getStuff()) + + def testStubsWithChainedMultipleReturnValues(self): + theMock = mock() + when(theMock).getStuff().thenReturn("foo", "bar") \ + .thenReturn("foobar") + + self.assertEqual("foo", theMock.getStuff()) + self.assertEqual("bar", theMock.getStuff()) + self.assertEqual("foobar", theMock.getStuff()) + + def testStubsWithMultipleExceptions(self): + theMock = mock() + when(theMock).getStuff().thenRaise(Exception("foo"), Exception("bar")) + + self.assertRaisesMessage("foo", theMock.getStuff) + self.assertRaisesMessage("bar", theMock.getStuff) + + def testStubsWithMultipleChainedExceptions(self): + theMock = mock() + when(theMock).getStuff() \ + .thenRaise(Exception("foo"), Exception("bar")) \ + .thenRaise(Exception("foobar")) + + self.assertRaisesMessage("foo", theMock.getStuff) + self.assertRaisesMessage("bar", theMock.getStuff) + self.assertRaisesMessage("foobar", theMock.getStuff) + + def testLeavesOriginalMethodUntouchedWhenCreatingStubFromRealClass(self): + class Person: + def get_name(self): + return "original name" + + # given + person = Person() + mockPerson = mock(Person) + + # when + when(mockPerson).get_name().thenReturn("stubbed name") + + # then + self.assertEqual("stubbed name", mockPerson.get_name()) + self.assertEqual("original name", person.get_name(), + 'Original method should not be replaced.') + + def testStubsWithThenAnswer(self): + m = mock() + + when(m).magic_number().thenAnswer(lambda: 5) + + self.assertEqual(m.magic_number(), 5) + + when(m).add_one(any()).thenAnswer(lambda number: number + 1) + + self.assertEqual(m.add_one(5), 6) + self.assertEqual(m.add_one(8), 9) + + when(m).do_times(any(), any()).thenAnswer(lambda one, two: one * two) + + self.assertEqual(m.do_times(5, 4), 20) + self.assertEqual(m.do_times(8, 5), 40) + + when(m).do_dev_magic(any(), any()).thenAnswer(lambda a, b: a / b) + + self.assertEqual(m.do_dev_magic(20, 4), 5) + self.assertEqual(m.do_dev_magic(40, 5), 8) + + def test_key_words(testing="Magic"): + return testing + " Stuff" + + when(m).with_key_words().thenAnswer(test_key_words) + self.assertEqual(m.with_key_words(), "Magic Stuff") + + when(m).with_key_words(testing=any()).thenAnswer(test_key_words) + self.assertEqual(m.with_key_words(testing="Very Funky"), + "Very Funky Stuff") + + def testSubsWithThenAnswerAndMixedArgs(self): + repo = mock() + + def method_one(value, active_only=False): + return None + + def method_two(name=None, active_only=False): + return ["%s Connor" % name] + + def method_three(name=None, active_only=False): + return [name, active_only, 0] + + when(repo).findby(1).thenAnswer(lambda x: "John May (%d)" % x) + when(repo).findby(1, active_only=True).thenAnswer(method_one) + when(repo).findby(name="Sarah").thenAnswer(method_two) + when(repo).findby( + name="Sarah", active_only=True).thenAnswer(method_three) + + self.assertEqual("John May (1)", repo.findby(1)) + self.assertEqual(None, repo.findby(1, active_only=True)) + self.assertEqual(["Sarah Connor"], repo.findby(name="Sarah")) + self.assertEqual( + ["Sarah", True, 0], repo.findby(name="Sarah", active_only=True)) + diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 0000000..67a958f --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,37 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import unittest + + +class TestBase(unittest.TestCase): + + def assertRaisesMessage(self, message, callable, *params): + try: + if (params): + callable(params) + else: + callable() + except Exception as e: + self.assertEqual(message, str(e)) + else: + self.fail('Exception with message "%s" expected, but never raised' + % (message)) + diff --git a/tests/unstub_test.py b/tests/unstub_test.py new file mode 100644 index 0000000..5743424 --- /dev/null +++ b/tests/unstub_test.py @@ -0,0 +1,130 @@ +import pytest + +from mockito import when, unstub, verify, ArgumentError + + +class Dog(object): + def waggle(self): + return 'Unsure' + + def bark(self, sound='Wuff'): + return sound + + +class TestUntub: + def testIndependentUnstubbing(self): + rex = Dog() + mox = Dog() + + when(rex).waggle().thenReturn('Yup') + when(mox).waggle().thenReturn('Nope') + + assert rex.waggle() == 'Yup' + assert mox.waggle() == 'Nope' + + unstub(rex) + + assert rex.waggle() == 'Unsure' + assert mox.waggle() == 'Nope' + + unstub(mox) + + assert mox.waggle() == 'Unsure' + +class TestAutomaticUnstubbing: + + def testWith1(self): + rex = Dog() + + with when(rex).waggle().thenReturn('Yup'): + assert rex.waggle() == 'Yup' + verify(rex).waggle() + + assert rex.waggle() == 'Unsure' + + def testWith2(self): + # Ensure the short version to return None works + rex = Dog() + + with when(rex).waggle(): + assert rex.waggle() is None + verify(rex).waggle() + + assert rex.waggle() == 'Unsure' + + def testWithRaisingSideeffect(self): + rex = Dog() + + with pytest.raises(RuntimeError): + with when(rex).waggle().thenRaise(RuntimeError('Nope')): + rex.waggle() + assert rex.waggle() == 'Unsure' + + def testNesting(self): + # That's not a real test, I just wanted to see how it would + # look like; bc you run out of space pretty quickly + rex = Dog() + mox = Dog() + + with when(rex).waggle().thenReturn('Yup'): + with when(mox).waggle().thenReturn('Nope'): + assert rex.waggle() == 'Yup' + + assert rex.waggle() == 'Unsure' + assert mox.waggle() == 'Unsure' + # though that's a good looking option + with when(rex).waggle().thenReturn('Yup'), \ + when(mox).waggle().thenReturn('Nope'): # noqa: E127 + assert rex.waggle() == 'Yup' + assert mox.waggle() == 'Nope' + + assert rex.waggle() == 'Unsure' + assert mox.waggle() == 'Unsure' + + def testOnlyUnstubTheExactStub(self): + rex = Dog() + + when(rex).bark('Shhh').thenReturn('Nope') + with when(rex).bark('Miau').thenReturn('Grrr'): + assert rex.bark('Miau') == 'Grrr' + + assert rex.bark('Shhh') == 'Nope' + verify(rex, times=2).bark(Ellipsis) + + def testOnlyUnstubTheExcatMethod(self): + rex = Dog() + + when(rex).bark('Shhh').thenReturn('Nope') + with when(rex).waggle().thenReturn('Yup'): + assert rex.waggle() == 'Yup' + + assert rex.bark('Shhh') == 'Nope' + verify(rex, times=1).bark(Ellipsis) + verify(rex, times=1).waggle() + + def testCleanupRegistryAfterLastStub(self): + rex = Dog() + + with when(rex).bark('Shhh').thenReturn('Nope'): + with when(rex).bark('Miau').thenReturn('Grrr'): + assert rex.bark('Miau') == 'Grrr' + assert rex.bark('Shhh') == 'Nope' + + with pytest.raises(ArgumentError): + verify(rex).bark(Ellipsis) + + + class TestEnsureCleanUnstubIfMockingAGlobal: + def testA(self): + with when(Dog).waggle().thenReturn('Sure'): + rex = Dog() + assert rex.waggle() == 'Sure' + + verify(Dog).waggle() + + def testB(self): + with when(Dog).waggle().thenReturn('Sure'): + rex = Dog() + assert rex.waggle() == 'Sure' + + verify(Dog).waggle() diff --git a/tests/verification_errors_test.py b/tests/verification_errors_test.py new file mode 100644 index 0000000..54c382d --- /dev/null +++ b/tests/verification_errors_test.py @@ -0,0 +1,153 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +from mockito import (mock, when, verify, VerificationError, + verifyNoMoreInteractions, verification) +from mockito.verification import never + + +class TestVerificationErrors: + + def testPrintsNicelyNothingIfNeverUsed(self): + theMock = mock() + with pytest.raises(VerificationError) as exc: + verify(theMock).foo() + assert str(exc.value) == ''' +Wanted but not invoked: + + foo() + +Instead got: + + Nothing + +''' + + def testPrintsNicely(self): + theMock = mock() + theMock.foo('bar') + theMock.foo(12, baz='boz') + with pytest.raises(VerificationError) as exc: + verify(theMock).foo(True, None) + assert str(exc.value) == ''' +Wanted but not invoked: + + foo(True, None) + +Instead got: + + foo('bar') + foo(12, baz='boz') + +''' + + def testPrintOnlySameMethodInvocationsIfAny(self): + theMock = mock() + theMock.foo('bar') + theMock.bar('foo') + + with pytest.raises(VerificationError) as exc: + verify(theMock).bar('fox') + + assert str(exc.value) == ''' +Wanted but not invoked: + + bar('fox') + +Instead got: + + bar('foo') + +''' + + def testPrintAllInvocationsIfNoInvocationWithSameMethodName(self): + theMock = mock() + theMock.foo('bar') + theMock.bar('foo') + + with pytest.raises(VerificationError) as exc: + verify(theMock).box('fox') + + assert str(exc.value) == ''' +Wanted but not invoked: + + box('fox') + +Instead got: + + foo('bar') + bar('foo') + +''' + + + def testPrintKeywordArgumentsNicely(self): + theMock = mock() + with pytest.raises(VerificationError) as exc: + verify(theMock).foo(foo='foo', one=1) + message = str(exc.value) + # We do not want to guarantee any order of arguments here + assert "foo='foo'" in message + assert "one=1" in message + + def testPrintsOutThatTheActualAndExpectedInvocationCountDiffers(self): + theMock = mock() + when(theMock).foo().thenReturn(0) + + theMock.foo() + theMock.foo() + + with pytest.raises(VerificationError) as exc: + verify(theMock).foo() + assert "\nWanted times: 1, actual times: 2" == str(exc.value) + + def testPrintsUnwantedInteraction(self): + theMock = mock() + theMock.foo(1, 'foo') + with pytest.raises(VerificationError) as exc: + verifyNoMoreInteractions(theMock) + assert "\nUnwanted interaction: foo(1, 'foo')" == str(exc.value) + + def testPrintsNeverWantedInteractionsNicely(self): + theMock = mock() + theMock.foo() + with pytest.raises(VerificationError) as exc: + verify(theMock, never).foo() + assert "\nUnwanted invocation of foo(), times: 1" == str(exc.value) + + +class TestReprOfVerificationClasses: + def testTimes(self): + times = verification.Times(1) + assert repr(times) == "" + + def testAtLeast(self): + atleast = verification.AtLeast(2) + assert repr(atleast) == "" + + def testAtMost(self): + atmost = verification.AtMost(3) + assert repr(atmost) == "" + + def testBetween(self): + between = verification.Between(1, 2) + assert repr(between) == "" diff --git a/tests/verifications_test.py b/tests/verifications_test.py new file mode 100644 index 0000000..0141162 --- /dev/null +++ b/tests/verifications_test.py @@ -0,0 +1,368 @@ +# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +from .test_base import TestBase +from mockito import ( + mock, when, verify, forget_invocations, inorder, VerificationError, + ArgumentError, verifyNoMoreInteractions, verifyZeroInteractions, + verifyNoUnwantedInteractions, verifyStubbedInvocationsAreUsed, + any) +from mockito.verification import never + + +class VerificationTestBase(TestBase): + def __init__(self, verification_function, *args, **kwargs): + self.verification_function = verification_function + TestBase.__init__(self, *args, **kwargs) + + def setUp(self): + self.mock = mock() + + def testVerifies(self): + self.mock.foo() + self.mock.someOtherMethod(1, "foo", "bar") + + self.verification_function(self.mock).foo() + self.verification_function(self.mock).someOtherMethod(1, "foo", "bar") + + def testVerifiesWhenMethodIsUsingKeywordArguments(self): + self.mock.foo() + self.mock.someOtherMethod(1, fooarg="foo", bararg="bar") + + self.verification_function(self.mock).foo() + self.verification_function(self.mock).someOtherMethod(1, bararg="bar", + fooarg="foo") + + def testVerifiesDetectsNamedArguments(self): + self.mock.foo(fooarg="foo", bararg="bar") + + self.verification_function(self.mock).foo(bararg="bar", fooarg="foo") + try: + self.verification_function(self.mock).foo(bararg="foo", + fooarg="bar") + self.fail() + except VerificationError: + pass + + def testKeywordArgumentsOrderIsUnimportant(self): + self.mock.blub( + line="blabla", runs="55", failures="1", errors="2") + self.verification_function(self.mock).blub( + runs="55", failures="1", errors="2", line="blabla") + + def testFailsVerification(self): + self.mock.foo("boo") + + self.assertRaises(VerificationError, + self.verification_function(self.mock).foo, "not boo") + + def testVerifiesAnyTimes(self): + self.mock = mock() + self.mock.foo() + + self.verification_function(self.mock).foo() + self.verification_function(self.mock).foo() + self.verification_function(self.mock).foo() + + def testVerifiesMultipleCalls(self): + self.mock = mock() + self.mock.foo() + self.mock.foo() + self.mock.foo() + + self.verification_function(self.mock, times=3).foo() + + def testVerifiesMultipleCallsWhenMethodUsedAsFunction(self): + self.mock = mock() + f = self.mock.foo + f(1, 2) + f('foobar') + + self.verification_function(self.mock).foo(1, 2) + self.verification_function(self.mock).foo('foobar') + + def testFailsVerificationOfMultipleCalls(self): + self.mock = mock() + self.mock.foo() + self.mock.foo() + self.mock.foo() + + self.assertRaises(VerificationError, + self.verification_function(self.mock, times=2).foo) + + def testVerifiesUsingAnyMatcher(self): + self.mock.foo(1, "bar") + + self.verification_function(self.mock).foo(1, any()) + self.verification_function(self.mock).foo(any(), "bar") + self.verification_function(self.mock).foo(any(), any()) + + def testVerifiesUsingAnyIntMatcher(self): + self.mock.foo(1, "bar") + + self.verification_function(self.mock).foo(any(int), "bar") + + def testFailsVerificationUsingAnyIntMatcher(self): + self.mock.foo(1, "bar") + + self.assertRaises(VerificationError, + self.verification_function(self.mock).foo, 1, + any(int)) + self.assertRaises(VerificationError, + self.verification_function(self.mock).foo, any(int)) + + def testNumberOfTimesDefinedDirectlyInVerify(self): + self.mock.foo("bar") + + self.verification_function(self.mock, times=1).foo("bar") + + def testFailsWhenTimesIsLessThanZero(self): + self.assertRaises(ArgumentError, self.verification_function, None, -1) + + def testVerifiesAtLeastTwoWhenMethodInvokedTwice(self): + self.mock.foo() + self.mock.foo() + + self.verification_function(self.mock, atleast=2).foo() + + def testVerifiesAtLeastTwoWhenMethodInvokedFourTimes(self): + self.mock.foo() + self.mock.foo() + self.mock.foo() + self.mock.foo() + + self.verification_function(self.mock, atleast=2).foo() + + def testFailsWhenMethodInvokedOnceForAtLeastTwoVerification(self): + self.mock.foo() + self.assertRaises(VerificationError, + self.verification_function(self.mock, atleast=2).foo) + + def testVerifiesAtMostTwoWhenMethodInvokedTwice(self): + self.mock.foo() + self.mock.foo() + + self.verification_function(self.mock, atmost=2).foo() + + def testVerifiesAtMostTwoWhenMethodInvokedOnce(self): + self.mock.foo() + + self.verification_function(self.mock, atmost=2).foo() + + def testFailsWhenMethodInvokedFourTimesForAtMostTwoVerification(self): + self.mock.foo() + self.mock.foo() + self.mock.foo() + self.mock.foo() + + self.assertRaises(VerificationError, + self.verification_function(self.mock, atmost=2).foo) + + def testVerifiesBetween(self): + self.mock.foo() + self.mock.foo() + + self.verification_function(self.mock, between=[1, 2]).foo() + self.verification_function(self.mock, between=[2, 3]).foo() + self.verification_function(self.mock, between=[1, 5]).foo() + self.verification_function(self.mock, between=[2, 2]).foo() + + def testFailsVerificationWithBetween(self): + self.mock.foo() + self.mock.foo() + self.mock.foo() + + self.assertRaises(VerificationError, + self.verification_function(self.mock, + between=[1, 2]).foo) + self.assertRaises(VerificationError, + self.verification_function(self.mock, + between=[4, 9]).foo) + + def testFailsAtMostAtLeastAndBetweenVerificationWithWrongArguments(self): + self.assertRaises(ArgumentError, self.verification_function, + self.mock, atleast=0) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, atleast=-5) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, atmost=0) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, atmost=-5) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, between=[5, 1]) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, between=[-1, 1]) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, between=(0, 1, 2)) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, between=0) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, atleast=5, atmost=5) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, atleast=5, between=[1, 2]) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, atmost=5, between=[1, 2]) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, atleast=5, atmost=5, between=[1, 2]) + + def runTest(self): + pass + + +class VerifyTest(VerificationTestBase): + def __init__(self, *args, **kwargs): + VerificationTestBase.__init__(self, verify, *args, **kwargs) + + def testVerifyNeverCalled(self): + verify(self.mock, never).someMethod() + + def testVerifyNeverCalledRaisesError(self): + self.mock.foo() + self.assertRaises(VerificationError, verify(self.mock, never).foo) + + +class InorderVerifyTest(VerificationTestBase): + def __init__(self, *args, **kwargs): + VerificationTestBase.__init__(self, inorder.verify, *args, **kwargs) + + def setUp(self): + self.mock = mock() + + def testPassesIfOneIteraction(self): + self.mock.first() + inorder.verify(self.mock).first() + + def testPassesIfMultipleInteractions(self): + self.mock.first() + self.mock.second() + self.mock.third() + + inorder.verify(self.mock).first() + inorder.verify(self.mock).second() + inorder.verify(self.mock).third() + + def testFailsIfNoInteractions(self): + self.assertRaises(VerificationError, inorder.verify(self.mock).first) + + def testFailsIfWrongOrderOfInteractions(self): + self.mock.first() + self.mock.second() + + self.assertRaises(VerificationError, inorder.verify(self.mock).second) + + def testErrorMessage(self): + self.mock.second() + self.mock.first() + self.assertRaisesMessage( + '\nWanted first() to be invoked,\ngot second() instead.', + inorder.verify(self.mock).first) + + + def testPassesMixedVerifications(self): + self.mock.first() + self.mock.second() + + verify(self.mock).first() + verify(self.mock).second() + + inorder.verify(self.mock).first() + inorder.verify(self.mock).second() + + def testFailsMixedVerifications(self): + self.mock.second() + self.mock.first() + + # first - normal verifications, they should pass + verify(self.mock).first() + verify(self.mock).second() + + # but, inorder verification should fail + self.assertRaises(VerificationError, inorder.verify(self.mock).first) + + +class VerifyNoMoreInteractionsTest(TestBase): + def testVerifies(self): + mockOne, mockTwo = mock(), mock() + mockOne.foo() + mockTwo.bar() + + verify(mockOne).foo() + verify(mockTwo).bar() + verifyNoMoreInteractions(mockOne, mockTwo) + + def testFails(self): + theMock = mock() + theMock.foo() + self.assertRaises(VerificationError, verifyNoMoreInteractions, theMock) + + +class VerifyZeroInteractionsTest(TestBase): + def testVerifies(self): + theMock = mock() + verifyZeroInteractions(theMock) + theMock.foo() + self.assertRaises( + VerificationError, verifyNoMoreInteractions, theMock) + + +class ClearInvocationsTest(TestBase): + def testClearsInvocations(self): + theMock1 = mock() + theMock2 = mock() + theMock1.do_foo() + theMock2.do_bar() + + self.assertRaises(VerificationError, verifyZeroInteractions, theMock1) + self.assertRaises(VerificationError, verifyZeroInteractions, theMock2) + + forget_invocations(theMock1, theMock2) + + verifyZeroInteractions(theMock1) + verifyZeroInteractions(theMock2) + + def testPreservesStubs(self): + theMock = mock() + when(theMock).do_foo().thenReturn('hello') + self.assertEqual('hello', theMock.do_foo()) + + forget_invocations(theMock) + + self.assertEqual('hello', theMock.do_foo()) + + +class TestRaiseOnUnknownObjects: + @pytest.mark.parametrize('verification_fn', [ + verify, + verifyNoMoreInteractions, + verifyZeroInteractions, + verifyNoUnwantedInteractions, + verifyStubbedInvocationsAreUsed + ]) + def testVerifyShouldRaise(self, verification_fn): + class Foo(object): + pass + + with pytest.raises(ArgumentError) as exc: + verification_fn(Foo) + assert str(exc.value) == "obj '%s' is not registered" % Foo + + diff --git a/tests/when2_test.py b/tests/when2_test.py new file mode 100644 index 0000000..1f21d51 --- /dev/null +++ b/tests/when2_test.py @@ -0,0 +1,149 @@ + +import pytest + +from mockito import when2, patch, spy2, verify +from mockito.utils import newmethod + + +import os + + +pytestmark = pytest.mark.usefixtures("unstub") + + +class Dog(object): + def bark(self, sound): + return sound + + def bark_hard(self, sound): + return sound + '!' + + +class TestMockito2: + def testWhen2(self): + rex = Dog() + when2(rex.bark, 'Miau').thenReturn('Wuff') + when2(rex.bark, 'Miau').thenReturn('Grrr') + assert rex.bark('Miau') == 'Grrr' + + + def testPatch(self): + rex = Dog() + patch(rex.bark, lambda sound: sound + '!') + assert rex.bark('Miau') == 'Miau!' + + + def testPatch2(self): + rex = Dog() + patch(rex.bark, rex.bark_hard) + assert rex.bark('Miau') == 'Miau!' + + def testPatch3(self): + rex = Dog() + + def f(self, sound): + return self.bark_hard(sound) + + f = newmethod(f, rex) + patch(rex.bark, f) + + assert rex.bark('Miau') == 'Miau!' + + def testAddFnWithPatch(self): + rex = Dog() + + patch(rex, 'newfn', lambda s: s) + assert rex.newfn('Hi') == 'Hi' + + +class TestFancyObjResolver: + def testWhen2WithArguments(self): + # This test is a bit flaky bc pytest does not like a patched + # `os.path.exists` module. + when2(os.path.commonprefix, '/Foo').thenReturn(False) + when2(os.path.commonprefix, '/Foo').thenReturn(True) + when2(os.path.exists, '/Foo').thenReturn(True) + + assert os.path.commonprefix('/Foo') + assert os.path.exists('/Foo') + + def testWhen2WithoutArguments(self): + import time + when2(time.time).thenReturn('None') + assert time.time() == 'None' + + def testWhenSplitOnNextLine(self): + # fmt: off + when2( + os.path.commonprefix, '/Foo').thenReturn(True) + # fmt: on + assert os.path.commonprefix('/Foo') + + def testEnsureWithWhen2SameLine(self): + with when2(os.path.commonprefix, '/Foo'): + pass + + def testEnsureWithWhen2SplitLine(self): + # fmt: off + with when2( + os.path.commonprefix, '/Foo'): + pass + # fmt: on + + def testEnsureToResolveMethodOnClass(self): + class A(object): + class B(object): + def c(self): + pass + + when2(A.B.c) + + def testEnsureToResolveClass(self): + class A(object): + class B(object): + pass + + when2(A.B, 'Hi').thenReturn('Ho') + assert A.B('Hi') == 'Ho' + + + def testPatch(self): + patch(os.path.commonprefix, lambda m: 'yup') + patch(os.path.commonprefix, lambda m: 'yep') + + assert os.path.commonprefix(Ellipsis) == 'yep' + + def testWithPatchGivenTwoArgs(self): + with patch(os.path.exists, lambda m: 'yup'): + assert os.path.exists('foo') == 'yup' + + assert not os.path.exists('foo') + + def testWithPatchGivenThreeArgs(self): + with patch(os.path, 'exists', lambda m: 'yup'): + assert os.path.exists('foo') == 'yup' + + assert not os.path.exists('foo') + + def testSpy2(self): + spy2(os.path.exists) + + assert os.path.exists('/Foo') is False + + verify(os.path).exists('/Foo') + + class TestRejections: + def testA(self): + with pytest.raises(TypeError) as exc: + when2(os) + assert str(exc.value) == "can't guess origin of 'os'" + + cp = os.path.commonprefix + with pytest.raises(TypeError) as exc: + spy2(cp) + assert str(exc.value) == "can't guess origin of 'cp'" + + ptch = patch + with pytest.raises(TypeError) as exc: + ptch(os.path.exists, lambda: 'boo') + assert str(exc.value) == "could not destructure first argument" diff --git a/tests/when_interface_test.py b/tests/when_interface_test.py new file mode 100644 index 0000000..e5cef53 --- /dev/null +++ b/tests/when_interface_test.py @@ -0,0 +1,141 @@ + +import pytest + +from mockito import when, when2, expect, verify, patch, mock, spy2 +from mockito.invocation import InvocationError + +class Dog(object): + def bark(self): + pass + + +class Unhashable(object): + def update(self, **kwargs): + pass + + def __hash__(self): + raise TypeError("I'm immutable") + + +@pytest.mark.usefixtures('unstub') +class TestUserExposedInterfaces: + + def testWhen(self): + whening = when(Dog) + assert whening.__dict__ == {} + + def testExpect(self): + expecting = expect(Dog) + assert expecting.__dict__ == {} + + def testVerify(self): + dummy = mock() + verifying = verify(dummy) + assert verifying.__dict__ == {} + + + def testEnsureUnhashableObjectCanBeMocked(self): + obj = Unhashable() + when(obj).update().thenReturn(None) + + +@pytest.mark.usefixtures('unstub') +class TestPassAroundStrictness: + + def testReconfigureStrictMock(self): + when(Dog).bark() # important first call, inits theMock + + when(Dog, strict=False).waggle().thenReturn('Sure') + expect(Dog, strict=False).weggle().thenReturn('Sure') + + + with pytest.raises(InvocationError): + when(Dog).wuggle() + + with pytest.raises(InvocationError): + when(Dog).woggle() + + rex = Dog() + assert rex.waggle() == 'Sure' + assert rex.weggle() == 'Sure' + + # For documentation; the inital strict value of the mock will be used + # here. So the above when(..., strict=False) just assures we can + # actually *add* an attribute to the mocked object + with pytest.raises(InvocationError): + rex.waggle(1) + + verify(Dog).waggle() + verify(Dog).weggle() + + def testReconfigureLooseMock(self): + when(Dog, strict=False).bark() # important first call, inits theMock + + when(Dog, strict=False).waggle().thenReturn('Sure') + expect(Dog, strict=False).weggle().thenReturn('Sure') + + with pytest.raises(InvocationError): + when(Dog).wuggle() + + with pytest.raises(InvocationError): + when(Dog).woggle() + + rex = Dog() + assert rex.waggle() == 'Sure' + assert rex.weggle() == 'Sure' + + # For documentation; see test above. strict is inherited from the + # initial mock. So we return `None` + assert rex.waggle(1) is None + + verify(Dog).waggle() + verify(Dog).weggle() + + # Where to put this test? + def testEnsureAddedAttributesGetRemovedOnUnstub(self): + with when(Dog, strict=False).wggle(): + pass + + with pytest.raises(AttributeError): + getattr(Dog, 'wggle') + + +@pytest.mark.usefixtures('unstub') +class TestDottedPaths: + + def testWhen(self): + when('os.path').exists('/Foo').thenReturn(True) + + import os.path + assert os.path.exists('/Foo') + + def testWhen2(self): + when2('os.path.exists', '/Foo').thenReturn(True) + + import os.path + assert os.path.exists('/Foo') + + def testPatch(self): + dummy = mock() + patch('os.path.exists', dummy) + + import os.path + assert os.path.exists('/Foo') is None + + verify(dummy).__call__('/Foo') + + def testVerify(self): + when('os.path').exists('/Foo').thenReturn(True) + + import os.path + os.path.exists('/Foo') + + verify('os.path', times=1).exists('/Foo') + + def testSpy2(self): + spy2('os.path.exists') + + import os.path + assert not os.path.exists('/Foo') + + verify('os.path', times=1).exists('/Foo')