When using inversion of control in python, we may have a class such as:
class A:
def __init__(self, b, c, d):
self.b = b
self.c = c
self.d = d
Where b
, c
and d
are instances of (or at least behave like) class B
, class C
and class D
. When writing unit tests for A, we would like to mock these objects with mocks that "behave like" the instances of the mentioned classes. When creating mocks as Mock(spec=an_object)
then the mock will answer to the same calls as an_object
. In our case we would have to call their respective constructors doing something similar to this (referred to as "spec-object" ahead):
b = Mock(spec=B(...))
c = Mock(spec=C(...))
d = Mock(spec=D(...))
But this goes against the idea of avoiding the use of the real classes and its instances. If the constructors change tomorrow, for example by adding new parameters, then these mocks would have to be updated to include those new parameters, resulting in a waste of effort.
If the mocks are created without specifying a spec
, then the mocks would be completely independent to changes in the real classes' constructors. For example, we could do something like this (referred to as "no-spec" ahead):
b = Mock()
c = Mock()
d = Mock()
Nevertheless, these mocks are as "dumb" as they could be as they can be called in any possible way and they won't break the test. That is, b
doesn't behave like a B instance and neither do c
and d
with their respective classes. This is troublesome as the unit tests don't cover the "correct calls" to these objects and won't fail with the expected AttributeError Exception
. If we're lucky, these invalid calls would be caught by (hypothetical) integration tests but they should've been the unit tests' responsibility.
A middle ground would be to hard-code the calls we expect b
, c
and d
to respond to. For example, in the following way (referred to as "spec-list" ahead)
b = Mock(spec=["method_b_1", "method_b_2"])
c = Mock(spec=["method_c_1"])
d = Mock(spec=["method_d_42"])
This way, if inside A
we do something like self.b.invalid_method
then an AttributeError
will be raised because the mock was not configured to respond to that call. The downsides of this approach are:
- If tomorrow we refactor
B
renamingmethod_b_1
to something else (assuming thatA
's usage of b was properly refactored too) then this test would fail. We would need to manually rename the mock's spec calls. If we were using the "spec-object" approach from above, then we wouldn't have to do this manual fix. - When defining the mock we have to manually specify all the calls that we expect to do to it. This can get really big and time consuming.
Finally, a commenter on this answer could say that I can simply pass None
to B
's, C
's and D
's constructors when using the "spec-object" approach. For example (referred to as "spec-object-none" ahead):
b = Mock(spec=B(None, None ...)]
c = Mock(spec=B(None, None ...))
d = Mock(spec=B(None, None ...))
But this approach may be passing invalid objects to these constructors and as we are calling the real constructors then they may (and probably will) break if they assume that one or more parameters are not None
. For example, if B
's constructor is something like the following then this test will break:
class A:
def __init__(self, e):
self.something = e.something
In conclusion, none of the ways I found to mock objects is as good as the ones in other languages (for example, in Java when creating mocks you don't have to call the real constructors and the mocks answer only to valid methods, throwing an Exception otherwise). The disadvantages for each approach were:
- spec-object: the test has to call the real class constructor passing valid parameters
- no-spec: the mocks can be called with invalid calls and the test will pass
- spec-list:
- the test has to be manually fixed when the real class methods are refactored
- the valid calls have to be manually specified when creating the mock
- spec-object-none: the test would break if the real class constructor assumes the parameters are not
None
Finally, we arrive to my question: is there a way to create mocks representing instances of a class without having to manually instantiate said class's objects?
EDIT: using create_autospec
@wim suggested using mock.create_autospec with instance=True
and this "kind of works" but it's not "perfect". For example:
In [1]: import unittest.mock
In [2]: class A:
...: def __init__(self, b):
...: self.b = b
...:
...: def get_b(self):
...: return self.b
...:
In [3]: mock = unittest.mock.create_autospec(spec=A, instance=True)
In [4]: mock.get_b()
Out[4]: <MagicMock name='mock.get_b()' id='140598183546064'>
In [5]: mock.invalid_call()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-5-09fd5f1726b9> in <module>
----> 1 mock.invalid_call()
~/anaconda3/lib/python3.7/unittest/mock.py in __getattr__(self, name)
596 elif self._mock_methods is not None:
597 if name not in self._mock_methods or name in _all_magics:
--> 598 raise AttributeError("Mock object has no attribute %r" % name)
599 elif _is_magic(name):
600 raise AttributeError(name)
AttributeError: Mock object has no attribute 'invalid_call'
In [6]: mock.b
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-26e5746cc8da> in <module>
----> 1 mock.b
~/anaconda3/lib/python3.7/unittest/mock.py in __getattr__(self, name)
596 elif self._mock_methods is not None:
597 if name not in self._mock_methods or name in _all_magics:
--> 598 raise AttributeError("Mock object has no attribute %r" % name)
599 elif _is_magic(name):
600 raise AttributeError(name)
AttributeError: Mock object has no attribute 'b'
In [7]: a = A(42)
In [8]: a.b
Out[8]: 42
When using this approach, the mock...
- ... will not raise an
Exception
if a valid method call is made (Good) - ... will raise
Exception
if an invalid method call is made (Good) - ... will raise
Exception
if we try to acces a valid attribute directly (Bad)
EDIT: workaround for dataclasses
In this answer by @mohi666 in another question they showed a way to create mocks of instances of a dataclass that have all the valid attributes accessible while throwing exceptions for invalid accesses. For example:
from unittest.mock import Mock
from dataclasses import fields, dataclass
def create_dataclass_mock(obj):
return Mock(spec=[field.name for field in fields(obj)])
@dataclass
class A:
aaa: str
bbb: int
basicMock = Mock()
specialMock = create_dataclass_mock(A)
basicMock.aaa # returns a mock (Good)
specialMock.aaa # returns a mock (Good)
basicMock.attribute_that_should_not_exist # returns a mock (Bad)
specialMock.attribute_that_should_not_exist # raises an exception (Good)