[go: up one dir, main page]
More Web Proxy on the site http://driver.im/

The Python Mock Module

Contents

Introduction

A problem that frequently occurs when writing unit tests is how to test a unit in isolation from other classes.

One solution to this problem is to create Mock Objects that mimic interface of the classes that the unit depends on. The Mock Object can then return canned values in response to method calls, or check that the expected methods are called with the correct arguments. Mock Objects are described in detail in the paper by Tim Mackinnon and Steve Freeman that can be found on the MockObjects website: http://www.mockobjects.com

One problem with Mock objects is that they can be tedious to create. In a statically typed language every method in the interface must be stubbed out, even if it is never called, or if the test is not interested in the call. Fortunately with a dynamically typed language such as Python there is an alternative way of creating Mock Objects.

The Python Mock Class

The idea behind the Python Mock class is simple. It can mimic any other Python class, and then be examined to see what methods have been called and what the parameters to the call were. The constructor for the Mock class takes an optional dictionary specifying method names and values to return when that method is called. Methods that are not specified in the constructor return None.

Method calls on a Mock Object are caught by the __getattr__ method and stored for later inspection. The __getattr__ method returns an intermediate object (MockCaller) that does all the work, so it is possible to treat it as a bound method.

for example:

>>> from mock import Mock 
>>> myMock = Mock( {"foo" : "you called foo"} ) 
>>> myMock.foo() 
'you called foo' 
>>> f = myMock.foo 
>>> f 
<mock.MockCaller instance at 15d46d0> 
>>> f() 'you called foo' 
>>> f( "wibble" ) 
'you called foo' 
>>>

It is possible to specify that successive calls to a method returns a sequence of values by using the ReturnValues or ReturnIterator objects.

Sometimes you may need to access attributes of the Mock class that are not methods. The simplest way is to create the Mock object first then assign the attributes required before they are needed. For example:

>>> from mock import Mock 
>>> myMock = Mock() 
>>> myMock.greeting = "hello world" 
>>> print myMock.greeting 
hello world 
>>>

Alternatively if the mock object is to be created in several places it may be preferable to create a subclass of Mock that initialises the attributes in the constructor.

As well as making the history of method calls available for inspection, the Mock class also allows you to set up expectations, which are assertions made at the time that a method is called. See below for more details on expectations.

One other issue to be aware of is that __getattr__ takes precedence over the the normal builtin methods that classes have by default, such as __repr__, __str__, __hash__ etc. For example,

>>> print myMock

will fail since __repr__ will return None instead of a string.

If the code you are testing makes use of these methods then you will need to derive a class from Mock that supplies the correct behaviour (see below for more on extending Mock).

Example Usage

Here is an example of how to use it in action. Assume you are testing an application that communicates with a database using the Python DB2.0 API. You want to write a test for a function called 'persistData', but without having to create a database connection, populate the database with test data, etc. The persistData function takes two parameters - an object with the data to persist, and a database connection object. You can write the test like this:

class PersistanceTestCase(unittest.TestCase): 
    def testPersistData(self): 
        #set up the mock objects 
        mockCursor = Mock()
        mockDB = Mock( { "cursor" : mockCursor } ) 
        #call the function to be tested
        persistData(testData, mockDB) 
        #test the correct calls were made on the database 
        objects mockDB.mockCheckCall(0, 'cursor')
        mockCursor.mockCheckCall(0, 
                                 'execute', 
                                 '...some SQL...',
                                  someData) 
        mockCursor.mockCheckCall(1, 
                                 'execute', 
                                 '...more SQL...', 
                                  moreData) 
        mockDB.mockCheckCall(1, 'commit')
        mockDB.mockCheckCall(2, 'close')

The test creates two mock objects, one for the database, and one for the cursor object that is returned when the cursor() method is called on the database.

Retrieving information about method calls

The Mock class has the following methods:

Several expectations can be set up on the same method, and they will be tested in turn.

Example: Create an expectation on method foo that the first parameter will be less than ten.:

>>> mock = Mock( { "foo" : 42 } )
>>> mock.mockSetExpectation('foo', lambda mockobj, call, count: call.getParam(0) < 10)
>>> mock.foo(5)
42
>>> mock.foo(50)
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
  File "C:\prj\mock\mock.py", line 96, in __call__
    returnVal = self.mock.mockReturnValues.get(self.name)
AssertionError: Expectation failed: foo(50)
>>>

There are some pre-defined expectation functions. See below for details.

