diff --git a/tests/helpers.py b/tests/helpers.py index d21d6c6..161b793 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio import unittest.mock -from typing import Any, Generic, TYPE_CHECKING, TypeVar +from typing import Any, ClassVar, Generic, TYPE_CHECKING, TypeVar from typing_extensions import ParamSpec @@ -70,7 +70,7 @@ class UnpropagatingMockMixin(Generic[T_Mock]): _mock_sealed: bool _extract_mock_name: Callable[[], str] - def _get_child_mock(self, **kwargs) -> T_Mock: + def _get_child_mock(self, **kwargs: object) -> T_Mock: """Make :attr:`.child_mock_type`` instances instead of instances of the same class. By default, this method creates a new mock instance of the same original class, and passes @@ -84,12 +84,12 @@ class UnpropagatingMockMixin(Generic[T_Mock]): obj_name = f"{mock_name}.{kwargs['name']}" if "name" in kwargs else f"{mock_name}()" raise AttributeError(f"Can't access {obj_name}, mock is sealed.") - # Propagate any other children as simple `unittest.mock.Mock` instances + # Propagate any other children as the `child_mock_type` instances # rather than `self.__class__` instances return self.child_mock_type(**kwargs) -class CustomMockMixin(UnpropagatingMockMixin): +class CustomMockMixin(UnpropagatingMockMixin[T_Mock], Generic[T_Mock]): """Provides common functionality for our custom mock types. * Stops propagation of same ``spec_set`` restricted mock in child mocks @@ -97,9 +97,16 @@ class CustomMockMixin(UnpropagatingMockMixin): * Allows using the ``spec_set`` attribute as class attribute """ - spec_set = None + spec_set: ClassVar[object] = None - def __init__(self, **kwargs): + def __init__(self, **kwargs: object): + # If `spec_set` is explicitly passed, have it take precedence over the class attribute. + # + # Although this is an edge case, and there usually shouldn't be a need for this. + # This is mostly for the sake of completeness, and to allow for more flexibility. if "spec_set" in kwargs: - self.spec_set = kwargs.pop("spec_set") - super().__init__(spec_set=self.spec_set, **kwargs) # type: ignore # Mixin class, this __init__ is valid + spec_set = kwargs.pop("spec_set") + else: + spec_set = self.spec_set + + super().__init__(spec_set=spec_set, **kwargs) # pyright: ignore[reportCallIssue] # Mixin class, this __init__ is valid diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..194cf22 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,58 @@ +from unittest.mock import MagicMock, Mock + +from tests.helpers import CustomMockMixin, UnpropagatingMockMixin, synchronize + + +def test_synchronize(): + """Test the :func:`synchronize` helper function.""" + + async def test_func(x: int) -> int: + if x == 5: + return 10 + return 0 + + assert synchronize(test_func)(5) == 10 + assert synchronize(test_func)(6) == 0 + + +def test_unpropagating_mock_mixin(): + """Test the :class:`UnpropagatingMockMixin` helper class. + + Mocks that inherit from this mixin should not propagate themselves when new attributes are accessed. + Instead, a general mock (of the generic type) should be returned. By default, this is a :class:`MagicMock`. + """ + + class MyUnpropagatingMock(UnpropagatingMockMixin[MagicMock], Mock): ... + + x = MyUnpropagatingMock(spec_set=str) + + # Test that the `spec_set` works as expected + assert hasattr(x, "removesuffix") + assert not hasattr(x, "notastringmethod") + + unpropagated_mock = x.removesuffix() + + # Test that the resulting mock behaves without spec_set + assert hasattr(unpropagated_mock, "removesuffix") + assert hasattr(unpropagated_mock, "notastringmethod") + + +def test_custom_mock_mixin(): + """Test the :class:`CustomMockMixin` helper class. + + This class is very similar to :class:`UnpropagatingMockMixin`, with the only difference being that it + supports setting ``spec_set`` as a class variable. + """ + + class MyCustomMock(CustomMockMixin[MagicMock], Mock): # pyright: ignore[reportUnsafeMultipleInheritance] + spec_set = str + + # Test that the mock really has the `spec_set` of `str` by default + x = MyCustomMock() + assert hasattr(x, "removesuffix") + assert not hasattr(x, "notastringmethod") + + # Explicitly setting `spec_set` on __init__ should take precedence + y = MyCustomMock(spec_set=int) + assert not hasattr(y, "removesuffix") + assert hasattr(y, "to_bytes")