Saturday, September 25, 2010

Monkey Patching in Django Tests AKA I Should've Used Mock Objects

   About a year ago I was introduced to mock objects, specifically PyMox. It seemed like a great idea, but I was new to unit tests to begin with, as well as python, and I had never even heard of mock objects. Since then I've lost a bit of my green color, but only now I'm starting to understand how mock objects work. In the meantime I've committed to unit tests and occasionally monkey patching them as ad hoc mocking. I will be the first to admit that this is not ideal, mock objects are probably the way to go. But for simple cases and just getting on with it, monkey patching may work for you.

If you aren't familiar with the term, monkey patching means changing code at runtime. Only possible in wicked languages like python, it can be a major source of headaches if you do this unexpectedly (or if somebody else does it unexpectedly)

I'm currently using Twilio for handling my phone number redirection service for mobile phones, Churp. In my unit tests I needed to make sure that my requests and responses were being handled properly, but I didn't want to do any real requests to Twilio every time I run the suite. So I monkey patched the Twilio rest client for my tests. It looks something like this:

class FakeTwilioRestClient(object):
    #this is a class variable that I use for switching the return value of the Fake Rest Client
    find_number = False
    def __init__(self, account_sid, account_token):
        # basically copy the __init__ of the real class here since this class
        # will be initialized just like the real one
        pass

    def request(self, request_resource, method_type, params=None):
        # parameters must be the same as the real method since that's how it's
        # going to be called inside your code
        if not self.find_number:
            return '{"friendly_name": "(604) 777 8856"}'
        else:
            return """{"available_phone_numbers": [{"friendly_name": "(604) 777 8856"}]}"""


Now when the view uses the client, it's been replaced by what's above, instead of doing a real request to Twilio. So that is the client, here is what the test looks like.

def test_my_twilio_view(self):
    from myviews import twilio
    original_account_class = twilio.Account
    twilio.Account = FakeTwilioClient
    .....

    #fix the monkey patching for the other tests
    twilio.Account = original_account_class


def test_my_twilio_view_2(self):
    from myviews import twilio
    original_account_class = twilio.Account
    FakeTwilioClient.find_number = True #now the fake client will return a different value
    twilio.Account = FakeTwilioClient

One thing that took me a while to really get was how to monkey patch the imports right. What won't work is this:

def test(self):
    import twilio
    twilio.Account = FakeTwilioClient

This won't patch anything. It is important  to import the module you're patching from the module you're testing, or nothing will happen and you might be scratching your head as to why (python modules have their own scope, this is why it's important to import the patched module from the right place and not from your python path)

As always, all comments and / or corrections are welcome. Part of the reason for this blog is to help me learn while trying to give back to the community that has helped me so much.

1 comment:

Anonymous said...

Hey, I need some help ! I am having a sign up with gmail option in my website .I do not want to test the gmail api .How do i write a monkey patching or mock the gmail response..