Update: We have developed pyDoubles, a test doubles framework for Python which supports mock objects among other test doubles. We no longer use mocker (described in this post).
The best article I’ve found on Mock Objects is again written by one of my favorite authors, Martin Fowler: Mocks aren’t Stubs.
Mock objects focus on behavior rather than state so it leads you to Behavior Driven Development, a particular flavor of Test Driven Development. I still don’t know if I prefer BDD or classic TDD, I am just learning but in some cases I like to use mock objects, specially on Python.
- Reason 1: Interpreted languages like Python, don’t have a compiler to tell you that API has changed so that if you make changes in a class and don’t look for references you get an runtime exception.
- Reason 2: Third party tools might change their API as well, so you get to reason 1.
- Reason 3: You can write down the expected behavior for the methods that are supposed to behave always in a certain manner.
My first test case mocks Django models to make sure the relationships work always as I want:
from django.db import models from siga.managementServices import * import mocker import unittest import os class managementServicesTests(mocker.MockerTestCase): def testMockMarkConfigAsParticular(self): stubServer = Server() mockServer = self.mocker.proxy(stubServer) mockConfigFile = self.mocker.proxy(ConfigFile) mockConfigFile.particularServers.add(mockServer) self.mocker.result(None) mockConfigFile.save() self.mocker.result(None) self.mocker.replay() service = ServersManagementService() service.markConfigAsParticular(mockServer, mockConfigFile) self.mocker.verify()
The models are Server and ConfigFile. They have a many2many relationship. The test above says:
There is a Server object and a ConfigFile object. Calling service.markConfigAsParticular and passing in the Server and the ConfigFile instances, will result in two calls in the ConfigFile instance (particularServers.add(server) and save()), both returning None.
Once the expectations are defined, we turn mocker into replay mode and call the real method, the one we’re testing:
def markConfigAsParticular(self, server, configFile): retInfo =  if server == None: retInfo = ['1', 'Server does not exist'] elif configFile == None: retInfo = ['2', 'Configuration file does not exist'] else: configFile.particularServers.add(server) configFile.save() retInfo = ['0', 'Success'] return retInfo
In the test method we not only define which methods are going to be invoked but also the order and their parameters. Another nice effect is that data is not persisted into database as the objects in the test are mocks.
I still don’t know how much useful is this as I am actually rewriting the method somehow. I’d say if the method was more complex the test would be more useful because it just keeps control of the important behavior, no matter what the remaining implementation is.
As Martin says, I guess one need to work long time following this methodology to realize how productive it is. That happens with TDD but I trust on it for large projects. Time will say it mock objects are good for my current project.