-
-
Save c80609a/8d5881dc385588e6f8ceddf0d3ea768a to your computer and use it in GitHub Desktop.
pytest_mock with patch.method
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import gc | |
import sys | |
import types | |
import unittest.mock | |
from typing import Any | |
from typing import Callable | |
from typing import Generator | |
from typing import Optional | |
from typing import TYPE_CHECKING | |
import pytest | |
import pytest_mock | |
def _class_holding(fn: Callable) -> Optional[type]: # see https://stackoverflow.com/a/65756960 | |
for possible_dict in gc.get_referrers(fn): | |
if not isinstance(possible_dict, dict): | |
continue | |
for possible_class in gc.get_referrers(possible_dict): | |
if isinstance(possible_class, type) and getattr(possible_class, fn.__name__, None) is fn: | |
return possible_class | |
return None | |
class MockerFixture(pytest_mock.MockerFixture): | |
patch: '_Patcher' | |
class _Patcher(pytest_mock.MockerFixture._Patcher): | |
if TYPE_CHECKING: | |
def method( | |
self, | |
method: Callable, | |
new: object = ..., | |
spec: Optional[object] = ..., | |
create: bool = ..., | |
spec_set: Optional[object] = ..., | |
autospec: Optional[object] = ..., | |
new_callable: object = ..., | |
**kwargs: Any, | |
) -> unittest.mock.MagicMock: | |
... | |
else: | |
def method( | |
self, | |
method: Callable, | |
*args: Any, | |
**kwargs: Any, | |
) -> unittest.mock.MagicMock: | |
""" | |
Enables patching bound methods: | |
-patch.object(my_instance, 'my_method') | |
+patch.method(my_instance.my_method) | |
and unbound methods: | |
-patch.object(MyClass, 'my_method') | |
+patch.method(MyClass.my_method) | |
by passing a reference (not stringly-typed paths!), allowing for easier IDE navigation and refactoring. | |
""" | |
if isinstance(method, types.MethodType): # handle bound methods | |
return self.object(method.__self__, method.__name__, *args, **kwargs) | |
elif isinstance(method, types.FunctionType): # handle unbound methods | |
cls = _class_holding(method) | |
if cls is None: | |
raise ValueError( | |
f"Could not determine class for {method}: if it's not an unbound method " | |
f'but a function, consider patch.object.' | |
) | |
return self.object(cls, method.__name__, *args, **kwargs) | |
else: | |
raise ValueError(f"{method} doesn't look like a method") | |
@pytest.fixture | |
def mocker(pytestconfig: Any) -> Generator[MockerFixture, None, None]: | |
""" | |
Extends the pytest_mock 'mocker' fixture with additional methods: | |
def test_foo(mocker): | |
mocker.patch.method(...) | |
""" | |
result = MockerFixture(pytestconfig) | |
yield result | |
result.stopall() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment