diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 14a5379..242a6e7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,12 +2,16 @@ Releases ======== UNRELEASED -------------------- +---------- + +* Fixed introspection for failed ``assert_has_calls`` (`#365`_). * Updated type annotations for ``mocker.patch`` and ``mocker.spy`` (`#364`_). +.. _#365: https://github.com/pytest-dev/pytest-mock/pull/365 .. _#364: https://github.com/pytest-dev/pytest-mock/pull/364 + 3.10.0 (2022-10-05) ------------------- diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 6699f4b..4e9609a 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -473,6 +473,54 @@ def assert_wrapper( raise e +def assert_has_calls_wrapper( + __wrapped_mock_method__: Callable[..., Any], *args: Any, **kwargs: Any +) -> None: + __tracebackhide__ = True + try: + __wrapped_mock_method__(*args, **kwargs) + return + except AssertionError as e: + any_order = kwargs.get("any_order", False) + if getattr(e, "_mock_introspection_applied", 0) or any_order: + msg = str(e) + else: + __mock_self = args[0] + msg = str(e) + if __mock_self.call_args_list is not None: + actual_calls = list(__mock_self.call_args_list) + expect_calls = args[1] + introspection = "" + from itertools import zip_longest + + for actual_call, expect_call in zip_longest(actual_calls, expect_calls): + if actual_call is not None: + actual_args, actual_kwargs = actual_call + else: + actual_args = tuple() + actual_kwargs = {} + + if expect_call is not None: + _, expect_args, expect_kwargs = expect_call + else: + expect_args = tuple() + expect_kwargs = {} + + try: + assert actual_args == expect_args + except AssertionError as e_args: + introspection += "\nArgs:\n" + str(e_args) + try: + assert actual_kwargs == expect_kwargs + except AssertionError as e_kwargs: + introspection += "\nKwargs:\n" + str(e_kwargs) + if introspection: + msg += "\n\npytest introspection follows:\n" + introspection + e = AssertionError(msg) + e._mock_introspection_applied = True # type:ignore[attr-defined] + raise e + + def wrap_assert_not_called(*args: Any, **kwargs: Any) -> None: __tracebackhide__ = True assert_wrapper(_mock_module_originals["assert_not_called"], *args, **kwargs) @@ -495,7 +543,9 @@ def wrap_assert_called_once_with(*args: Any, **kwargs: Any) -> None: def wrap_assert_has_calls(*args: Any, **kwargs: Any) -> None: __tracebackhide__ = True - assert_wrapper(_mock_module_originals["assert_has_calls"], *args, **kwargs) + assert_has_calls_wrapper( + _mock_module_originals["assert_has_calls"], *args, **kwargs + ) def wrap_assert_any_call(*args: Any, **kwargs: Any) -> None: diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index d3f8ac6..3d53241 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -630,6 +630,86 @@ def test_assert_has_calls(mocker: MockerFixture) -> None: stub.assert_has_calls([mocker.call("bar")]) +def test_assert_has_calls_multiple_calls(mocker: MockerFixture) -> None: + stub = mocker.stub() + stub("foo") + stub("bar") + stub("baz") + stub.assert_has_calls([mocker.call("foo"), mocker.call("bar"), mocker.call("baz")]) + with assert_traceback(): + stub.assert_has_calls( + [ + mocker.call("foo"), + mocker.call("bar"), + mocker.call("baz"), + mocker.call("bat"), + ] + ) + with assert_traceback(): + stub.assert_has_calls( + [mocker.call("foo"), mocker.call("baz"), mocker.call("bar")] + ) + + +def test_assert_has_calls_multiple_calls_subset(mocker: MockerFixture) -> None: + stub = mocker.stub() + stub("foo") + stub("bar") + stub("baz") + stub.assert_has_calls([mocker.call("bar"), mocker.call("baz")]) + with assert_traceback(): + stub.assert_has_calls([mocker.call("foo"), mocker.call("baz")]) + with assert_traceback(): + stub.assert_has_calls( + [mocker.call("foo"), mocker.call("bar"), mocker.call("bat")] + ) + with assert_traceback(): + stub.assert_has_calls([mocker.call("baz"), mocker.call("bar")]) + + +def test_assert_has_calls_multiple_calls_any_order(mocker: MockerFixture) -> None: + stub = mocker.stub() + stub("foo") + stub("bar") + stub("baz") + stub.assert_has_calls( + [mocker.call("foo"), mocker.call("baz"), mocker.call("bar")], any_order=True + ) + with assert_traceback(): + stub.assert_has_calls( + [ + mocker.call("foo"), + mocker.call("baz"), + mocker.call("bar"), + mocker.call("bat"), + ], + any_order=True, + ) + + +def test_assert_has_calls_multiple_calls_any_order_subset( + mocker: MockerFixture, +) -> None: + stub = mocker.stub() + stub("foo") + stub("bar") + stub("baz") + stub.assert_has_calls([mocker.call("baz"), mocker.call("foo")], any_order=True) + with assert_traceback(): + stub.assert_has_calls( + [mocker.call("baz"), mocker.call("foo"), mocker.call("bat")], any_order=True + ) + + +def test_assert_has_calls_no_calls( + mocker: MockerFixture, +) -> None: + stub = mocker.stub() + stub.assert_has_calls([]) + with assert_traceback(): + stub.assert_has_calls([mocker.call("foo")]) + + def test_monkeypatch_ini(testdir: Any, mocker: MockerFixture) -> None: # Make sure the following function actually tests something stub = mocker.stub()