Monkey patching Python's SMTP lib for unit-testing


I'm about to discuss monkey patching in Python. Always consult an adult before monkey patching.

A little while back Jeremy Jones talked about Testing, Logging, and the Art of Monkey Patching in which he demonstrated using monkey patching to replace the use of tcp sockets. Thus removing the need to created real network connections when testing. In his case he no longer needed to write a dedicated server, just for testing.

Anyway, that got me thinking about the unit testing I am doing for chrss (my chess by rss service). The issue I had was that some of the code to test was using Python's smptlib (to send out account activation and password reset emails). Smptlib is great for this, but I needed to be able to turn it off (or better yet redirect it) when testing.

I believe Django handles testing code that sends email very well. This is because Django provides it's own email sending functionality and so can just change how that behaves when testing.

I could have taken the same approach in my code, but I'd then also be replacing the code that was actually sending the email and it would not be covered by the tests. I'd also have to change my existing code...

Monkey patching to the rescue!.

I created a DummySMTP class that replaces smtplib.SMTP:

# monkey-patch smtplib so we don't send actual emails
smtp=None
inbox=[]

class Message(object):
    def __init__(self,from_address,to_address,fullmessage):
        self.from_address=from_address
        self.to_address=to_address
        self.fullmessage=fullmessage

class DummySMTP(object):
    def __init__(self,address):
        self.address=address
        global smtp
        smtp=self

    def login(self,username,password):
        self.username=username
        self.password=password

    def sendmail(self,from_address,to_address,fullmessage):
        global inbox
        inbox.append(Message(from_address,to_address,fullmessage))
        return []

    def quit(self):
        self.has_quit=True

# this is the actual monkey patch (simply replacing one class with another)
import smtplib
smtplib.SMTP=DummySMTP

Basically you import the module containing this code and smtplib.SMTP will refer to the DummySMTP class from there on in - for all modules. That last part is particularly important. This is why you need to be careful when monkey patching, as you will be replacing code globally.

For unit-testing though, this is quite often what we want. In other languages we might have to use mock objects and all sorts of other tricks, but in a nice dynamic language like Python we can just get in there and monkey around (pun intended) with the internals of the runtime.

Assuming the above code was in a module called testutil, you would then import it at the top of your tests and access any sent messages via testutil.inbox and the last SMTP class created via testutil.smtp (handy for checking you have used the correct login credentials).

e.g.:

import testutil

def test_some_email():
    testutil.inbox = [] # clear the inbox
    # code that does some emailing here
    assert len(testutil.inbox) == 1 # check one email was sent
    assert testutil.inbox[0].to_address == 'someone@somewhere.com'

So that's how you can monkey patch Python's SMTP lib for unit-testing.