diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ab7cf60..6ba2d56 100644 --- a/CHANGELOG.rst +++ b/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) ------------------- diff --git a/README.rst b/README.rst index bd1ccc2..452411a 100644 --- a/README.rst +++ b/README.rst @@ -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 ---- diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 470072b..dad8465 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -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): """ diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index 04261f5..0ba2a2a 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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): @@ -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