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