Skip to content

Commit

Permalink
Change spy attributes to 'spy_return' and 'spy_exception' (#177)
Browse files Browse the repository at this point in the history
Change spy attributes to 'spy_return' and 'spy_exception'
  • Loading branch information
nicoddemus committed Jan 4, 2020
2 parents 7bddcd5 + ccb76e5 commit 67357f1
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 82 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.rst
@@ -1,3 +1,23 @@
2.0.0 (2020-01-04)
------------------

Breaking Changes
++++++++++++++++

* ``mocker.spy`` attributes for tracking returned values and raised exceptions of its spied functions
are now called ``spy_return`` and ``spy_exception``, instead of reusing the existing
``MagicMock`` attributes ``return_value`` and ``side_effect``.

Version ``1.13`` introduced a serious regression: after a spied function using ``mocker.spy``
raises an exception, further calls to the spy will not call the spied function,
always raising the first exception instead: assigning to ``side_effect`` causes
``unittest.mock`` to behave this way (`#175`_).

* The deprecated ``mock`` alias to the ``mocker`` fixture has finally been removed.

.. _#175: https://github.com/pytest-dev/pytest-mock/issues/175


1.13.0 (2019-12-05)
-------------------

Expand Down
54 changes: 34 additions & 20 deletions README.rst
Expand Up @@ -2,10 +2,8 @@
pytest-mock
===========

This plugin installs a ``mocker`` fixture which is a thin-wrapper around the patching API
provided by the `mock package <http://pypi.python.org/pypi/mock>`_,
but with the benefit of not having to worry about undoing patches at the end
of a test:
This plugin provides a ``mocker`` fixture which is a thin-wrapper around the patching API
provided by the `mock package <http://pypi.python.org/pypi/mock>`_:

.. code-block:: python
Expand All @@ -23,6 +21,9 @@ of a test:
os.remove.assert_called_once_with('file')
Besides undoing the mocking automatically after the end of the test, it also provides other
nice utilities such as ``spy`` and ``stub``, and uses pytest introspection when
comparing calls.

|python| |version| |anaconda| |ci| |coverage| |black|

Expand Down Expand Up @@ -70,7 +71,7 @@ The supported methods are:
* `mocker.stopall <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch.stopall>`_
* ``mocker.resetall()``: calls `reset_mock() <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.reset_mock>`_ in all mocked objects up to this point.

These objects from the ``mock`` module are accessible directly from ``mocker`` for convenience:
Also, as a convenience, these names from the ``mock`` module are accessible directly from ``mocker``:

* `Mock <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock>`_
* `MagicMock <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock>`_
Expand All @@ -85,33 +86,47 @@ These objects from the ``mock`` module are accessible directly from ``mocker`` f
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.
The ``mocker.spy`` object acts exactly like the original method in all cases, except the spy
also tracks method calls, return values and exceptions raised.

.. code-block:: python
def test_spy(mocker):
class Foo(object):
def bar(self):
return 42
def bar(self, v):
return v * 2
foo = Foo()
mocker.spy(foo, 'bar')
assert foo.bar() == 42
assert foo.bar.call_count == 1
spy = mocker.spy(foo, 'bar')
assert foo.bar(21) == 42
Since version ``1.11``, it is also possible to query the ``return_value`` attribute
to observe what the spied function/method returned.
spy.assert_called_once_with(21)
assert spy.spy_return == 42
Since version ``1.13``, it is also possible to query the ``side_effect`` attribute
to observe any exception thrown by the spied function/method.
The object returned by ``mocker.spy`` is a ``MagicMock`` object, so all standard checking functions
are available (like ``assert_called_once_with`` in the example above).

In addition, spy objects contain two extra attributes:

* ``spy_return``: contains the returned value of the spied function.
* ``spy_exception``: contain the last exception value raised by the spied function/method when
it was last called, or ``None`` if no exception was raised.

``mocker.spy`` also works for class and static methods.

.. note::

In versions earlier than ``2.0``, the attributes were called ``return_value`` and
``side_effect`` respectively, but due to incompatibilities with ``unittest.mock``
they had to be renamed (see `#175`_ for details).

.. _#175: https://github.com/pytest-dev/pytest-mock/issues/175

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).
The stub is a mock object that accepts any arguments and is useful to test callbacks.
It may receive an optional name that is shown in its ``repr``, useful for debugging.

