diff --git a/LICENSE b/LICENSE index 1dd90f2..72a8093 100644 --- a/LICENSE +++ b/LICENSE @@ -1,165 +1,21 @@ -GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 +MIT License - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +Copyright (c) [2016] [Bruno Oliveira] +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: - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. \ No newline at end of file +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. \ No newline at end of file diff --git a/PKG-INFO b/PKG-INFO index 47b3137..1ee9ff6 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,55 +1,56 @@ Metadata-Version: 1.1 Name: pytest-mock -Version: 0.8.1 +Version: 1.1 Summary: Thin-wrapper around the mock package for easier use with py.test Home-page: https://github.com/pytest-dev/pytest-mock/ Author: Bruno Oliveira Author-email: nicoddemus@gmail.com -License: LGPL +License: MIT Description: =========== pytest-mock =========== This plugin installs a ``mocker`` fixture which is a thin-wrapper around the patching API - provided by the excellent `mock `_ package, + provided by the `mock package `_, but with the benefit of not having to worry about undoing patches at the end of a test: .. code-block:: python - + def test_unix_fs(mocker): mocker.patch('os.remove') UnixFS.rm('file') os.remove.assert_called_once_with('file') - + .. Using PNG badges because PyPI doesn't support SVG - |python| |version| |downloads| |ci| |coverage| + |python| |version| |downloads| |ci| |appveyor| |coverage| .. |version| image:: http://img.shields.io/pypi/v/pytest-mock.png :target: https://pypi.python.org/pypi/pytest-mock - + .. |downloads| image:: http://img.shields.io/pypi/dm/pytest-mock.png :target: https://pypi.python.org/pypi/pytest-mock .. |ci| image:: http://img.shields.io/travis/pytest-dev/pytest-mock.png :target: https://travis-ci.org/pytest-dev/pytest-mock + .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/pid1t7iuwhkm9eh6/branch/master?svg=true + :target: https://ci.appveyor.com/project/pytestbot/pytest-mock + .. |coverage| image:: http://img.shields.io/coveralls/pytest-dev/pytest-mock.png :target: https://coveralls.io/r/pytest-dev/pytest-mock - .. |python| image:: https://pypip.in/py_versions/pytest-mock/badge.png + .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-mock.svg :target: https://pypi.python.org/pypi/pytest-mock/ - :alt: Supported Python versions - Usage ===== The ``mocker`` fixture has the same API as - `mock.patch `_, + `mock.patch `_, supporting the same arguments: .. code-block:: python @@ -59,16 +60,187 @@ Description: =========== mocker.patch('os.remove') mocker.patch.object(os, 'listdir', autospec=True) mocked_isfile = mocker.patch('os.path.isfile') - + The supported methods are: - + * ``mocker.patch``: see http://www.voidspace.org.uk/python/mock/patch.html#patch. * ``mocker.patch.object``: see http://www.voidspace.org.uk/python/mock/patch.html#patch-object. * ``mocker.patch.multiple``: see http://www.voidspace.org.uk/python/mock/patch.html#patch-multiple. * ``mocker.patch.dict``: see http://www.voidspace.org.uk/python/mock/patch.html#patch-dict. - * ``mocker.stopall()``: stops all active patches at this point. + * ``mocker.stopall()``: stops all active patches up to this point. + * ``mocker.resetall()``: calls ``reset_mock()`` in all mocked objects up to this point. - Note that, although mocker's API is intentionally the same as ``mock.patch``'s, its uses as context managers and function decorators are **not** supported. The purpose of this plugin is to make the use of context managers and function decorators for mocking unnecessary. Indeed, trying to use the functionality in ``mocker`` in this manner can lead to non-intuitive errors: + Some objects from the ``mock`` module are accessible directly from ``mocker`` for convenience: + + * `Mock `_ + * `MagicMock `_ + * `PropertyMock `_ + * `ANY `_ + * `call `_ *(Version 1.1)* + + + Spy + --- + + The spy acts exactly like the original method in all cases, except it allows use of `mock` + features with it, like retrieving call count. It also works for class and static methods. + + + .. code-block:: python + + def test_spy(mocker): + class Foo(object): + def bar(self): + return 42 + + foo = Foo() + mocker.spy(foo, 'bar') + assert foo.bar() == 42 + assert foo.bar.call_count == 1 + + Stub + ---- + + + The stub is a mock object that accepts any arguments and is useful to test callbacks, for instance. + May be passed a name to be used by the constructed stub object in its repr (useful for debugging). + + .. code-block:: python + + def test_stub(mocker): + def foo(on_something): + on_something('foo', 'bar') + + stub = mocker.stub(name='on_something_stub') + + foo(stub) + stub.assert_called_once_with('foo', 'bar') + + + Improved reporting of mock call assertion errors + ------------------------------------------------ + + + This plugin monkeypatches the mock library to improve pytest output for failures + of mock call assertions like ``Mock.assert_called_with()`` by hiding internal traceback + entries from the ``mock`` module. + + It also adds introspection information on differing call arguments when + calling the helper methods. This features catches `AssertionError` raised in + the method, and uses py.test's own `advanced assertions`_ to return a better + diff:: + + + m = mocker.patch.object(DS, 'create_char') + DS().create_char('Raistlin', class_='mag', gift=12) + > m.assert_called_once_with('Raistlin', class_='mage', gift=12) + E assert {'class_': 'mag', 'gift': 12} == {'class_': 'mage', 'gift': 12} + E Omitting 1 identical items, use -v to show + E Differing items: + E {'class_': 'mag'} != {'class_': 'mage'} + E Use -v to get the full diff + + + This is useful when asserting mock calls with many/nested arguments and trying + to quickly see the difference. + + This feature is probably safe, but if you encounter any problems it can be disabled in + your ``pytest.ini`` file: + + .. code-block:: ini + + [pytest] + mock_traceback_monkeypatch = false + + Note that this feature is automatically disabled with the ``--tb=native`` option. The underlying + mechanism used to suppress traceback entries from ``mock`` module does not work with that option + anyway plus it generates confusing messages on Python 3.5 due to exception chaining + + .. _advanced assertions: https://pytest.org/latest/assert.html + + + Requirements + ============ + + * Python 2.6+, Python 3.3+ + * pytest + * mock (for Python 2) + + + Install + ======= + + Install using `pip `_: + + .. code-block:: console + + $ pip install pytest-mock + + Changelog + ========= + + Please consult the `changelog page`_. + + .. _changelog page: https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst + + Why bother with a plugin? + ========================= + + There are a number of different ``patch`` usages in the standard ``mock`` API, + but IMHO they don't scale very well when you have more than one or two + patches to apply. + + It may lead to an excessive nesting of ``with`` statements, breaking the flow + of the test: + + .. code-block:: python + + import mock + + def test_unix_fs(): + with mock.patch('os.remove'): + UnixFS.rm('file') + os.remove.assert_called_once_with('file') + + with mock.patch('os.listdir'): + assert UnixFS.ls('dir') == expected + # ... + + with mock.patch('shutil.copy'): + UnixFS.cp('src', 'dst') + # ... + + + One can use ``patch`` as a decorator to improve the flow of the test: + + .. code-block:: python + + @mock.patch('os.remove') + @mock.patch('os.listdir') + @mock.patch('shutil.copy') + def test_unix_fs(mocked_copy, mocked_listdir, mocked_remove): + UnixFS.rm('file') + os.remove.assert_called_once_with('file') + + assert UnixFS.ls('dir') == expected + # ... + + UnixFS.cp('src', 'dst') + # ... + + But this poses a few disadvantages: + + - test functions must receive the mock objects as parameter, even if you don't plan to + access them directly; also, order depends on the order of the decorated ``patch`` + functions; + - receiving the mocks as parameters doesn't mix nicely with pytest's approach of + naming fixtures as parameters, or ``pytest.mark.parametrize``; + - you can't easily undo the mocking during the test execution; + + + **Note** + + Although mocker's API is intentionally the same as ``mock.patch``'s, its uses as context managers and function decorators are **not** supported. The purpose of this plugin is to make the use of context managers and function decorators for mocking unnecessary. Indeed, trying to use the functionality in ``mocker`` in this manner can lead to non-intuitive errors: .. code-block:: python @@ -86,154 +258,24 @@ Description: =========== E AttributeError: __exit__ - You can also access ``Mock`` and ``MagicMock`` directly using from ``mocker`` - fixture: - - .. code-block:: python - - def test_feature(mocker): - ret = [mocker.Mock(return_value=True), mocker.Mock(return_value=True)] - mocker.patch('mylib.func', side_effect=ret) - - *New in version 0.5* - - Spy - --- - - *New in version 0.6* - - The spy acts exactly like the original method in all cases, except it allows use of `mock` - features with it, like retrieving call count. - - From version 0.7 onward it also works for class and static methods. Originally it was only safe to - use with instance methods. - - .. code-block:: python - - def test_spy(mocker): - class Foo(object): - def bar(self): - return 42 - - foo = Foo() - mocker.spy(foo, 'bar') - assert foo.bar() == 42 - assert foo.bar.call_count == 1 - - Stub - ---- - - *New in version 0.6* - - The stub is a mock object that accepts any arguments and is useful to test callbacks, for instance. - - .. code-block:: python - - def test_stub(mocker): - def foo(on_something): - on_something('foo', 'bar') - - stub = mocker.stub() - - foo(stub) - stub.assert_called_once_with('foo', 'bar') - - Note - ---- - - Prior to version ``0.4.0``, the ``mocker`` fixture was named ``mock``. - This was changed because naming the fixture ``mock`` conflicts with the - actual ``mock`` module, which made using it awkward when access to both the - module and the plugin were required within a test. - - The old fixture ``mock`` still works, but its use is discouraged and will be - removed in version ``1.0``. - - Requirements - ============ - - * Python 2.6+, Python 3.2+ - * pytest - * mock (for Python < 3.3) - - - Install + License ======= - Install using `pip `_: + Distributed under the terms of the `MIT`_ license. - .. code-block:: console - - $ pip install pytest-mock - - Changelog - ========= - - Please consult `releases `_. - - Why bother with a plugin? - ========================= - - There are a number of different ``patch`` usages in the standard ``mock`` API, - but IMHO they don't scale very well when you have more than one or two - patches to apply. - - It may lead to an excessive nesting of ``with`` statements, breaking the flow - of the test: - - .. code-block:: python - - import mock - - def test_unix_fs(): - with mock.patch('os.remove'): - UnixFS.rm('file') - os.remove.assert_called_once_with('file') - - with mock.patch('os.listdir'): - assert UnixFS.ls('dir') == expected - # ... - - with mock.patch('shutil.copy'): - UnixFS.cp('src', 'dst') - # ... - - - One can use ``patch`` as a decorator to improve the flow of the test: - - .. code-block:: python - - @mock.patch('os.remove') - @mock.patch('os.listdir') - @mock.patch('shutil.copy') - def test_unix_fs(mocked_copy, mocked_listdir, mocked_remove): - UnixFS.rm('file') - os.remove.assert_called_once_with('file') - - assert UnixFS.ls('dir') == expected - # ... - - UnixFS.cp('src', 'dst') - # ... - - But this poses a few disadvantages: - - - test functions must receive the mock objects as parameter, even if you don't plan to - access them directly; also, order depends on the order of the decorated ``patch`` - functions; - - receiving the mocks as parameters doesn't mix nicely with pytest's approach of - naming fixtures as parameters, or ``pytest.mark.parametrize``; - - you can't easily undo the mocking during the test execution; + .. _MIT: https://github.com/pytest-dev/pytest-mock/blob/master/LICENSE Keywords: pytest mock Platform: any -Classifier: Development Status :: 3 - Alpha +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) +Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Testing diff --git a/README.rst b/README.rst index 28c02d7..d0d3033 100644 --- a/README.rst +++ b/README.rst @@ -3,45 +3,46 @@ pytest-mock =========== This plugin installs a ``mocker`` fixture which is a thin-wrapper around the patching API -provided by the excellent `mock `_ package, +provided by the `mock package `_, but with the benefit of not having to worry about undoing patches at the end of a test: .. code-block:: python - + def test_unix_fs(mocker): mocker.patch('os.remove') UnixFS.rm('file') os.remove.assert_called_once_with('file') - + .. Using PNG badges because PyPI doesn't support SVG -|python| |version| |downloads| |ci| |coverage| +|python| |version| |downloads| |ci| |appveyor| |coverage| .. |version| image:: http://img.shields.io/pypi/v/pytest-mock.png :target: https://pypi.python.org/pypi/pytest-mock - + .. |downloads| image:: http://img.shields.io/pypi/dm/pytest-mock.png :target: https://pypi.python.org/pypi/pytest-mock .. |ci| image:: http://img.shields.io/travis/pytest-dev/pytest-mock.png :target: https://travis-ci.org/pytest-dev/pytest-mock +.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/pid1t7iuwhkm9eh6/branch/master?svg=true + :target: https://ci.appveyor.com/project/pytestbot/pytest-mock + .. |coverage| image:: http://img.shields.io/coveralls/pytest-dev/pytest-mock.png :target: https://coveralls.io/r/pytest-dev/pytest-mock -.. |python| image:: https://pypip.in/py_versions/pytest-mock/badge.png +.. |python| image:: https://img.shields.io/pypi/pyversions/pytest-mock.svg :target: https://pypi.python.org/pypi/pytest-mock/ - :alt: Supported Python versions - Usage ===== The ``mocker`` fixture has the same API as -`mock.patch `_, +`mock.patch `_, supporting the same arguments: .. code-block:: python @@ -51,16 +52,187 @@ supporting the same arguments: mocker.patch('os.remove') mocker.patch.object(os, 'listdir', autospec=True) mocked_isfile = mocker.patch('os.path.isfile') - + The supported methods are: - + * ``mocker.patch``: see http://www.voidspace.org.uk/python/mock/patch.html#patch. * ``mocker.patch.object``: see http://www.voidspace.org.uk/python/mock/patch.html#patch-object. * ``mocker.patch.multiple``: see http://www.voidspace.org.uk/python/mock/patch.html#patch-multiple. * ``mocker.patch.dict``: see http://www.voidspace.org.uk/python/mock/patch.html#patch-dict. -* ``mocker.stopall()``: stops all active patches at this point. +* ``mocker.stopall()``: stops all active patches up to this point. +* ``mocker.resetall()``: calls ``reset_mock()`` in all mocked objects up to this point. -Note that, although mocker's API is intentionally the same as ``mock.patch``'s, its uses as context managers and function decorators are **not** supported. The purpose of this plugin is to make the use of context managers and function decorators for mocking unnecessary. Indeed, trying to use the functionality in ``mocker`` in this manner can lead to non-intuitive errors: +Some objects from the ``mock`` module are accessible directly from ``mocker`` for convenience: + +* `Mock `_ +* `MagicMock `_ +* `PropertyMock `_ +* `ANY `_ +* `call `_ *(Version 1.1)* + + +Spy +--- + +The spy acts exactly like the original method in all cases, except it allows use of `mock` +features with it, like retrieving call count. It also works for class and static methods. + + +.. code-block:: python + + def test_spy(mocker): + class Foo(object): + def bar(self): + return 42 + + foo = Foo() + mocker.spy(foo, 'bar') + assert foo.bar() == 42 + assert foo.bar.call_count == 1 + +Stub +---- + + +The stub is a mock object that accepts any arguments and is useful to test callbacks, for instance. +May be passed a name to be used by the constructed stub object in its repr (useful for debugging). + +.. code-block:: python + + def test_stub(mocker): + def foo(on_something): + on_something('foo', 'bar') + + stub = mocker.stub(name='on_something_stub') + + foo(stub) + stub.assert_called_once_with('foo', 'bar') + + +Improved reporting of mock call assertion errors +------------------------------------------------ + + +This plugin monkeypatches the mock library to improve pytest output for failures +of mock call assertions like ``Mock.assert_called_with()`` by hiding internal traceback +entries from the ``mock`` module. + +It also adds introspection information on differing call arguments when +calling the helper methods. This features catches `AssertionError` raised in +the method, and uses py.test's own `advanced assertions`_ to return a better +diff:: + + + m = mocker.patch.object(DS, 'create_char') + DS().create_char('Raistlin', class_='mag', gift=12) + > m.assert_called_once_with('Raistlin', class_='mage', gift=12) + E assert {'class_': 'mag', 'gift': 12} == {'class_': 'mage', 'gift': 12} + E Omitting 1 identical items, use -v to show + E Differing items: + E {'class_': 'mag'} != {'class_': 'mage'} + E Use -v to get the full diff + + +This is useful when asserting mock calls with many/nested arguments and trying +to quickly see the difference. + +This feature is probably safe, but if you encounter any problems it can be disabled in +your ``pytest.ini`` file: + +.. code-block:: ini + + [pytest] + mock_traceback_monkeypatch = false + +Note that this feature is automatically disabled with the ``--tb=native`` option. The underlying +mechanism used to suppress traceback entries from ``mock`` module does not work with that option +anyway plus it generates confusing messages on Python 3.5 due to exception chaining + +.. _advanced assertions: https://pytest.org/latest/assert.html + + +Requirements +============ + +* Python 2.6+, Python 3.3+ +* pytest +* mock (for Python 2) + + +Install +======= + +Install using `pip `_: + +.. code-block:: console + + $ pip install pytest-mock + +Changelog +========= + +Please consult the `changelog page`_. + +.. _changelog page: https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst + +Why bother with a plugin? +========================= + +There are a number of different ``patch`` usages in the standard ``mock`` API, +but IMHO they don't scale very well when you have more than one or two +patches to apply. + +It may lead to an excessive nesting of ``with`` statements, breaking the flow +of the test: + +.. code-block:: python + + import mock + + def test_unix_fs(): + with mock.patch('os.remove'): + UnixFS.rm('file') + os.remove.assert_called_once_with('file') + + with mock.patch('os.listdir'): + assert UnixFS.ls('dir') == expected + # ... + + with mock.patch('shutil.copy'): + UnixFS.cp('src', 'dst') + # ... + + +One can use ``patch`` as a decorator to improve the flow of the test: + +.. code-block:: python + + @mock.patch('os.remove') + @mock.patch('os.listdir') + @mock.patch('shutil.copy') + def test_unix_fs(mocked_copy, mocked_listdir, mocked_remove): + UnixFS.rm('file') + os.remove.assert_called_once_with('file') + + assert UnixFS.ls('dir') == expected + # ... + + UnixFS.cp('src', 'dst') + # ... + +But this poses a few disadvantages: + +- test functions must receive the mock objects as parameter, even if you don't plan to + access them directly; also, order depends on the order of the decorated ``patch`` + functions; +- receiving the mocks as parameters doesn't mix nicely with pytest's approach of + naming fixtures as parameters, or ``pytest.mark.parametrize``; +- you can't easily undo the mocking during the test execution; + + +**Note** + +Although mocker's API is intentionally the same as ``mock.patch``'s, its uses as context managers and function decorators are **not** supported. The purpose of this plugin is to make the use of context managers and function decorators for mocking unnecessary. Indeed, trying to use the functionality in ``mocker`` in this manner can lead to non-intuitive errors: .. code-block:: python @@ -78,141 +250,9 @@ Note that, although mocker's API is intentionally the same as ``mock.patch``'s, E AttributeError: __exit__ -You can also access ``Mock`` and ``MagicMock`` directly using from ``mocker`` -fixture: - -.. code-block:: python - - def test_feature(mocker): - ret = [mocker.Mock(return_value=True), mocker.Mock(return_value=True)] - mocker.patch('mylib.func', side_effect=ret) - -*New in version 0.5* - -Spy ---- - -*New in version 0.6* - -The spy acts exactly like the original method in all cases, except it allows use of `mock` -features with it, like retrieving call count. - -From version 0.7 onward it also works for class and static methods. Originally it was only safe to -use with instance methods. - -.. code-block:: python - - def test_spy(mocker): - class Foo(object): - def bar(self): - return 42 - - foo = Foo() - mocker.spy(foo, 'bar') - assert foo.bar() == 42 - assert foo.bar.call_count == 1 - -Stub ----- - -*New in version 0.6* - -The stub is a mock object that accepts any arguments and is useful to test callbacks, for instance. - -.. code-block:: python - - def test_stub(mocker): - def foo(on_something): - on_something('foo', 'bar') - - stub = mocker.stub() - - foo(stub) - stub.assert_called_once_with('foo', 'bar') - -Note ----- - -Prior to version ``0.4.0``, the ``mocker`` fixture was named ``mock``. -This was changed because naming the fixture ``mock`` conflicts with the -actual ``mock`` module, which made using it awkward when access to both the -module and the plugin were required within a test. - -The old fixture ``mock`` still works, but its use is discouraged and will be -removed in version ``1.0``. - -Requirements -============ - -* Python 2.6+, Python 3.2+ -* pytest -* mock (for Python < 3.3) - - -Install +License ======= -Install using `pip `_: +Distributed under the terms of the `MIT`_ license. -.. code-block:: console - - $ pip install pytest-mock - -Changelog -========= - -Please consult `releases `_. - -Why bother with a plugin? -========================= - -There are a number of different ``patch`` usages in the standard ``mock`` API, -but IMHO they don't scale very well when you have more than one or two -patches to apply. - -It may lead to an excessive nesting of ``with`` statements, breaking the flow -of the test: - -.. code-block:: python - - import mock - - def test_unix_fs(): - with mock.patch('os.remove'): - UnixFS.rm('file') - os.remove.assert_called_once_with('file') - - with mock.patch('os.listdir'): - assert UnixFS.ls('dir') == expected - # ... - - with mock.patch('shutil.copy'): - UnixFS.cp('src', 'dst') - # ... - - -One can use ``patch`` as a decorator to improve the flow of the test: - -.. code-block:: python - - @mock.patch('os.remove') - @mock.patch('os.listdir') - @mock.patch('shutil.copy') - def test_unix_fs(mocked_copy, mocked_listdir, mocked_remove): - UnixFS.rm('file') - os.remove.assert_called_once_with('file') - - assert UnixFS.ls('dir') == expected - # ... - - UnixFS.cp('src', 'dst') - # ... - -But this poses a few disadvantages: - -- test functions must receive the mock objects as parameter, even if you don't plan to - access them directly; also, order depends on the order of the decorated ``patch`` - functions; -- receiving the mocks as parameters doesn't mix nicely with pytest's approach of - naming fixtures as parameters, or ``pytest.mark.parametrize``; -- you can't easily undo the mocking during the test execution; +.. _MIT: https://github.com/pytest-dev/pytest-mock/blob/master/LICENSE diff --git a/pytest_mock.egg-info/PKG-INFO b/pytest_mock.egg-info/PKG-INFO new file mode 100644 index 0000000..1ee9ff6 --- /dev/null +++ b/pytest_mock.egg-info/PKG-INFO @@ -0,0 +1,281 @@ +Metadata-Version: 1.1 +Name: pytest-mock +Version: 1.1 +Summary: Thin-wrapper around the mock package for easier use with py.test +Home-page: https://github.com/pytest-dev/pytest-mock/ +Author: Bruno Oliveira +Author-email: nicoddemus@gmail.com +License: MIT +Description: =========== + pytest-mock + =========== + + This plugin installs a ``mocker`` fixture which is a thin-wrapper around the patching API + provided by the `mock package `_, + but with the benefit of not having to worry about undoing patches at the end + of a test: + + .. code-block:: python + + + def test_unix_fs(mocker): + mocker.patch('os.remove') + UnixFS.rm('file') + os.remove.assert_called_once_with('file') + + + .. Using PNG badges because PyPI doesn't support SVG + + |python| |version| |downloads| |ci| |appveyor| |coverage| + + .. |version| image:: http://img.shields.io/pypi/v/pytest-mock.png + :target: https://pypi.python.org/pypi/pytest-mock + + .. |downloads| image:: http://img.shields.io/pypi/dm/pytest-mock.png + :target: https://pypi.python.org/pypi/pytest-mock + + .. |ci| image:: http://img.shields.io/travis/pytest-dev/pytest-mock.png + :target: https://travis-ci.org/pytest-dev/pytest-mock + + .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/pid1t7iuwhkm9eh6/branch/master?svg=true + :target: https://ci.appveyor.com/project/pytestbot/pytest-mock + + .. |coverage| image:: http://img.shields.io/coveralls/pytest-dev/pytest-mock.png + :target: https://coveralls.io/r/pytest-dev/pytest-mock + + .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-mock.svg + :target: https://pypi.python.org/pypi/pytest-mock/ + + Usage + ===== + + The ``mocker`` fixture has the same API as + `mock.patch `_, + supporting the same arguments: + + .. code-block:: python + + def test_foo(mocker): + # all valid calls + mocker.patch('os.remove') + mocker.patch.object(os, 'listdir', autospec=True) + mocked_isfile = mocker.patch('os.path.isfile') + + The supported methods are: + + * ``mocker.patch``: see http://www.voidspace.org.uk/python/mock/patch.html#patch. + * ``mocker.patch.object``: see http://www.voidspace.org.uk/python/mock/patch.html#patch-object. + * ``mocker.patch.multiple``: see http://www.voidspace.org.uk/python/mock/patch.html#patch-multiple. + * ``mocker.patch.dict``: see http://www.voidspace.org.uk/python/mock/patch.html#patch-dict. + * ``mocker.stopall()``: stops all active patches up to this point. + * ``mocker.resetall()``: calls ``reset_mock()`` in all mocked objects up to this point. + + Some objects from the ``mock`` module are accessible directly from ``mocker`` for convenience: + + * `Mock `_ + * `MagicMock `_ + * `PropertyMock `_ + * `ANY `_ + * `call `_ *(Version 1.1)* + + + Spy + --- + + The spy acts exactly like the original method in all cases, except it allows use of `mock` + features with it, like retrieving call count. It also works for class and static methods. + + + .. code-block:: python + + def test_spy(mocker): + class Foo(object): + def bar(self): + return 42 + + foo = Foo() + mocker.spy(foo, 'bar') + assert foo.bar() == 42 + assert foo.bar.call_count == 1 + + Stub + ---- + + + The stub is a mock object that accepts any arguments and is useful to test callbacks, for instance. + May be passed a name to be used by the constructed stub object in its repr (useful for debugging). + + .. code-block:: python + + def test_stub(mocker): + def foo(on_something): + on_something('foo', 'bar') + + stub = mocker.stub(name='on_something_stub') + + foo(stub) + stub.assert_called_once_with('foo', 'bar') + + + Improved reporting of mock call assertion errors + ------------------------------------------------ + + + This plugin monkeypatches the mock library to improve pytest output for failures + of mock call assertions like ``Mock.assert_called_with()`` by hiding internal traceback + entries from the ``mock`` module. + + It also adds introspection information on differing call arguments when + calling the helper methods. This features catches `AssertionError` raised in + the method, and uses py.test's own `advanced assertions`_ to return a better + diff:: + + + m = mocker.patch.object(DS, 'create_char') + DS().create_char('Raistlin', class_='mag', gift=12) + > m.assert_called_once_with('Raistlin', class_='mage', gift=12) + E assert {'class_': 'mag', 'gift': 12} == {'class_': 'mage', 'gift': 12} + E Omitting 1 identical items, use -v to show + E Differing items: + E {'class_': 'mag'} != {'class_': 'mage'} + E Use -v to get the full diff + + + This is useful when asserting mock calls with many/nested arguments and trying + to quickly see the difference. + + This feature is probably safe, but if you encounter any problems it can be disabled in + your ``pytest.ini`` file: + + .. code-block:: ini + + [pytest] + mock_traceback_monkeypatch = false + + Note that this feature is automatically disabled with the ``--tb=native`` option. The underlying + mechanism used to suppress traceback entries from ``mock`` module does not work with that option + anyway plus it generates confusing messages on Python 3.5 due to exception chaining + + .. _advanced assertions: https://pytest.org/latest/assert.html + + + Requirements + ============ + + * Python 2.6+, Python 3.3+ + * pytest + * mock (for Python 2) + + + Install + ======= + + Install using `pip `_: + + .. code-block:: console + + $ pip install pytest-mock + + Changelog + ========= + + Please consult the `changelog page`_. + + .. _changelog page: https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst + + Why bother with a plugin? + ========================= + + There are a number of different ``patch`` usages in the standard ``mock`` API, + but IMHO they don't scale very well when you have more than one or two + patches to apply. + + It may lead to an excessive nesting of ``with`` statements, breaking the flow + of the test: + + .. code-block:: python + + import mock + + def test_unix_fs(): + with mock.patch('os.remove'): + UnixFS.rm('file') + os.remove.assert_called_once_with('file') + + with mock.patch('os.listdir'): + assert UnixFS.ls('dir') == expected + # ... + + with mock.patch('shutil.copy'): + UnixFS.cp('src', 'dst') + # ... + + + One can use ``patch`` as a decorator to improve the flow of the test: + + .. code-block:: python + + @mock.patch('os.remove') + @mock.patch('os.listdir') + @mock.patch('shutil.copy') + def test_unix_fs(mocked_copy, mocked_listdir, mocked_remove): + UnixFS.rm('file') + os.remove.assert_called_once_with('file') + + assert UnixFS.ls('dir') == expected + # ... + + UnixFS.cp('src', 'dst') + # ... + + But this poses a few disadvantages: + + - test functions must receive the mock objects as parameter, even if you don't plan to + access them directly; also, order depends on the order of the decorated ``patch`` + functions; + - receiving the mocks as parameters doesn't mix nicely with pytest's approach of + naming fixtures as parameters, or ``pytest.mark.parametrize``; + - you can't easily undo the mocking during the test execution; + + + **Note** + + Although mocker's API is intentionally the same as ``mock.patch``'s, its uses as context managers and function decorators are **not** supported. The purpose of this plugin is to make the use of context managers and function decorators for mocking unnecessary. Indeed, trying to use the functionality in ``mocker`` in this manner can lead to non-intuitive errors: + + .. code-block:: python + + def test_context_manager(mocker): + a = A() + with mocker.patch.object(a, 'doIt', return_value=True, autospec=True): + assert a.doIt() == True + + .. code-block:: console + + ================================== FAILURES =================================== + ____________________________ test_context_manager _____________________________ + in test_context_manager + with mocker.patch.object(a, 'doIt', return_value=True, autospec=True): + E AttributeError: __exit__ + + + License + ======= + + Distributed under the terms of the `MIT`_ license. + + .. _MIT: https://github.com/pytest-dev/pytest-mock/blob/master/LICENSE + +Keywords: pytest mock +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: Pytest +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Topic :: Software Development :: Testing diff --git a/pytest_mock.egg-info/SOURCES.txt b/pytest_mock.egg-info/SOURCES.txt new file mode 100644 index 0000000..0a429db --- /dev/null +++ b/pytest_mock.egg-info/SOURCES.txt @@ -0,0 +1,13 @@ +LICENSE +MANIFEST.in +README.rst +pytest_mock.py +setup.cfg +setup.py +test_pytest_mock.py +pytest_mock.egg-info/PKG-INFO +pytest_mock.egg-info/SOURCES.txt +pytest_mock.egg-info/dependency_links.txt +pytest_mock.egg-info/entry_points.txt +pytest_mock.egg-info/requires.txt +pytest_mock.egg-info/top_level.txt \ No newline at end of file diff --git a/pytest_mock.egg-info/dependency_links.txt b/pytest_mock.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pytest_mock.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/pytest_mock.egg-info/entry_points.txt b/pytest_mock.egg-info/entry_points.txt new file mode 100644 index 0000000..abb7b36 --- /dev/null +++ b/pytest_mock.egg-info/entry_points.txt @@ -0,0 +1,3 @@ +[pytest11] +pytest_mock = pytest_mock + diff --git a/pytest_mock.egg-info/requires.txt b/pytest_mock.egg-info/requires.txt new file mode 100644 index 0000000..79ee644 --- /dev/null +++ b/pytest_mock.egg-info/requires.txt @@ -0,0 +1,4 @@ +pytest>=2.7 + +[:python_version=="2.6" or python_version=="2.7"] +mock diff --git a/pytest_mock.egg-info/top_level.txt b/pytest_mock.egg-info/top_level.txt new file mode 100644 index 0000000..59bf78d --- /dev/null +++ b/pytest_mock.egg-info/top_level.txt @@ -0,0 +1 @@ +pytest_mock diff --git a/pytest_mock.py b/pytest_mock.py index 9f06cd6..7c8ebfc 100644 --- a/pytest_mock.py +++ b/pytest_mock.py @@ -1,14 +1,15 @@ -import sys import inspect +import sys import pytest - if sys.version_info >= (3, 3): # pragma: no cover import unittest.mock as mock_module else: import mock as mock_module +version = '1.1' + class MockFixture(object): """ @@ -18,11 +19,21 @@ class MockFixture(object): Mock = mock_module.Mock MagicMock = mock_module.MagicMock + PropertyMock = mock_module.PropertyMock + call = mock_module.call ANY = mock_module.ANY def __init__(self): self._patches = [] # list of mock._patch objects - self.patch = self._Patcher(self._patches) + self._mocks = [] # list of MagicMock objects + self.patch = self._Patcher(self._patches, self._mocks) + + def resetall(self): + """ + Call reset_mock() on all patchers started by this fixture. + """ + for m in self._mocks: + m.reset_mock() def stopall(self): """ @@ -32,6 +43,7 @@ class MockFixture(object): for p in reversed(self._patches): p.stop() self._patches[:] = [] + self._mocks[:] = [] def spy(self, obj, name): """ @@ -49,25 +61,30 @@ class MockFixture(object): # Can't use autospec classmethod or staticmethod objects # see: https://bugs.python.org/issue23078 if inspect.isclass(obj): - # bypass class descriptor: + # Bypass class descriptor: # http://stackoverflow.com/questions/14187973/python3-check-if-method-is-static - value = obj.__getattribute__(obj, name) - if isinstance(value, (classmethod, staticmethod)): - autospec = False + try: + value = obj.__getattribute__(obj, name) + except AttributeError: + pass + else: + if isinstance(value, (classmethod, staticmethod)): + autospec = False result = self.patch.object(obj, name, side_effect=method, autospec=autospec) return result - def stub(self): + def stub(self, name=None): """ Creates a stub method. It accepts any arguments. Ideal to register to callbacks in tests. + :param name: the constructed stub's name as used in repr :rtype: mock.MagicMock :return: Stub object. """ - return mock_module.MagicMock(spec=lambda *args, **kwargs: None) + return mock_module.MagicMock(spec=lambda *args, **kwargs: None, name=name) class _Patcher(object): """ @@ -75,8 +92,9 @@ class MockFixture(object): etc. We need this indirection to keep the same API of the mock package. """ - def __init__(self, patches): + def __init__(self, patches, mocks): self._patches = patches + self._mocks = mocks def _start_patch(self, mock_func, *args, **kwargs): """Patches something by calling the given function from the mock @@ -86,6 +104,7 @@ class MockFixture(object): p = mock_func(*args, **kwargs) mocked = p.start() self._patches.append(p) + self._mocks.append(mocked) return mocked def object(self, *args, **kwargs): @@ -126,3 +145,114 @@ def mock(mocker): warnings.warn('"mock" fixture has been deprecated, use "mocker" instead', DeprecationWarning) return mocker + + +_mock_module_patches = [] +_mock_module_originals = {} + + +def assert_wrapper(__wrapped_mock_method__, *args, **kwargs): + __tracebackhide__ = True + try: + __wrapped_mock_method__(*args, **kwargs) + except AssertionError as e: + __mock_self = args[0] + if __mock_self.call_args is not None: + actual_args, actual_kwargs = __mock_self.call_args + assert actual_args == args[1:] + assert actual_kwargs == kwargs + raise AssertionError(*e.args) + + +def wrap_assert_not_called(*args, **kwargs): + __tracebackhide__ = True + assert_wrapper(_mock_module_originals["assert_not_called"], + *args, **kwargs) + + +def wrap_assert_called_with(*args, **kwargs): + __tracebackhide__ = True + assert_wrapper(_mock_module_originals["assert_called_with"], + *args, **kwargs) + + +def wrap_assert_called_once_with(*args, **kwargs): + __tracebackhide__ = True + assert_wrapper(_mock_module_originals["assert_called_once_with"], + *args, **kwargs) + + +def wrap_assert_has_calls(*args, **kwargs): + __tracebackhide__ = True + assert_wrapper(_mock_module_originals["assert_has_calls"], + *args, **kwargs) + + +def wrap_assert_any_call(*args, **kwargs): + __tracebackhide__ = True + assert_wrapper(_mock_module_originals["assert_any_call"], + *args, **kwargs) + + +def wrap_assert_methods(config): + """ + Wrap assert methods of mock module so we can hide their traceback and + add introspection information to specified argument asserts. + """ + # Make sure we only do this once + if _mock_module_originals: + return + + wrappers = { + 'assert_not_called': wrap_assert_not_called, + 'assert_called_with': wrap_assert_called_with, + 'assert_called_once_with': wrap_assert_called_once_with, + 'assert_has_calls': wrap_assert_has_calls, + 'assert_any_call': wrap_assert_any_call, + } + for method, wrapper in wrappers.items(): + try: + original = getattr(mock_module.NonCallableMock, method) + except AttributeError: + continue + _mock_module_originals[method] = original + patcher = mock_module.patch.object( + mock_module.NonCallableMock, method, wrapper) + patcher.start() + _mock_module_patches.append(patcher) + + if hasattr(config, 'add_cleanup'): + add_cleanup = config.add_cleanup + else: + # pytest 2.7 compatibility + add_cleanup = config._cleanup.append + add_cleanup(unwrap_assert_methods) + + +def unwrap_assert_methods(): + for patcher in _mock_module_patches: + patcher.stop() + _mock_module_patches[:] = [] + _mock_module_originals.clear() + + +def pytest_addoption(parser): + parser.addini('mock_traceback_monkeypatch', + 'Monkeypatch the mock library to improve reporting of the ' + 'assert_called_... methods', + default=True) + + +def parse_ini_boolean(value): + if value in (True, False): + return value + try: + return {'true': True, 'false': False}[value.lower()] + except KeyError: + raise ValueError('unknown string for bool: %r' % value) + + +def pytest_configure(config): + tb = config.getoption('--tb') + if parse_ini_boolean(config.getini('mock_traceback_monkeypatch')) and tb != 'native': + wrap_assert_methods(config) diff --git a/setup.py b/setup.py index 3ca9b55..6a8e126 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,43 @@ +import re + from setuptools import setup +with open('pytest_mock.py') as f: + m = re.search("version = '(.*)'", f.read()) + assert m is not None + version = m.group(1) setup( name='pytest-mock', - version='0.8.1', + version=version, entry_points={ 'pytest11': ['pytest_mock = pytest_mock'], }, py_modules=['pytest_mock'], platforms='any', install_requires=[ - 'pytest>=2.4', + 'pytest>=2.7', ], extras_require={ ':python_version=="2.6" or python_version=="2.7"': ['mock'], }, url='https://github.com/pytest-dev/pytest-mock/', - license='LGPL', + license='MIT', author='Bruno Oliveira', author_email='nicoddemus@gmail.com', description='Thin-wrapper around the mock package for easier use with py.test', long_description=open('README.rst').read(), keywords="pytest mock", classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable', + 'Framework :: Pytest', 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', + 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Topic :: Software Development :: Testing', diff --git a/test_pytest_mock.py b/test_pytest_mock.py index 7caf17d..3980462 100644 --- a/test_pytest_mock.py +++ b/test_pytest_mock.py @@ -1,9 +1,12 @@ import os import platform +import sys +from contextlib import contextmanager + +import py.code import pytest - pytest_plugins = 'pytester' # could not make some of the tests work on PyPy, patches are welcome! @@ -129,28 +132,58 @@ def test_deprecated_mock(mock, tmpdir): assert os.listdir(str(tmpdir)) == [] -def test_mocker_has_magic_mock_class_as_attribute_for_instantiation(): +@pytest.mark.parametrize('name', ['MagicMock', 'PropertyMock', 'Mock', 'call', 'ANY']) +def test_mocker_aliases(name): from pytest_mock import mock_module, MockFixture mocker = MockFixture() - assert isinstance(mocker.MagicMock(), mock_module.MagicMock) + assert getattr(mocker, name) is getattr(mock_module, name) -def test_mocker_has_mock_class_as_attribute_for_instantiation(): - from pytest_mock import mock_module, MockFixture +def test_mocker_resetall(mocker): + listdir = mocker.patch('os.listdir') + open = mocker.patch('os.open') - mocker = MockFixture() - assert isinstance(mocker.Mock(), mock_module.Mock) + listdir("/tmp") + open("/tmp/foo.txt") + listdir.assert_called_once_with("/tmp") + open.assert_called_once_with("/tmp/foo.txt") + + mocker.resetall() + + assert not listdir.called + assert not open.called -def test_mocker_stub(mocker): - def foo(on_something): - on_something('foo', 'bar') +class TestMockerStub: + def test_call(self, mocker): + stub = mocker.stub() + stub('foo', 'bar') + stub.assert_called_once_with('foo', 'bar') - stub = mocker.stub() + def test_repr_with_no_name(self, mocker): + stub = mocker.stub() + assert not 'name' in repr(stub) - foo(stub) - stub.assert_called_once_with('foo', 'bar') + def test_repr_with_name(self, mocker): + test_name = 'funny walk' + stub = mocker.stub(name=test_name) + assert "name={0!r}".format(test_name) in repr(stub) + + def __test_failure_message(self, mocker, **kwargs): + expected_name = kwargs.get('name') or 'mock' + expected_message = 'Expected call: {0}()\nNot called'.format(expected_name) + stub = mocker.stub(**kwargs) + with pytest.raises(AssertionError) as exc_info: + stub.assert_called_with() + assert exc_info.value.msg == expected_message + + def test_failure_message_with_no_name(self, mocker): + self.__test_failure_message(mocker) + + @pytest.mark.parametrize('name', (None, '', 'f', 'The Castle of aaarrrrggh')) + def test_failure_message_with_name(self, mocker, name): + self.__test_failure_message(mocker, name=name) def test_instance_method_spy(mocker): @@ -186,6 +219,27 @@ def test_instance_method_by_class_spy(mocker): assert spy.call_args_list == calls +@skip_pypy +def test_instance_method_by_subclass_spy(mocker): + from pytest_mock import mock_module + + class Base(object): + + def bar(self, arg): + return arg * 2 + + class Foo(Base): + pass + + spy = mocker.spy(Foo, 'bar') + foo = Foo() + other = Foo() + assert foo.bar(arg=10) == 20 + assert other.bar(arg=10) == 20 + calls = [mock_module.call(foo, arg=10), mock_module.call(other, arg=10)] + assert spy.call_args_list == calls + + @skip_pypy def test_class_method_spy(mocker): class Foo(object): @@ -200,6 +254,24 @@ def test_class_method_spy(mocker): spy.assert_called_once_with(arg=10) +@skip_pypy +@pytest.mark.xfail(sys.version_info[0] == 2, reason='does not work on Python 2') +def test_class_method_subclass_spy(mocker): + class Base(object): + + @classmethod + def bar(self, arg): + return arg * 2 + + class Foo(Base): + pass + + spy = mocker.spy(Foo, 'bar') + assert Foo.bar(arg=10) == 20 + Foo.bar.assert_called_once_with(arg=10) + spy.assert_called_once_with(arg=10) + + @skip_pypy def test_class_method_with_metaclass_spy(mocker): class MetaFoo(type): @@ -231,3 +303,198 @@ def test_static_method_spy(mocker): assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) spy.assert_called_once_with(arg=10) + + +@skip_pypy +@pytest.mark.xfail(sys.version_info[0] == 2, reason='does not work on Python 2') +def test_static_method_subclass_spy(mocker): + class Base(object): + + @staticmethod + def bar(arg): + return arg * 2 + + class Foo(Base): + pass + + spy = mocker.spy(Foo, 'bar') + assert Foo.bar(arg=10) == 20 + Foo.bar.assert_called_once_with(arg=10) + spy.assert_called_once_with(arg=10) + + +@contextmanager +def assert_traceback(): + """ + Assert that this file is at the top of the filtered traceback + """ + try: + yield + except AssertionError: + traceback = py.code.ExceptionInfo().traceback + crashentry = traceback.getcrashentry() + assert crashentry.path == __file__ + else: + raise AssertionError("DID NOT RAISE") + + +@contextmanager +def assert_argument_introspection(left, right): + """ + Assert detailed argument introspection is used + """ + try: + yield + except AssertionError as e: + # this may be a bit too assuming, but seems nicer then hard-coding + import _pytest.assertion.util as util + # NOTE: we assert with either verbose or not, depending on how our own + # test was run by examining sys.argv + verbose = any(a.startswith('-v') for a in sys.argv) + expected = '\n '.join(util._compare_eq_iterable(left, right, verbose)) + assert expected in e.msg + else: + raise AssertionError("DID NOT RAISE") + + +@pytest.mark.skipif(sys.version_info[:2] in [(3, 3), (3, 4)], + reason="assert_not_called not available in python 3.3 and 3.4") +def test_assert_not_called_wrapper(mocker): + stub = mocker.stub() + stub.assert_not_called() + stub() + with assert_traceback(): + stub.assert_not_called() + + +def test_assert_called_with_wrapper(mocker): + stub = mocker.stub() + stub("foo") + stub.assert_called_with("foo") + with assert_traceback(): + stub.assert_called_with("bar") + + +def test_assert_called_once_with_wrapper(mocker): + stub = mocker.stub() + stub("foo") + stub.assert_called_once_with("foo") + stub("foo") + with assert_traceback(): + stub.assert_called_once_with("foo") + + +def test_assert_called_args_with_introspection(mocker): + stub = mocker.stub() + + complex_args = ('a', 1, set(['test'])) + wrong_args = ('b', 2, set(['jest'])) + + stub(*complex_args) + stub.assert_called_with(*complex_args) + stub.assert_called_once_with(*complex_args) + + with assert_argument_introspection(complex_args, wrong_args): + stub.assert_called_with(*wrong_args) + stub.assert_called_once_with(*wrong_args) + + +def test_assert_called_kwargs_with_introspection(mocker): + stub = mocker.stub() + + complex_kwargs = dict(foo={'bar': 1, 'baz': 'spam'}) + wrong_kwargs = dict(foo={'goo': 1, 'baz': 'bran'}) + + stub(**complex_kwargs) + stub.assert_called_with(**complex_kwargs) + stub.assert_called_once_with(**complex_kwargs) + + with assert_argument_introspection(complex_kwargs, wrong_kwargs): + stub.assert_called_with(**wrong_kwargs) + stub.assert_called_once_with(**wrong_kwargs) + + +def test_assert_any_call_wrapper(mocker): + stub = mocker.stub() + stub("foo") + stub("foo") + stub.assert_any_call("foo") + with assert_traceback(): + stub.assert_any_call("bar") + + +def test_assert_has_calls(mocker): + from pytest_mock import mock_module + stub = mocker.stub() + stub("foo") + stub.assert_has_calls([mock_module.call("foo")]) + with assert_traceback(): + stub.assert_has_calls([mock_module.call("bar")]) + + +def test_monkeypatch_ini(mocker, testdir): + # Make sure the following function actually tests something + stub = mocker.stub() + assert stub.assert_called_with.__module__ != stub.__module__ + + testdir.makepyfile(""" + import py.code + def test_foo(mocker): + stub = mocker.stub() + assert stub.assert_called_with.__module__ == stub.__module__ + """) + testdir.makeini(""" + [pytest] + mock_traceback_monkeypatch = false + """) + if hasattr(testdir, 'runpytest_subprocess'): + result = testdir.runpytest_subprocess() + else: + # pytest 2.7.X + result = testdir.runpytest() + assert result.ret == 0 + + +def test_parse_ini_boolean(testdir): + import pytest_mock + assert pytest_mock.parse_ini_boolean('True') is True + assert pytest_mock.parse_ini_boolean('false') is False + with pytest.raises(ValueError): + pytest_mock.parse_ini_boolean('foo') + + +def test_patched_method_parameter_name(mocker): + """Test that our internal code uses uncommon names when wrapping other + "mock" methods to avoid conflicts with user code (#31). + """ + + class Request: + @classmethod + def request(cls, method, args): + pass + + m = mocker.patch.object(Request, 'request') + Request.request(method='get', args={'type': 'application/json'}) + m.assert_called_once_with(method='get', args={'type': 'application/json'}) + + +def test_monkeypatch_native(testdir): + """Automatically disable monkeypatching when --tb=native. + """ + testdir.makepyfile(""" + def test_foo(mocker): + stub = mocker.stub() + stub(1, greet='hello') + stub.assert_called_once_with(1, greet='hey') + """) + if hasattr(testdir, 'runpytest_subprocess'): + result = testdir.runpytest_subprocess('--tb=native') + else: + # pytest 2.7.X + result = testdir.runpytest('--tb=native') + assert result.ret == 1 + assert 'During handling of the above exception' not in result.stdout.str() + assert 'Differing items:' not in result.stdout.str() + traceback_lines = [x for x in result.stdout.str().splitlines() + if 'Traceback (most recent call last)' in x] + assert len(traceback_lines) == 1 # make sure there are no duplicated tracebacks (#44)