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.