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()
CodePudding user response:
Once you patched the target class, anything you try to access under that class will be mocked with MagicMock (also recursively). Therefore, if you want to keep the specification of that class, then yes, you should use the autospec=true flag. But because you are trying to mock a class within a class accessed by a property, You have to keep the specification of each class you want to test:
def test_class_a():
class_b_mock = create_autospec(class_a.ClassB)
class_c_mock = create_autospec(class_b.ClassC)
class_b_mock.class_c = 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()