
juneaftn at orgio
Aug 25, 2001, 6:16 AM
Post #5 of 14
(682 views)
Permalink
|
"Jesse F. W" <jessefw [at] loop> wrote in message news:<mailman.998679338.16044.python-list [at] python>... > 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 # ''' 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)
|