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 3443d1c
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 38 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.rst
@@ -1,3 +1,23 @@
2.0.0 (2020-01-04)
------------------

**Breaking change**: after a spied function using ``mocker.spy`` is called
and raises an exception, calls to the spy will not call the spied function from then on,
with the original exception always being raised instead.

In previous versions, a ``mocker.spy`` would track the return value and exceptions raised in the
``return_value`` and ``side_effect`` properties. Unfortunately this behavior breaks internal assumptions of
the ``unittest.mock`` module.

The best solution devised was to make a breaking release: now spies will track return values and exceptions
raised in the ``spy_returned_value`` and ``spy_exception_raised`` attributes to avoid any conflicts with
``unittest.mock``.

More details on `#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 3443d1c

Please sign in to comment.