Unit testing - suitable for all development?

Roy Smith roy at panix.com
Sun Mar 7 22:19:31 EST 2004


In article <153fa67.0403071515.7cf93986 at posting.google.com>,
 kylotan at hotmail.com (Kylotan) wrote:
> Here's an example of my current system: the Creature class requires a
> Race class and a World class to be instantiated, as the Creature's
> __init__ function relies on calling methods of these objects (not just
> on their presence). Sure, technically I could create dummy objects to
> pass in that have do-nothing functions, but then I break most of
> Creature's accessors, which are implemented in terms of delegating to
> the other objects. In turn this would make the tests meaningless.

OK, let's imagine you've got something like this:

class Creature:
   def __init__ (self, race, world):
      """A creature starts out with as many locally available
      weapons as he's got hands to hold them."""

      weapons = []
      for appendage in race.getAppendages ():
         if appendage.isPrehensile ():
            weapons.append (world.getWeapon ())

and you want to test the constructor.  Think about what could go wrong.  
Well, the most obvious thing is that you could have some kind of syntax 
or semantic error in your constructor code, so the first test I usually 
write for an object is just that I can create one.  It looks like to do 
that, you need three stub classes:

1) A Race class, which has a working getAppendages() method.

2) An Appendage class, which has a working isPrehensile() method.

3) A World class, with a working getWeapon() method.

At this point, I might decide that I want to push the isPrehensile() 
functionality down into the Race class, so I can rewrite my Creature 
constructor as:

class Creature:
   def __init__ (self, race, world):
      """A creature starts out with as many locally available
      weapons as he's got hands that can hold them."""

      weapons = []
      for appendage in race.getPrehensileAppendages ():
            weapons.append (world.getWeapon ())

On the surface, the reason I did this is because it made my testing 
easier (fewer stubs to write), but the positive side effect is it 
reduced the coupling between classes.  So, now I'll go ahead and write 
my stubs:

class RaceStub:
   def __init__ (self, appendages):
      self.appendages = appendages

   def getPrehensileAppendages (self):
      return self.appendages

class WorldStub:
    def getWeapon (self):
      return "roto-plooker"

These are pretty simplistic, but they're good enough to test the 
operation of my Creature constructor.  Now, I'll write the unit tests 
(leaving out some boilerplate):

def setUp (self):
   protoElephant = RaceStub (["arm1", "arm2", "proboscus"])
   ameboid = RaceStub ([])
   world = WorldStub ()
   self.creature1 = Creature (protoElephant, world)
   self.creature2 = Creature (ameboid, world)

def testCreation (self):
   self.assertEquals (len (self.creature1.weapons, 3))
   self.assertEquals (len (self.creature2.weapons, 0))

It doesn't seem like I've done much, but in fact I've done quite a lot.  
I've proven that I can create a Creature, and explored one of the 
possible corner cases, that of a race which doesn't have any hands in 
which to hold weapons.

In fact, when I run the tests, what I'll really discover is that I've 
got a logic error in my constructor; where I wrote "weapons", I should 
have written "self.weapons".  I did this on purpose to illustrate a 
point.  As written above, the constructor executes without any errors, 
and leaves a defective object which the unit test code catches.

Now, you may complain, I've written more lines of test code than of 
production code.  Yeah, sometimes that happens.  But the test code is 
usually pretty quick to write (especially if your classes are well 
designed), and the payback in the long run is worth it.

The next evolutionary step is "test first", but I'll leave that for 
another post.



More information about the Python-list mailing list