Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/sentry/testutils/pytest/mocking.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
51 changes: 50 additions & 1 deletion tests/sentry/testutils/pytest/mocking/test_mocking.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
Loading