Monkey patching Python’s SMTP lib for unit-testing

September 20th, 2007

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.

chrss (chess by rss) update 17

September 18th, 2007

This update adds two features:

  • you can now offer a draw to an opponent
  • comments are now included in rss feeds for games and users

The first new feature was needed so I could finish this game. A bit like when I had to add the ability to resign a game. That’s probably the last major missing feature added. I could add code for detecting threefold repetition or the fifty move rule, but that seemed overkill for what I have in mind for chrss. After all it’s meant to be for playing friendly games of correspondence chess – so agreeing whether to draw a game should not be a problem.

I also figured that adding comments to the rss feeds might be nice. With a game like chess, moves can be few and far between, so making the rss feeds a bit more active might be quite good. It also means you can nudge your opponent – as leaving a comment will show up in their feed reader and might well serve to remind them it’s their turn to play!

I’ve still got PGN in mind for my next feature. I imagine I’ll need to spend a bit of time to make sure that the exported PGN files will work with other chess programs.

chrss code coverage

September 1st, 2007

I’ve generally speaking been a fairly good boy when it comes to writing unit tests for chrss, but I know I haven’t covered everything. Most of the focus has been on the underlying chess module, so I thought I’d best have a look and see what my actual unit test code coverage was like (i.e. how much of the code is actually getting run during tests).

Nosetests has a handy --with-coverage option, that requires installing coverage.py, for just this problem.

Running:

nosetests --with-coverage --cover-package=chrss --cover-erase

Then gives me the following output:


Name                     Stmts   Exec  Cover   Missing
------------------------------------------------------
chrss                        0      0   100%   
chrss.chess2               578    549    94%   69, 260, 274, 281-286, 302, 340, 343-344, 347, 349, 351, 365, 367, 371, 382-384, 387, 389, 391, 680, 886, 918, 921, 939
chrss.config                 0      0   100%   
chrss.controllers          504    232    46%   35-36, 39, 50, 52, 54, 56, 58, 60, 62, 186, 195, 197, 210-237, 248-257, 261-266, 271-277, 282-290, 296-301, 307-311, 316-323, 330, 341-346, 352-380, 428, 435, 462, 478-479, 482-494, 498, 508, 512, 516, 520, 524, 529-531, 537-553, 558-566, 593, 601, 606-618, 627, 630, 636-637, 644-648, 652-678, 683-685, 690, 694-710, 714-739, 744-755, 758-760, 763-776, 779-798
chrss.model                325    228    70%   66, 68, 75, 82, 84, 91, 94, 101, 106, 115, 131-133, 136-142, 160, 164, 170-179, 187, 191-198, 207, 209, 222-224, 228, 231, 237, 245-265, 308-310, 326-335, 338-339, 354-355, 427-436, 439-442, 445-446, 455, 458-461
chrss.rest                  31      0     0%   1-43
chrss.rss                   86     50    58%   14, 18, 48-58, 67, 81-91, 95-105
chrss.templates              0      0   100%   
chrss.templates.master     305    236    77%   28-29, 43, 61-65, 79, 107, 135, 148, 217, 226-249, 268, 295, 321, 346, 357, 409-430, 444-447, 453, 509-510
chrss.urls                  34      0     0%   1-57
chrss.widgets              131    115    87%   7-11, 61, 68, 109-114, 147-148, 158
------------------------------------------------------
TOTAL                     1994   1410    70%   
----------------------------------------------------------------------

So overall I am (apparently) managing 70% coverage and the core chrss.chess2 module has 94% coverages. Not too bad I suppose. However I could do better and there are some notable gaps. In particular chrss.controllers only has 46% coverage and that represents some fairly important code for the whole site.

I’ve held of writing tests for some of the controller functions, as they involve sending email (signups, resetting passwords etc). I’ll have to write some sort of email mock class for that purpose, but it will be worth it as I can then be more certain things are working as they are intended.