3
\$\begingroup\$

I want to test my core of handler, so I wrote this:

@unittest_run_loop
async def test_post_response(self):
    async def coro(a):
        return a
    validator = Mock()
    validator.validate = Mock(return_value=True)
    model = Mock()
    model.create_instance = Mock(side_effect=coro)
    normalizer = Mock
    normalizer.normalized = Mock(side_effect=lambda a: a)
    with patch.object(CreateMixin, 'validator', new=validator):
        with patch.object(CreateMixin, 'model', new=model):
            with patch.object(CreateMixin, 'normalizer', new=normalizer):
                response = await self.client.post('/', data='{"body": "ok"}')
                text = await response.text()
                self.assertTrue(text == '{"body": "ok"}' or text == '{\n    "body": "ok"\n}')

How can I improve this using decorators instead?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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}')
\$\endgroup\$
1
  • \$\begingroup\$ Sorry, dude. Your idea looks not so working realy workly, but complexly for unittesting. First is that properties that I want to mock it's not a dicts, basicly it looks like APIView in django rest, Second is that my main idea is convert my usage patch not like context manager, but like decorator. Anyway thank you, I have read it and idea could get some place in my code later. \$\endgroup\$
    – Denny
    Sep 18, 2017 at 6:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.