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()
...