ghickman
ghickman

Reputation: 6043

How to provide conditional arguments to a mock function in Python?

My project makes various calls to external API's using Python's urllib2.urlopen. I'm using NoseTests for my unit testing and MiniMock to mock up calls made to urllib2.urlopen.

The mocking code:

from hashlib import md5
from os.path import dirname, join
from urllib2 import Request, urlopen

from minimock import mock, restore

def urlopen_stub(url, data=None, timeout=30):
    """
    Mock urllib2.urlopen and return a local file handle or create file if
    not existent and then return it.
    """

    if isinstance(url, Request):
        key = md5(url.get_full_url()).hexdigest()
    else:
        key = md5(url).hexdigest()
    data_file = join(dirname(__file__), 'cache', '%s.xml' % key)
    try:
        f = open(data_file)
    except IOError:
        restore() # restore normal function
        data = urlopen(url, data).read()
        mock('urlopen', returns_func=urlopen_stub, tracker=None) # re-mock it.
        with open(data_file, 'w') as f:
            f.write(data)
        f = open(data_file, 'r')
    return f

mock('urlopen', returns_func=urlopen_stub, tracker=None)

I'm running my tests like so:

from os.path import isfile, join
from shutil import copytree, rmtree

from nose.tools import assert_raises, assert_true

import urlopenmock

class TestMain(object):
    working = 'testing/working'

    def setUp(self):
        files = 'testing/files'
        copytree(files, self.working)

    def tearDown(self):
        rmtree(self.working)

    def test_foo(self):
        func_a_calling_urlopen()
        assert_true(isfile(join(self.working, 'file_b.txt')))

    def test_bar(self):
        func_b_calling_urlopen()
        assert_true(isfile(join(self.working, 'file_b.txt')))

    def test_bar_exception(self):
        assert_raises(AnException, func_c_calling_urlopen)

Originally I had the test checking for the exception in a separate module which imported a different mocking file that returned a broken XML file when urlopen was called. However importing that mocking class overrode the one shown above, breaking all the tests as the broken XML was used each time.

I assume this was because the exception testing module was loaded after the others thus it's import was called last and the mocked function returning the broken XML overrode the original mocked function.

I'd like to be able to tell the mocking code to use the broken XML file when test_bar_exception is run so that it raises the exception. How would I go about doing this?

Upvotes: 2

Views: 3431

Answers (2)

kk1957
kk1957

Reputation: 8824

Assume that your input to request is 'a url' and output would be 'aresponse', and for input 'burl' output is 'bresponse', so use

@mock.patch('requests.get', mock.Mock(side_effect = lambda k:{'aurl': 'a response', 'burl' : 'b response'}.get(k, 'unhandled request %s'%k)))

Upvotes: 0

jsw
jsw

Reputation: 2203

Seems to me that you will need to setup and teardown your mock urlopen on each test that needs to use it and have a different mock urlopen to return a broken xml file for those tests dealing with that error condition. Something like:

class TestMain(object):
    # ...

    def test_foo(self):
        mock('urlopen', returns_func=urlopen_stub, tracker=None)
        func_a_calling_urlopen()
        assert_true(isfile(join(self.working, 'file_b.txt')))
        restore()

    # ...

    def test_bar_exception(self):
        mock('urlopen', 
                returns_func=urlopen_stub_which_returns_broken_xml_file, 
                tracker=None)
        assert_raises(AnException, func_c_calling_urlopen)
        restore()

There is a problem with the above, however, if a test raises an exception and the restore() call is not reached. You could wrap with try: ... finally: however, that's a lot of red-tape for each test.

You might want to take a look Michael Foord's mock library.

Pycon presentation: http://blip.tv/file/4881513

Take a look at patch which gives you decorator and context-manager options for replacing and restoring your calls on urlopen. Its very nifty!

Upvotes: 3

Related Questions