First, note that this is all untested, but the ideas should hold up. I tried to abstract a lot of the logic by distilling the Mock
object creation to a dictionary containing the data, but it turned into a bit of a mess. To start with, consider the following structure for your Mock
object data:
async def coro(a):
return a
mock_object_data = {
'validator': {
'subattrs': [
{'validate':
{'return_value': True}
}
]
},
'model': {
'subattrs': [
{'create_instance':
{'side_effect': coro}
}
]
},
'normalizer': {
'subattrs': [
{'normalized':
'side_effect': lambda a: a
}
]
}
}
This contains the name of each Mock
object, any keyword arguments that are necessary, and any attributes that also need to be created (as Mock
objects themselves). These can be created from this structure using the following functions:
def _generate_mock_object(attr_dict):
if 'subattrs' in attr_dict:
subattrs = attr_dict.pop('subattrs')
else:
subattrs = []
mock_object = Mock(**attr_dict)
for subattr_dict in subattrs:
for subname, kwargs in subattr_dict.items():
setattr(mock_object, subname, _generate_mock_object(kwargs))
return mock_object
def generate_mock_objects(object_data):
return {n: _generate_mock_object(d) for n, d in object_data.items()}
This allows for a arbitrary number of Mock
objects to be created. However, this runs into some problems when we want to use the context manager since it is not natural to have an arbitrary number of required contexts. This can be achieved using a recursive function. Here, we will supply a sequence of callables that are to be opened with the with
statement, and a callback function and arguments to be executed once all of the contexts are opened.
def recursive_context_manager(callable_seq, callback, *args, **kwargs):
if not callable_seq:
return callback(*args, **kwargs)
else:
current, *rest = callable_seq
with current():
return recursive_context_manager(rest, callback, *args, **kwargs)
Finally, we want to create a decorator which will do the following things: (a) create all of the Mock
objects, (b) generate and enter the required contexts, and (c) execute the supplied function.
def bind_mock_object_data(data):
def decorator(f):
factory = lambda name, obj: lambda: patch.object(CreateMixin, name, new=obj)
mock_callables = [factory(n, o) for n, o in generate_mock_objects(data).items()]
def wrapper(*args, **kwargs):
return recursive_context_manager(mock_callables, f, *args, **kwargs)
return wrapper
After all of this overhead, your final test code comes out nice and clean.
@unittest_run_loop
@bind_mock_object_data(mock_object_data)
async def test_post_response(self):
response = await self.client.post('/', data='{"body": "ok"}')
text = await response.text()
self.assertTrue(text == '{"body": "ok"}' or text == '{\n "body": "ok"\n}')