Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change spy attributes to 'spy_return' and 'spy_exception' #177

Merged
merged 2 commits into from
Jan 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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