.. code-block:: python
Expand All @@ -128,7 +143,6 @@ May be passed a name to be used by the constructed stub object in its repr (usef
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.
Expand Down
25 changes: 8 additions & 17 deletions src/pytest_mock/plugin.py
Expand Up @@ -113,17 +113,21 @@ def spy(self, obj, name):

@w
def wrapper(*args, **kwargs):
spy_obj.spy_return = None
spy_obj.spy_exception = None
try:
r = method(*args, **kwargs)
except Exception as e:
result.side_effect = e
spy_obj.spy_exception = e
raise
else:
result.return_value = r
spy_obj.spy_return = r
return r

result = self.patch.object(obj, name, side_effect=wrapper, autospec=autospec)
return result
spy_obj = self.patch.object(obj, name, side_effect=wrapper, autospec=autospec)
spy_obj.spy_return = None
spy_obj.spy_exception = None
return spy_obj

def stub(self, name=None):
"""
Expand Down Expand Up @@ -204,19 +208,6 @@ def mocker(pytestconfig):
result.stopall()


@pytest.fixture
def mock(mocker):
"""
Same as "mocker", but kept only for backward compatibility.
"""
import warnings

warnings.warn(
'"mock" fixture has been deprecated, use "mocker" instead', DeprecationWarning
)
return mocker


_mock_module_patches = []
_mock_module_originals = {}

Expand Down
92 changes: 47 additions & 45 deletions tests/test_pytest_mock.py
Expand Up @@ -132,28 +132,6 @@ def test_mock_patch_dict_resetall(mocker):
assert x == {"new": 10}


def test_deprecated_mock(testdir):
"""
Use backward-compatibility-only mock fixture to ensure complete coverage.
"""
p1 = testdir.makepyfile(
"""
import os
def test(mock, tmpdir):
mock.patch("os.listdir", return_value=["mocked"])
assert os.listdir(str(tmpdir)) == ["mocked"]
mock.stopall()
assert os.listdir(str(tmpdir)) == []
"""
)
result = testdir.runpytest(str(p1))
result.stdout.fnmatch_lines(
['*DeprecationWarning: "mock" fixture has been deprecated, use "mocker"*']
)
assert result.ret == 0


@pytest.mark.parametrize(
"name",
[
Expand Down Expand Up @@ -238,28 +216,52 @@ def bar(self, arg):
assert foo.bar(arg=10) == 20
assert other.bar(arg=10) == 20
foo.bar.assert_called_once_with(arg=10)
assert foo.bar.return_value == 20
assert foo.bar.spy_return == 20
spy.assert_called_once_with(arg=10)
assert spy.return_value == 20
assert spy.spy_return == 20


def test_instance_method_spy_exception(mocker):
excepted_message = "foo"

class Foo(object):
def bar(self, arg):
raise Exception(excepted_message)
raise Exception("Error with {}".format(arg))

foo = Foo()
other = Foo()
spy = mocker.spy(foo, "bar")

with pytest.raises(Exception) as exc_info:
foo.bar(10)
assert str(exc_info.value) == excepted_message
expected_calls = []
for i, v in enumerate([10, 20]):
with pytest.raises(Exception, match="Error with {}".format(v)) as exc_info:
foo.bar(arg=v)

foo.bar.assert_called_once_with(arg=10)
assert spy.side_effect == exc_info.value
expected_calls.append(mocker.call(arg=v))
assert foo.bar.call_args_list == expected_calls
assert str(spy.spy_exception) == "Error with {}".format(v)


def test_spy_reset(mocker):
class Foo(object):
def bar(self, x):
if x == 0:
raise ValueError("invalid x")
return x * 3

spy = mocker.spy(Foo, "bar")
assert spy.spy_return is None
assert spy.spy_exception is None

Foo().bar(10)
assert spy.spy_return == 30
assert spy.spy_exception is None

with pytest.raises(ValueError):
Foo().bar(0)
assert spy.spy_return is None
assert str(spy.spy_exception) == "invalid x"

Foo().bar(15)
assert spy.spy_return == 45
assert spy.spy_exception is None


@skip_pypy
Expand Down Expand Up @@ -293,7 +295,7 @@ class Foo(Base):
assert other.bar(arg=10) == 20
calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)]
assert spy.call_args_list == calls
assert spy.return_value == 20
assert spy.spy_return == 20


@skip_pypy
Expand All @@ -306,9 +308,9 @@ def bar(cls, arg):
spy = mocker.spy(Foo, "bar")
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10)
assert Foo.bar.return_value == 20
assert Foo.bar.spy_return == 20
spy.assert_called_once_with(arg=10)
assert spy.return_value == 20
assert spy.spy_return == 20


@skip_pypy
Expand All @@ -325,9 +327,9 @@ class Foo(Base):
spy = mocker.spy(Foo, "bar")
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10)
assert Foo.bar.return_value == 20
assert Foo.bar.spy_return == 20
spy.assert_called_once_with(arg=10)
assert spy.return_value == 20
assert spy.spy_return == 20


@skip_pypy
Expand All @@ -346,9 +348,9 @@ def bar(cls, arg):
spy = mocker.spy(Foo, "bar")
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10)
assert Foo.bar.return_value == 20
assert Foo.bar.spy_return == 20
spy.assert_called_once_with(arg=10)
assert spy.return_value == 20
assert spy.spy_return == 20


@skip_pypy
Expand All @@ -361,9 +363,9 @@ def bar(arg):
spy = mocker.spy(Foo, "bar")
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10)
assert Foo.bar.return_value == 20
assert Foo.bar.spy_return == 20
spy.assert_called_once_with(arg=10)
assert spy.return_value == 20
assert spy.spy_return == 20


@skip_pypy
Expand All @@ -380,9 +382,9 @@ class Foo(Base):
spy = mocker.spy(Foo, "bar")
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10)
assert Foo.bar.return_value == 20
assert Foo.bar.spy_return == 20
spy.assert_called_once_with(arg=10)
assert spy.return_value == 20
assert spy.spy_return == 20


def test_callable_like_spy(testdir, mocker):
Expand All @@ -402,7 +404,7 @@ def __call__(self, x):
spy = mocker.spy(uut, "call_like")
uut.call_like(10)
spy.assert_called_once_with(10)
assert spy.return_value == 20
assert spy.spy_return == 20


@contextmanager
Expand Down

0 comments on commit 67357f1

Please sign in to comment.