Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock nested / multiple layers of return objects in python

I'm currently struggling to find a good way of mocking multiple layers / nested return values. In other words, I want to return a magic mock that in turn returns a magic mock with it's own set return values. I'm finding this relatively cumbersome and am looking for a more elegant and maintainable solution.

I'm trying to test the following code efficiently. the URL returns a json string that needs further processing:

import json
from urllib.request import url open

def load_json():
    # first return value
    response = urlopen("http://someurl.com/api/getjson")
    # in turn, contains two nested return values for read and decode
        response_dict = json.loads(response.read().decode('utf-8'))

This is how I've mocked this so far, which is extremely inelegant and makes maintenance complicated:

class MyTestCase(TestCase):
    @patch('load_json_path.urlopen')
    def test_load_json(self, mock_urlopen):
        ### trying to simplify all of this
        # third nested return
        mock_decode = MagicMock(return_value='["myjsondata"]')
        # second nested return value
        mock_response = MagicMock()
        mock_response.read.return_value=mock_decode
        # first nested return value
        mock_urlopen.return_value = mock_response
        ### trying to simplify all of this            

        load_json()

In the end, all i'm trying to mock is the returned data from the decode function, that originates from the url open function. This should be possible in one line or in a simpler way, using perhaps the enter methods. Ideally the mock would look something like this in the test_load_json function:

mock_urlopen.__enter__.loads.__enter__.decode.return_value = '["myjsondata"]'

Unfortunately, I can't seem to find anything useful in the mock documentation. Any help appreciated.

like image 478
Ric W Avatar asked Sep 12 '16 19:09

Ric W


2 Answers

Turns out this is easily possible and documented. However, the naming is not straightforward and needed to know what one is looking for. The referred to mocking is chained calls, which are in fact documented in the mock library.

In this example, the mock_urlopen should look like this:

    mock_urlopen.return_value.read.return_value.decode.return_value = '["myjsondata"]'

This works beautifully. For more details check out the python doc: https://docs.python.org/3/library/unittest.mock-examples.html#mocking-chained-calls

like image 178
Ric W Avatar answered Nov 17 '22 17:11

Ric W


I have made this for you as a helper class:

from unittest.mock import Mock

class ProxyMock:
    """Put me for easy referral"""
    def __init__(self, mock, _first=True):
        self._mock_ = mock
        self._first_ = _first

    def __getattr__(self, name):
        if self._first_:
            new_mock = getattr(self._mock_, name)
        else:
            new_mock = getattr(self._mock_.return_value, name)
        return ProxyMock(new_mock, _first=False)

    def __setattr__(self, name, value):
        if name in ("_mock_", "_first_"):
            return super().__setattr__(name, value)
        setattr(self._mock_, name, value)

a = Mock()
ProxyMock(a).b.c.return_value = 123

assert a.b().c() == 123
like image 45
Bharel Avatar answered Nov 17 '22 19:11

Bharel