Mocking Zope interfaces

Posted on Wed 28 September 2011 in Coding

In one of my pet projects I use Twisted for handling asynchronous network events. While browsing the Twisted source code, I encountered Zope interfaces. In a dynamically typed language like Python, what good are interfaces?

It turns out that Zope interfaces are slightly different from interfaces found in a statically typed object-oriented language. They facilitate the use of components in Python. Given a Zope interface, you can easily verify if an object claims to provide a particular service, and create an adapter on the fly that bridges two interfaces. I won’t go into details; read this document for more information.

I decided to use Zope interfaces in my pet project, but when I ran my tests, my mock objects didn’t work anymore. In this post, I’ll show how I got back on track with my mocks! By the way, I use this mock framework.

Suppose that we have the following Zope interface in our code (in foo.py):

from zope.interface import Interface

class IFoo(Interface):
    """The IFoo interface."""
    def bar(action):
        """Given action, do bar."""

We also have a function that we want to test, and the function accepts an object that implements the IFoo interface (also in foo.py):

def baz(foo):
    foo = IFoo(foo)
    foo.bar("test")

First attempt

Let’s create a unit test for the baz function, one that mocks the argument so that we can verify that its bar method was called. It may look something like this:

import unittest
from mock import Mock
from foo import IFoo, baz

class TestBaz(unittest.TestCase):
    def test_call(self):
        mock = Mock(IFoo)
        baz(mock)
        mock.bar.assert_called_with("test")

if __name__ == "__main__":
    unittest.main()

Our intention is for the Mock call to create a mock object that implements the IFoo interface. This does not work as expected, though. Instead, we get an error message: “AttributeError:Mock object has no attribute ‘bar’“. What happened? Apparently, our mock survived the foo = IFoo(foo) part. This is because the mock dynamically satisfies all checks that the Zope framework throws at it. But the reason for the failure is that when we call Mock with a class object, the resulting mock will only respond to the methods that it finds in the class using dir(cls), but the baz method is not in that list!

Second attempt

Lucky for us, the interface class has a names() method that we can use:

    def test_call(self):
        mock = Mock(IFoo.names())
        ...

We get closer but not close enough. This time, the error is: “TypeError: (‘Could not adapt’, , )”. But why do we fail earlier this time? Well, it’s because in our first attempt the mock did mock all the required infrastructure methods. But this time, it only mocks baz. And since the Mock class has not declared that it implements IFoo, and there is no adapter installed, the mock isn’t recognized.

Third attempt

It’s not a good idea to dynamically change the Mock class to implement the IFoo interface; that would possibly ruin other test cases. Instead, we create a sub class:

from zope.interface import implements

class IFooMock(Mock):
    """A specialized IFoo mock class."""
    implements(IFoo)

And in our unit test:

    def test_call(self):
        mock = IFooMock(IFoo.names())
        ...

This time, the outcome is much more pleasant; the test will pass!

A general solution

We can go even further, by defining a method that dynamically creates the interface/Mock sub class for us (error handling elided for brevity):

from mock import Mock
from zope.interface import classImplements
import types

def create_interface_mock(interface_class):
    """Dynamically create a Mock sub class that implements the given Zope interface class."""

    # the init method, automatically specifying the interface methods
    def init(self, *args, **kwargs):
        Mock.__init__(self, spec=interface_class.names(),
                      *args, **kwargs)

    # we derive the sub class name from the interface name
    name = interface_class.__name__ + "Mock"

    # create the class object and provide the init method
    klass = types.TypeType(name, (Mock, ), {"__init__": init})

    # the new class should implement the interface
    classImplements(klass, interface_class)

    # make the class available to unit tests
    globals()[name] = klass

Now, it’s sufficient to call the method above for each interface to mock:

if __name__ == "__main__":
    create_interface_mock(IFoo)
    unittest.main()

And finally, the unit test can be simplified to this:

    def test_call(self):
        mock = IFooMock()
        ...