From 06a4a9221d8210f0a55a5a5742f590052f7e89cf Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 24 Nov 2025 14:58:52 -0800 Subject: [PATCH 1/2] add `count_matching_calls` helper --- src/sentry/testutils/pytest/mocking.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/sentry/testutils/pytest/mocking.py b/src/sentry/testutils/pytest/mocking.py index b8f3c3471e8f2f..77fc22f636cacc 100644 --- a/src/sentry/testutils/pytest/mocking.py +++ b/src/sentry/testutils/pytest/mocking.py @@ -1,7 +1,9 @@ from __future__ import annotations from collections.abc import Callable -from typing import ParamSpec, TypeVar +from typing import Any, ParamSpec, TypeVar +from unittest.mock import MagicMock +from unittest.mock import call as MockCall # TODO: Once we're on python 3.12, we can get rid of these and change the first line of the # signature of `capture_results` to @@ -122,3 +124,14 @@ def wrapped_fn(*args: P.args, **kwargs: P.kwargs) -> T: return returned_value return wrapped_fn + + +def count_matching_calls(mock_fn: MagicMock, *args: Any, **kwargs: Any) -> int: + """ + Given a mock function, count the calls which match the given args and kwargs. + + Note: As is the case with the built-in mock methods `assert_called_with` and friends, the given + args and kwargs must match the full list of what was passed to the given mock. + """ + matching_calls = [call for call in mock_fn.call_args_list if call == MockCall(*args, **kwargs)] + return len(matching_calls) From 7ee3f9249d8ae899e52d9f61bf1341c820555c45 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 24 Nov 2025 14:58:52 -0800 Subject: [PATCH 2/2] add tests --- .../testutils/pytest/mocking/test_mocking.py | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/sentry/testutils/pytest/mocking/test_mocking.py b/tests/sentry/testutils/pytest/mocking/test_mocking.py index bbcc9635be8d5a..018bc4dfe93d5b 100644 --- a/tests/sentry/testutils/pytest/mocking/test_mocking.py +++ b/tests/sentry/testutils/pytest/mocking/test_mocking.py @@ -1,7 +1,8 @@ from typing import Any from unittest import TestCase, mock +from unittest.mock import MagicMock -from sentry.testutils.pytest.mocking import capture_results +from sentry.testutils.pytest.mocking import capture_results, count_matching_calls from tests.sentry.testutils.pytest.mocking.animals import ( a_function_that_calls_erroring_get_dog, a_function_that_calls_get_cat, @@ -68,3 +69,51 @@ def test_records_thrown_exception(self) -> None: error_message = result.args[0] assert error_message == "Expected dog, but got cat instead." + + +class MockCallCountingTest(TestCase): + def test_no_args_no_kwargs_matching(self) -> None: + describe_dogs = MagicMock() + # Call the function more than once to show it's not just the total number of calls being + # counted, and call it with something else second, to show it's not just looking at the most + # recent call + describe_dogs() + describe_dogs("maisey") + + assert count_matching_calls(describe_dogs) == 1 + + def test_arg_matching(self) -> None: + describe_dogs = MagicMock() + describe_dogs("maisey") + describe_dogs("charlie") + describe_dogs("maisey") + describe_dogs("maisey", "charlie") + + assert count_matching_calls(describe_dogs, "maisey") == 2 + assert count_matching_calls(describe_dogs, "charlie") == 1 + assert count_matching_calls(describe_dogs, "maisey", "charlie") == 1 + + def test_kwarg_matching(self) -> None: + describe_dogs = MagicMock() + describe_dogs(number_1_dog="maisey") + describe_dogs(number_1_dog="charlie") + describe_dogs(number_1_dog="maisey") + describe_dogs(numer_1_dog="maisey", co_number_1_dog="charlie") + + assert count_matching_calls(describe_dogs, number_1_dog="maisey") == 2 + assert count_matching_calls(describe_dogs, number_1_dog="charlie") == 1 + assert ( + count_matching_calls(describe_dogs, numer_1_dog="maisey", co_number_1_dog="charlie") + == 1 + ) + + def test_mixed_matching(self) -> None: + describe_dogs = MagicMock() + describe_dogs("maisey", is_number_1_dog=True) + describe_dogs("charlie", is_number_1_dog=True) + describe_dogs("maisey", is_number_1_dog=True) + describe_dogs("maisey", "charlie", co_number_1_dogs=True) + + assert count_matching_calls(describe_dogs, "maisey", is_number_1_dog=True) == 2 + assert count_matching_calls(describe_dogs, "charlie", is_number_1_dog=True) == 1 + assert count_matching_calls(describe_dogs, "maisey", "charlie", co_number_1_dogs=True) == 1