I'm wondering if it's possible to mock a class which contains properties by using patch and autospec? The goal in the example below is to mock (recursively) ClassB.
Example:
# file: class_c.py
class ClassC:
def get_default(self) -> list[int]:
return [1, 2, 3]
def delete(self, name: str):
print(f"delete {name}")
# ----------------------
# file: class_b.py
from class_c import ClassC
class ClassB:
def __init__(self) -> None:
self._ds = ClassC()
@property
def class_c(self) -> ClassC:
return self._ds
def from_config(self, cred: str) -> str:
return cred
# ----------------------
# file: class_a.py
from class_b import ClassB
class ClassA:
def __init__(self):
self._client = ClassB()
@property
def class_b(self) -> ClassB:
return self._client
def test(self, cred: str) -> str:
return cred
# ----------------------
# file: test.py
import pytest
from unittest import mock
@mock.patch("class_a.ClassB", autospec=True)
def test_class_a(_):
class_a = ClassA()
with pytest.raises(TypeError):
class_a.class_b.from_config() # ✅ raised - missing 1 required positional argument: 'cred'
with pytest.raises(TypeError):
class_a.class_b.class_c.delete() # <- ❌ Problem - should raise exception since autospec=True
The property class_c
of the class ClassB
is not mocked properly. I would expect TypeError
when trying to call delete()
without any argument
I've tried several things but without success. Any idea?
EDIT:
The code is just an example, and the test function was just written to demonstrate the expected behaviour. ClassB can be seen as a third-party service which needs to be mocked.
EDIT2:
Additionally to the accepted answer, I would propose to use PropertyMock
for mocking properties:
def test_class_a():
class_b_mock = create_autospec(class_a.ClassB)
class_c_mock = create_autospec(class_b.ClassC)
type(class_b).class_c = PropertyMock(return_value=class_c_mock)
with mock.patch("class_a.ClassB", return_value=class_b_mock):
class_a_instance = ClassA()
with pytest.raises(TypeError):
class_a_instance.class_b.from_config()
with pytest.raises(TypeError):
class_a_instance.class_b.class_c.delete()