Art of Unit Testing: Part 2

June Kim juneaftn at orgio.net
Sat Aug 25 09:16:56 EDT 2001


"Jesse F. W" <jessefw at loop.com> wrote in message news:<mailman.998679338.16044.python-list at python.org>...
> Dear Python-list-iners,
> 	I have another Testing related question.  How are unit tests done 
> when the units to be tested get lots of information from nested 
> objects, e.g. (in a method of a class to be tested):
> if self.app.cnt_player.battle.kind=='stop':
> how would this be tested?

First of all, it really is a Bad Smell. It obviously violates
LoD(which is not so obvious though). Formal definition of LoD goes
something like(for full and exact definition you should read the
original paper, "Assuring good style for object-oriented programs"
IEEE Software, pages 38--48, September 1989):

For an object O of class C and for any method m defined for O, each
receiver of a message within m must be one of the following objects:

 1. O itself
 2. any objects passed into m
 3. any objects O created
 4. any composite objects
 5. any objects referred to by a global variable

There are some variation versions of LoD, but all of them share the
idea of "Tell Don't Ask." The Pragmatic Programmers have rendered it
so nicely, and I think it's decent enough as today's LoD.
(http://www.pragmaticprogrammer.com/ppllc/papers/1998_05.html)


> 	Should you simulate the parts of the app object that are needed? 
> In that case, you have to update the tests when you use parts of the 
> app object you did not use before.  Should you use a real app 
> object?  Then you are not really doing _unit_ testing, and it would 
> make running detailed tests very difficult?  Is there another solution 
> that anyone knows about?

Unit Tests test the intention of units. We don't care about how the
unit process internally, as long as the unit does its work. Moreover,
if the unit tests depend upon the internals of the units, more often
than not, you have to change the unit tests, which could be hazardous,
when refactoring the code.

If the unit was to return a list of customers, you don't want worry
about how it produces the object, whether by walking through data or
by conneting to DB, whatever. And, remember that naming is one of the
most important practice for good codes, and it should reveal the
intention. Therefore, if the method was getCustomersList, we test if
it returns customers list(object), no more no less. By any chance, if
you have refactored the code into several responsibilities, such as
removeDeadCustomers, you test the code if it really removes dead
customers.

Sever the unit that you want to test, and test it as independently as
you can.

If you can't cut the dependency chain easily, you should consider
using mock objects first. (Actually, if you do TestDrivenDesign or
TestFirstProgramming, you wouldn't worry about those -- good
dependency management, and easy to mock up. Testability is given from
the very beginning.)

One of the most famous papers on mock objects was presented at
XPUniverse 2000
(http://www.cs.ualberta.ca/~hoover/cmput401/XP-Notes/xp-conf/papersList.html)
, of which papers has been published as a book "XP Examined." -- worth
a read. Its original URL is, AFAIK,
http://www.sidewize.com/company/mockobjects.pdf

We have a nice Python mock objects module already. It's at
http://groups.yahoo.com/group/extremeprogramming/files/PythonMock.zip

Since the code is short, I include it in this mail. (BTW, the code is
originally written by Dave Kirby, and I added one method hasParam,
which calls unittest modules asserts, so that I can use it with
unittest module easily)

With this module, you can do:

>>> import mock
>>> customers=mock.Mock({'getCustomerList':('Dave','Peter','Laura')})
>>> customers.getCustomerList()
>>> customers.someMethod(1,200,name='Charlie')
>>> print customers.getAllCalls()
[getCustomerList(), someMethod(1, 200, name='Charlie')]
>>> someMethodCall=customers.getNamedCalls('someMethod')
>>> print someMethodCall[0].getParam('name')
Charlie
>>> print someMethodCall[0].getParam(1)
200
>>> someMethodCall[0].hasParam(1,200,name='Charlie')

If the legacy code's unit to test uses a specific external module, and
you want to test the unit before refactoring to improve the dependency
relationships, you can simply substitute the external module with mock
objects.

Say, we have the following legacy code in customers.py:

import customerDB

class Customers:
    .... 
    def getCustomersList(self):
	     allCustomers=customerDB.getAll()
		 ....

We can test it as in the following test code:
import unittest, mock
import sys
from customers import Customers

class TestGetCustomersList:
    def testReturnAll(self):
		mockCustomerDB=mock.Mock({'getAll':('Charlie','Dave','None')})
		sys.modules['customerDB']=mockCustomerDB
		customers=Customers(...)
	    self.assertEqual(('Charlie','Dave'),customers.getCustomersList())

#
# (c) Dave Kirby 2001
# dkirby at bigfoot.com
#
'''
The Mock class emulates any other class for testing purposes.
All method calls are stored for later examination.
The class constructor takes a dictionary of method names and the
values
they return.  Methods that are not in the dictionary will return None.
'''
import unittest

class Mock:
    def __init__(self, returnValues={} ):
        self.mockCalledMethods = {}
        self.mockAllCalledMethods = []
        self.mockReturnValues = returnValues
        
    def __getattr__( self, name ):
        return MockCaller( name, self )
    
    def getAllCalls(self):
        '''return a list of MockCall objects,
        representing all the methods in the order they were called'''
        return self.mockAllCalledMethods

    def getNamedCalls(self, methodName ):
        '''return a list of MockCall objects,
        representing all the calls to the named method in the order
they were called'''
        return self.mockCalledMethods.get(methodName, [] )

class MockCall(unittest.TestCase):
    def __init__(self, name, params, kwparams ):
        self.name = name
        self.params = params
        self.kwparams = kwparams
    def getParam( self, n ):
        if type(n) == type(1):
            return self.params[n]
        elif type(n) == type(''):
            return self.kwparams[n]
        else:
            raise IndexError, 'illegal index type for getParam'
    def getName(self):
        return self.name
    def hasParam(self,*params,**kwparams):
        self.assertEqual(tuple(self.params),tuple(params))
        self.assertEqual(self.kwparams,kwparams)

    #pretty-print the method call
    def __str__(self):
        s = self.name + "("
        sep = ''
        for p in self.params:
            s = s + sep + repr(p)
            sep = ', '
        for k,v in self.kwparams.items():
            s = s + sep + k+ '='+repr(v)
            sep = ', '
        s = s + ')'
        return s
    def __repr__(self):
        return self.__str__()

class MockCaller:
    def __init__( self, name, mock):
        self.name = name
        self.mock = mock
    def __call__(self,  *params, **kwparams ):
        thisCall = MockCall( self.name, params, kwparams )
        calls = self.mock.mockCalledMethods.get(self.name, [] )
        if calls == []:
            self.mock.mockCalledMethods[self.name] = calls
        calls.append(thisCall)
        self.mock.mockAllCalledMethods.append(thisCall)
        return self.mock.mockReturnValues.get(self.name)



More information about the Python-list mailing list