The MockCall class

The MockCall class encapsulates a single call. It saves the name of the method and the parameters passed to it, both positional and keyword.

It has the following methods for retrieving this information:

Predefined expectations

There are currently four predefined expectations, and more may be added in future. These are factory functions that return the actual expecation functions - see the source for details.

Some examples:

# test that parameter 0 is an integer between 0 and 100 inclusive
mock.mockSetExpectation('someMethod', expectParam( 0, AND( ISINSTANCE(int), GE(0), LE(100) ) ) )

# test that the keyword attribute 'color' has the value 'RED', 'GREEN' or 'BLUE'
mock.mockSetExpectation('someMethod', expectParam( 'color', IN( ['RED', 'GREEN', 'BLUE'] ) ) )

# test that parameter 0 is a dictionary that does not contain the key 'foo'
mock.mockSetExpectation('someMethod', expectParam( 0, AND( ISINSTANCE(dict), NOT(CONTAINS('foo') ) ) ) )

Extending the Mock class

There are times when the default behaviour of the Mock class is not sufficient for the tests you want to perform. Perhaps you need a method that does more than return the same value every time, or you want to make assertions about the call at the point that it is made rather than afterwards, or you want to raise an exception from within a method to test your unit's error handling. To do all these simply derive a new class from Mock and add the methods with the specialised behaviour. All the other methods will still be handled by the Mock class.

There is one issue to watch out for. If you want the call to your specialised method to be recorded along with all the other methods then you must pass the method call up to the base class. However you cannot do this in the normal way because of the way that the call is intercepted by __getattr__. Instead the method must call __getattr__ directly to get a MockCaller object, then call the MockCaller to pass it the parameters.

For example:

class MockSub( Mock ):
     def getSquare( self, x ):
         '''return the square of the parameter passed'''
         #call base class method so that it gets recorded
         Mock.__getattr__(self, 'getSquare')( x )
         return x*x

Interface Checking

One problem with using Mocks in Python is that the unit tests can get out of sync with the production code. An interface method that is called through a mock object in a unit test may later be renamed in production code, but the mock and some production code that called it may be left using the old name.

To protect against such problems, you can optionally pass in a class that is being mocked to the Mock constructor. As each call is made to the Mock object, it will check that a method with that name also exists on the real class, and the call parameters match the argument list of the original method.

class MyOriginalClass:
    def writeUserToDb(self, user):
        '''Write the user to the database.'''
        longRunningFunction(user)
        #...

>>> from mock import Mock
>>> myMock = Mock({"writeUserToDb" : False}, MyOriginalClass)
>>> myMock.unknownFunc()
[snip traceback]
mock.MockInterfaceError: Calling mock method 'unknownFunc' that was not found in the original class
>>> myMock.writeUserToDb(None, 1)
[snip traceback]
mock.MockInterfaceError: Original writeUserToDb() takes 2 arguments (3 given)

Methods with varargs, keyword parameters and/or defaults are also supported.

Multiple Inheritance

Sometimes you don't want to mock all the methods of a class; the real methods in the original class may be fine for many methods. For instance, a class that implements a template method pattern is designed to be subclassed. Template methods must each be implemented by all subclasses.

You may want to assert how template methods are called based upon calls to the base class interface methods. In cases like this, you can multiply inherit from both the production class and from the Mock class. By subclassing from both, you get the default behaviours of the original methods, and method call recording from the Mock class.

For example:

class TemplateMethodBase:
    def func(self, x):
        '''Calls template method "overflow" if x is too big.'''
        if x > 10000000:
            self.overflow(x)
        return x*x
    def overflow(self, x):
        raise NotImplementedError

class TemplateMethodMock(TemplateMethodBase, Mock):
    def overflow(self, x):
        print "%d might be a little too big" % x

>>> m = TemplateMethodMock()
>>> m.func(20000000)
20000000 may be a little too big
400000000000000L
>>> print m.mockGetAllCalls()[0]
func(20000000)
>>> print m.mockGetAllCalls()[1]
overflow(20000000)

This feature can be used in conjunction with expectations, for example to cause a method call on a non-mock object to raise an exception.

License

This Python module and associated files are released under the FreeBSD license. Essentially, you can do what you like with it except pretend you wrote it yourself.

Copyright (c) 2005, Dave Kirby

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.

    * Neither the name of this library nor the names of its
      contributors may be used to endorse or promote products derived from
      this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    mock@thedeveloperscoach.com
SourceForge.net Logo