Mocking with Python
After recent conversations about the surgery I did to the DISQUS IMAP processing code in order to make it testable, I was steered toward Python mock modules. Of which there are a lot.
So I installed the python-mock module and coded up a quick example use case.
Often when writing unit tests, you stumble upon dependencies that the code you’re testing uses, but are orthogonal to the actual functionality being tested.
At DISQUS, this was IMAP processing code that clearly depended heavily on an api into an IMAP mail server. But the functionality being tested was just what the processing did to incoming email from said server. So I wrote a fake IMAP api that just passed around test messages.
However this is generally easier to do using mock objects. The mock objects can catch all methods called on them, so there’s no need to strictly match the expected api. And we can inspect the actual calls made to make sure they are what we expected.
So for an example I coded up a simple logger. In a production environment, it would be helpful to log data for debugging, etc. But while testing this is unnecessary.
The following code does BFS using a simple logger:
def search(self, label):
"""
Search for label in graph. Return list of nodes visited in path.
Note: Uses BFS.
"""
visited = set()
queue = [(self, [])]
while queue:
node, path = queue.pop(0)
self.logger.log('Visiting node %s with path %s.' %
(node.label, [n.label for n in path]), severity=1)
if node in visited:
continue
visited.add(node)
path.append(node)
if node.label==label:
return path
else:
for edge in node.edges:
edge_path = copy.copy(path)
queue.append((edge, edge_path))
self.logger.log('Not found.', severity=2)
return None
Which is easy enough to test, except that we’ll be leaving log files around with this information. Instead of a real logger, we can pass in a mocked object as such:
class NodeTests(unittest.TestCase):
def setUp(self):
logger = mock.Mock( {'log' : 'log called'} )
austin = Node('austin', [], logger)
dallas = Node('dallas', [austin], logger)
houston = Node('houston', [austin, dallas], logger)
san_antonio = Node('san antonio', [austin, houston], logger)
self.logger = logger
self.graph = san_antonio
def testSearch(self):
result = self.graph.search('dallas')
labels = [node.label for node in result]
self.assertEqual(['san antonio', 'houston', 'dallas'], labels)
self.assertEqual(5, len(self.logger.mockGetNamedCalls('log')))
Easy enough. And a lot easier that writing up a fake logger that matches the expected api.