Skip to content

Commit

Permalink
Change spy attributes to 'spy_return' and 'spy_exception'
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Jan 4, 2020
1 parent 7bddcd5 commit fd02462
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 38 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.rst
@@ -1,3 +1,21 @@
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`_).

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


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

Expand Down
37 changes: 26 additions & 11 deletions README.rst
Expand Up @@ -85,26 +85,41 @@ 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 spy 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.

It 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
----
Expand Down
12 changes: 8 additions & 4 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
70 changes: 47 additions & 23 deletions tests/test_pytest_mock.py
Expand Up @@ -238,28 +238,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 +317,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 +330,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 +349,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 +370,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 +385,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 +404,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 +426,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 fd02462

Please sign in to comment.