Friday, December 2, 2011

Test-driven Development: Implementing TypedList (part 1)

As I've been contemplating the next steps in the DataConnectors module, I've been waffling back and forth on how best to implement certain aspects of it. That's usually a sign, for me at least, that I need to take a step back and let things percolate for a while. I mentioned that I'd want/need to implement the TypedList before I got to the concrete classes of DataConnectors anyway, so:

In today's post, I'm going to tackle the creation of the TypedList class in the TypedContainers module. The shared code can be found at dl.dropbox.com/u/1917253/site-packages/TypedContainers.py, as noted when I posted the TypedDictionary some time back. I'm going to approach this from a slightly different angle, though - I'm going to start with as little as I can on the class-definition, build out the unit-tests that it needs in order to do what it needs to do, and write code until the tests pass. If this sounds familiar to you, then you're probably already using Test-Driven Development (TDD).

Part of the reason I'm taking this approach is because the standard unit-test structure that I use (specifically the testPropertyCountAndTests and testMethodCountAndTests test-methods that've been part of all of the recent test-code I've shown make the discovery of the underlying methods and their signatures fairly easy, though it can also be a bit on the tedious side when there's a significant number of properties or methods to be discovered. Another option, and one that might be considerably faster, is to just build a stub-class extending the base that we're starting with (a list in this case), then take a look at it's members with dir() or with pydoc. Use whatever works best, or is easiest for you.

So, to start with, we'll define the TypedList class with as little to it as we can manage. That means that it must inherit from the Python list, and, since we're going to be using the MemberTypes functionality provided by BaseTypedCollection, it needs to inherit from that as well. The bare-bones starting-point, then looks something like this:

class TypedList( BaseTypedCollection, list ):
    """Represents a strongly-typed List."""

    ##################################
    # Class Attributes               #
    ##################################

    ##################################
    # Class Property-Getter Methods  #
    ##################################

    ##################################
    # Class Property-Setter Methods  #
    ##################################

    ##################################
    # Class Property-Deleter Methods #
    ##################################

    ##################################
    # Class Properties               #
    ##################################

    ##################################
    # Object Constructor             #
    ##################################

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'memberTypes', None, '(Iterable, required) An iterable collection of item-types to be allowed as members in the list.' )
    def __init__( self, memberTypes ):
        """Object constructor."""
        # Nominally final: Don't allow any class other than this one
        if self.__class__ != TypedList:
            raise NotImplementedError( 'TypedList is (nominally) a final class, and is not intended to be derived from.' )
        self._SetMemberTypes( memberTypes )
        raise NotImplementedError( 'TypedList is not implemented yet.' )

    ##################################
    # Object Destructor              #
    ##################################

    ##################################
    # Class Methods                  #
    ##################################

__all__ += [ 'TypedList' ]

