Mocking a file-like object in python with mock framework

This is a follow-up to my previous post, where I described a manner in which you can mock a file-like object with fudge in python 3.

The challenges were in figuring out:

  • where the built-in read() was located;
  • which calls are being made to the file-like object, and where to mock them

I have since my friend delfick correctly point out that while there is nothing wrong with Fudge, it has not had much love and the Mock library is the Way of the Future as it has been included in the Python3 standard library.

Given that, I will write new tests using the Mock framework, whether they be for Python2.7 or Python3x.

So, this post is a demonstration of mocking a file-type object, this time the response from urlopen. Here is a snippet of my production code:

def forward_geocode(self, address_line, suburb, state, postcode=None):
    params = urllib.urlencode({
        'address': '{0}, {1}, {2}'.format(address_line, suburb, state),
        'key': self.GOOGLE_GEOCODE_API_KEY
        }
    )
    url = 'http://maps.googleapis.com/maps/api/geocode/json?{params}'\
        .format(params=params)

    try:
        response = urlfetch.urllib2.urlopen(url)
        if response.code in [200, 206]:
            data = json.load(response)
            if data['status'] == 'OK':
                return ndb.GeoPt(
                    float(data['results']['geometry']['location']
                        ['lat']),
                    float(data['results']['geometry']['location']
                        ['lng']))
        ...

As you can see, it’s a fairly simple method for calling Google’s Geocoding API. The json.load(response) call expects response to be a file-type object. To Mock this we use the following in a unit test:

def test_forward_geocode(monkeypatch):
    ...
    mock_response = mock.MagicMock(spec=file)
    mock_response.code = 200
    fake_buffer = b'''
{
    "status" : "OK",
        "results" : {
            "geometry" : {            
                "location" : {
                    "lat" : -89.2,
                    "lng" : 45.6
                }
            }
        }
}'''

    fake_empty_buffer = b''
    mock_response.read.side_effect = [fake_buffer, fake_empty_buffer]

    mock_open = mock.MagicMock(return_value=mock_response)
    monkeypatch.setattr(urlfetch.urllib2, 'urlopen', mock_open)
    ...

One Comment

  1. Stephen says:

    You can also just give it a StringIO object

    so:

    from six import StringIO
    import json

    print json.load(StringIO(‘{“blah”: “stuff”}’))

    :)

    Also, mock comes with handy decorators

    with mock.patch.object(urlfetch.urllib2, “urlopen”, mock_open):

    [do stuff]

    And finally, you can avoid the situation where you have the string at the start of the line using dedent.

    from textwrap import dedent

    fake_buffer = dedent(b”””
    { “status”: “OK”
    , “other_variables”: “are_here”
    }
    “””)

    https://docs.python.org/2/library/textwrap.html#textwrap.dedent

Leave a Reply

*

Contact Nixz Kerr