Mocking a Python standard library
14 March 2008
Here's one of many things I've learnt today at PyCon. Inspired by code that Grig Gheorghiu showed in his slides on automated testing, you can monkey patch a standard library that your application is using in your unit tests to, in my case, mock a remote service without having to run a server. I've done lots of monkey-patching in Zope but then I've only been monkey patching individual methods or attributes of imported classes. This is very similar to that. Here's what my application does:
from poplib import POP3 class MyZopeApp(...): def check4mail(self, hostname, port, user, pwd): connection = POP3(hostname, port=port) ...download emails and process them...
Adjacent to this I have a unit/integration test that looks like this:
class TestCase(ZopeTestCase): def test_check4mail(self): # monkey patch! # note that this imports a module, not a class from Products.IssueTrackerProduct import IssueTracker FakePOP3.files = ('test1.email',) IssueTracker.POP3 = FakePOP3 # now check what happens when check4mail() is run result = self.folder.tracker.check4mail() assert ...
Now for the mock. The mock is a fake POP3 class that instead of getting its data from the network reads local filesystem files. Here's what the code for
from poplib import POP3, error_proto class FakePOP3(POP3): username = 'test' password = 'test' files =  def __init__(self, hostname, port=110): self.hostname = hostname self.port = port def getwelcome(self): return "Welcome to fake account" def user(self, user): if user != self.username: raise error_proto("Wrong username.") def pass_(self, pswd): if pswd != self.password: raise error_proto("Wrong password.") def list(self, which=None): # eg. ('+OK 4 messages:', ['1 71017', '2 2201', '3 7723', '4 44152'], 34) files = self.files responses =  for i, f in enumerate(files): responses.append('%s %s' % (i+1, os.stat(f)[stat.ST_SIZE])) return ('+OK %s messages:' % len(files), responses, None) def retr(self, which): # ['response', ['line', ...], octets] filename = self.files[which-1] return ('response', open(filename, 'r').xreadlines(), None) def quit(self): pass
That's it! That's how you fake a POP3 server without having to run an actual mock server which could have been a solution.