Next, and more importantly, I'm going to generate the unit-tests for the Typedlist class. I'm going to start with only my standard unit-test stub-code, in order to show how it can be used to reveal the underlying properties and methods of other objects. It's starting-point, then is very simple:

    class TypedListDerived( TypedList ):
        def __init__( self ):
            pass
    
    class testTypedList( unittest.TestCase ):
        """Unit-tests the TypedList class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
#        def testDerived( self ):
#            """Testing abstract nature of the TypedList class."""
#            pass
    
#        def testFinal( self ):
#            """Testing final nature of the TypedList class."""
#            pass
    
        def testConstruction( self ):
            """Testing construction of the TypedList class."""
            pass
    
        def testPropertyCountAndTests( self ):
            """Testing the properties of the TypedList class."""
            items = getMemberNames( TypedList )[0]
            actual = len( items )
            expected = 0
            self.assertEquals( expected, actual, 'TypedList is expected to have %d properties to test, but %d were dicovered by inspection.' % ( expected, actual ) )
            for item in items:
                self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s property (test%s), but none was identifiable.' % ( item, item ) )

        def testMethodCountAndTests( self ):
            """Testing the methods of the TypedList class."""
            items = getMemberNames( TypedList )[1]
            actual = len( items )
            expected = 0
            self.assertEquals( expected, actual, 'TypedList is expected to have %d methods to test, but %d were dicovered by inspection.' % ( expected, actual ) )
            for item in items:
                self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s method (test%s), but none was identifiable.' % ( item, item ) )

    testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testTypedList ) )

With that basic test-stub in place, I'm going to run my module, and see what the unit-tests reveal (I'll trim the detailed code-dumps, though, and just show the results):

Running unit-tests
###############################################################################
Unit-tests failed (see items below)
###############################################################################
Failures
#-----------------------------------------------------------------------------#
AssertionError: TypedList is expected to have 0 methods to test, but 1 were 
dicovered by inspection.
#-----------------------------------------------------------------------------#
AssertionError: TypedList is expected to have 0 properties to test, but 1 were 
dicovered by inspection.
#-----------------------------------------------------------------------------#
###############################################################################
So, immediately, we can see that there's at least one property and one method that need to be added to our test-cases. My next step is to see what those items are, and in order to do that, I'll change the expected values in testPropertyCountAndTests and testMethodCountAndTests to match what the test-output results are expecting, then re-run the tests:
Running unit-tests
###############################################################################
Unit-tests failed (see items below)
###############################################################################
Failures
#-----------------------------------------------------------------------------#
AssertionError: There should be a test for the IsValidMemberType method 
(testIsValidMemberType), but none was identifiable.
#-----------------------------------------------------------------------------#
AssertionError: There should be a test for the MemberTypes property 
(testMemberTypes), but none was identifiable.
#-----------------------------------------------------------------------------#
###############################################################################
So, already, the standard unit-test structure has shown that we need to create testIsValidMemberType to test the IsValidMemberType method that TypedList inherits from BaseTypedCollection, and the MemberTypes property, also from BaseTypedCollection.

Since those are actually implemented in BaseTypedCollection, all we really need to test for is that they are the inherited members, though:

        # Test Properties
        def testMemberTypes( self ):
            """Unit-tests the MemberTypes property of the TypedList class."""
            self.assertEquals( TypedList.MemberTypes, BaseTypedCollection.MemberTypes, 'TypedList should inherit the MemberTypes property of BaseTypedCollection.' )

        # Test Methods
        def testIsValidMemberType( self ):
            """Unit-tests the IsValidMemberType method of the TypedList class."""
            self.assertEquals( TypedList.IsValidMemberType, BaseTypedCollection.IsValidMemberType, 'TypedList should inherit the IsValidMemberType method of BaseTypedCollection.' )

Once those test-methods are in place, we re-run the tests:

###############################################################################
Running unit-tests
###############################################################################
All unit-tests passed! Ready for build/copy!
 + Copied TypedContainers.py to ~/.local/lib/python2.6/site-packages
###############################################################################
Et viola! We now have the members inherited from BaseTypedCollection confirmed. In retrospect, these same two tests should also be added to TypedDictionary, as well as the testMethodCountAndTests and testPropertyCountAndTests test-methods, which I'm going to do now, before I forget. They didn't cause any test-failures, so that was an easy addition...

That doesn't even begin to cover the members (public or otherwise) of a list, though... If we take a look at a list-instance in a terminal, we see that there are actually a lot of members (I actually used a quick little hacked-up script to generate this list so I could show what types of members they all are):

  • __add__ ............ <method-wrapper '__add__'...
  • __class__ .......... <type 'list'>
  • __contains__ ....... <method-wrapper '__contains__'...
  • __delattr__ ........ <method-wrapper '__delattr__'...
  • __delitem__ ........ <method-wrapper '__delitem__'...
  • __delslice__ ....... <method-wrapper '__delslice__'...
  • __doc__ ............ list() -> new empty list
    list(iterable) -> new list initialized from iterable's items
  • __eq__ ............. <method-wrapper '__eq__'...
  • __format__ ......... <built-in method __format__...
  • __ge__ ............. <method-wrapper '__ge__'...
  • __getattribute__ ... <method-wrapper '__getattribute__'...
  • __getitem__ ........ <built-in method __getitem__...
  • __getslice__ ....... <method-wrapper '__getslice__'...
  • __gt__ ............. <method-wrapper '__gt__'...
  • __hash__ ........... None
  • __iadd__ ........... <method-wrapper '__iadd__'...
  • __imul__ ........... <method-wrapper '__imul__'...
  • __init__ ........... <method-wrapper '__init__'...
  • __iter__ ........... <method-wrapper '__iter__'...
  • __le__ ............. <method-wrapper '__le__'...
  • __len__ ............ <method-wrapper '__len__'...
  • __lt__ ............. <method-wrapper '__lt__'...
  • __mul__ ............ <method-wrapper '__mul__'...
  • __ne__ ............. <method-wrapper '__ne__'...
  • __new__ ............ <built-in method __new__...
  • __reduce__ ......... <built-in method __reduce__...
  • __reduce_ex__ ...... <built-in method __reduce_ex__...
  • __repr__ ........... <method-wrapper '__repr__'...
  • __reversed__ ....... <built-in method __reversed__...
  • __rmul__ ........... <method-wrapper '__rmul__'...
  • __setattr__ ........ <method-wrapper '__setattr__'...
  • __setitem__ ........ <method-wrapper '__setitem__'...
  • __setslice__ ....... <method-wrapper '__setslice__'...
  • __sizeof__ ......... <built-in method __sizeof__...
  • __str__ ............ <method-wrapper '__str__'...
  • __subclasshook__ ... <built-in method __subclasshook__...
  • append ............. <built-in method append...
  • count .............. <built-in method count...
  • extend ............. <built-in method extend...
  • index .............. <built-in method index...
  • insert ............. <built-in method insert...
  • pop ................ <built-in method pop...
  • remove ............. <built-in method remove...
  • reverse ............ <built-in method reverse...
  • sort ............... <built-in method sort...
The reason that the various public members from append through sort at the end of this list don't raise errors in the methods-count test is because they are built-ins, rather than "normal" methods. It would be possible to add a testBuiltinCountAndTests test-method, using the same pattern as testMethodCountAndTests and testPropertyCountAndTests, but that would require modification of UnitTestUtilities that I don't want to get into right now (I'll note it for later addition, though, don't worry...).

Instead, I'm going to take a look at the pydoc output for my class, and/or dig around a bit in the Python list documentation and Emulating container types documentation for some guidance on what all of these members do. As a fall-back, in case I don't have network-access to check the online documentation, I could also create a simple list-extension class, override all of it's methods to print out that they were called (and with what arguments) before calling the parent list-methods, and see what happens when I do normal list-manipulation. I'll need to look at those normal list-manipulations anyway in order to build out unit-tests for basic list capabilities for the TypedList, so that has benefits beyond just figuring out what each method does. But that'd be time-consuming, with 45(!) items to look into, so I'm going to check the documentation first...

And that seems like a good spot to stop for today.

No comments:

Post a Comment