I have a question related to Python unittest.mock.Mock
and spec_set
functionalities.
My goal is to create a Mock with the following functionalities:
- It has a spec of an arbitrary class I decide at creation time.
- I must be able to assign on the mock only attributes or methods according to the spec of point 1
- The Mock must raise AttributeError in the following situations:
- I try to assign an attribute that is not in the spec
- I call or retrieve a property that is either missing in the
spec_set
, or present in thespec_set
but assigned according to the above point.
Some examples of the behavior I would like:
class MyClass:
property: int = 5
def func() -> int:
pass
# MySpecialMock is the Mock with the functionalities I am dreaming about :D
mock = MyMySpecialMock(spec_set=MyClass)
mock.not_existing # Raise AttributeError
mock.func() # Raise AttributeError
mock.func = lambda: "it works"
mock.func() # Returns "it works"
I have tried multiple solutions without any luck, or without being explicitly verbose. The following are some examples:
- Using
Mock(spec_set=...)
, but it does not raise errors in case I call a specced attribute which I did not explicitly set - Using
Mock(spec_set=...)
and explicitly override every attribute with a function with an Exception side effect, but it is quite verbose since I must repeat all the attributes...
My goal is to find a way to automatize 2, but I have no clean way to do so. Did you ever encounter such a problem, and solve it?
For the curious ones, the goal is being able to enhance the separation of unit testings; I want to be sure that my mocks are called only on the methods I explicitly set, to avoid weird and unexpected side effects.
Thank you in advance!
CodePudding user response:
spec_set
defines a mock
object which is the same as the class, but then doesn't allow any changes to be made to it, since it defines special __getattr__
and __setattr__
. This means that the first test (calling a non-existent attr) will fail as expected, but then so will trying to set an attr:
from unitest import mock
class X:
pass
m = mock.Mock(spec_set=X)
m.func()
# __getattr__: AttributeError: Mock object has no attribute 'func'
m.func = lambda: "it works"
# __setattr__: AttributeError: Mock object has no attribute 'func'
Instead, you can use create_autospec()
which copies an existing function, and adds the mock
functions to it, but without affecting __setattr__
:
n = mock.create_autospec(X)
n.func()
# __getattr__: AttributeError: Mock object has no attribute 'func'
n.func = lambda: "it works"
n.func()
# 'it works'
CodePudding user response:
I think I found a satisfying answer to my problem, by using the dir
method.
To create the Mock with the requirements I listed above, it should be enough to do the following:
def create_mock(spec: Any) -> Mock:
mock = Mock(spec_set=spec)
attributes_to_override = dir(spec)
for attr in filter(lambda name: not name.startswith("__"), attributes_to_override):
setattr(mock, attr, Mock(side_effect=AttributeError(f"{attr} not implemented")))
return mock