Python Unit Testing with Mock

In the past, while creating unit tests for C# classes, I’ve frequently used a mock object framework such as Moq to isolate classes under test. When I started working in Python, I initially didn’t think I’d need such a framework to write my tests. Creating a stub with a given set of attributes and methods is easy in language like Python.

However, I soon noticed I was writing a lot of these little stubs, and this was ‘dirtying up’ my nice, readable tests. The next problem I encountered was 3rd party libraries with more complex interfaces, like this Twitter library. It contains a client class that dynamically wraps Twitter’s RESTful API. So instead of calling http://twitter.com/statuses/friends.format, you call Twitter.statuses.friends(). The Twitter client doesn’t have a statuses.friends method defined, it creates the URL dynamically from the dot notation you enter.

Creating fakes to test code using such a class would be tedious. Additionally, I was looking at having to pass in instances of other 3rd party classes I was using. The whole thing was getting dangerously close to the DI/IoC mess we’d left behind when we switched from C#. As Jonathon commented a few weeks back:

In Python, hard is a smell.

Enter Mock. Mock is a mocking and testing library for Python by Michael Foord. Unlike many of the other mocking libraries for Python that use the record/replay pattern, Mock uses an action/assert pattern similar to Moq.

Below is a snippet of code that uses the previously mentioned Twitter RESTful client with the dynamic API methods. I’d like to mock up the actual API calls so that I’m not connecting to Twitter when I run my tests.

from twitter import Twitter, TwitterError

class TwitterClient(object):

    def __init__(self, login, password):
        self._twitter = Twitter(login, password)

    def all_friends(self):
        return self._get_statuses(
            lambda crs: self._twitter.statuses.friends(cursor = crs))

    def all_followers(self):
        return self._get_statuses(
            lambda crs: self._twitter.statuses.followers(cursor = crs))

    def _get_statuses(self, call):
        crs = -1
        results = []
        while crs != 0:
            page = call(crs)
            users = page['users']
            crs = page['next_cursor']
            results.extend(users)
        return results

Mock makes this easy.

class TwitterClientTests(unittest.TestCase):

    def test_all_friends(self):
        """Verify TwitterClient.all_friends()"""
        tc = TwitterClient('username', 'password')
        tc._twitter = Mock()
        tc._twitter.statuses.friends.return_value = \
            {'users':[{'a':1}], 'next_cursor':0}
        result = tc.all_friends()
        tc._twitter.statuses.friends.assert_called_with(cursor = -1)
        self.assertEquals([{'a':1}], result)

After creating my test class, I mock out the actual internal Twitter instance that is created. I then set the return value for the API method that all_friends will pass to _get_statuses. I don’t need to reproduce the full JSON returned from a real Twitter API call, I just need enough to test a single loop through the cursor-based call. Once I’m finished, in addition to validating the results returned using regular TestCase asserts, I can verify that my mock method was called with the correct parameters.

This just scratches the surface of what you can do with Mock. It contains facilities for patching, which is useful if you want to mock objects that get created during a method call (i.e. they aren’t easy-to-get-at class members like self.foo). Check out Mock’s documentation and introduction for more details.

Be Sociable, Share!
  • Pingback: Confluence: Product Development()

  • jh

    Would it fail to operate in the same way if you had applied the mock on the TwitterClient class directly rather than instance within it?

  • http://www.insomnihack.com Dale

    Not really. TwitterClient was the class whose interface was being tested. I was verifying its contract.

  • Lesus

    Expert master Dale to write more about Python Unit Testing with Mock.