Monday, December 5, 2011

Test-driven Development: Implementing TypedList (part 2)

The next step, if we approach this in a TDD fashion, is probably to define our unit-tests. Technically, this step probably should've been undertaken earlier, perhaps even before starting to track down the methods that we'd need to override, but since I expected that there were going to be a lot of them (or at least a lot to evaluate), I wanted to get that started fairly early. The unit-tests need to encompass all of the expected uses that we can put a TypedList to, and need to include both passing and failing (negative) tests in order to assure that they're covering the code sufficiently.

At the minimum, that means that we need to test each of the following cases:

  • Instantiation of a TypedList with values presented to the constructor;
  • Appending items to a TypedList with +;
  • Appending items to a TypedList with +=;
  • Appending items to a TypedList with append;
  • Appending (or extending) items to a TypedList with extend;
  • Setting the value in a specific TypedList location with TypedList[n] = value;
  • Setting one or more values in a slice of a TypedList with TypedList[i:j] = value;
  • Inserting a value into a specific TypedList location with insert

Given that we need to test all of these both positively (to prove that basic list behavior for allowed types is preserved) and negatively (to raise errors when they are presented with an invalid type), there are a lot of assertions that have to be made in the testListBehavior test-method. Most of the rest of the test-methods are pretty typical of the pattern I've followed thus far, so I won't spend a lot of time commenting on those.

Unit-tests

    class TypedListDerived( TypedList ):
        def __init__( self, memberTypes, iterable=None ):
            TypedList.__init__( self, memberTypes, iterable )
    
    class testTypedList( unittest.TestCase ):
        """Unit-tests the TypedList class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass

        def testFinal( self ):
            """Testing final nature of the TypedList class."""
            try:
                testObject = TypedListDerived( [ types.IntType ] )
                self.fail( 'TypedList is nominally a final class, and is not intended to be extended.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'TypedList is expected to raise NotImplementedError if a derived class is instantiated, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            testObject = TypedList( [ types.IntType ] )
            self.assertEquals( testObject, [], 'A Typedlist constructed with no values should equal an empty list.' )

        def testPropertyCountAndTests( self ):
            """Testing the properties of the TypedList class."""
            items = getMemberNames( TypedList )[0]
            actual = len( items )
            expected = 1
            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 = 4
            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 ) )

        # Test list behavior
        def testListBehavior( self ):
            """Unit-tests list-like behavior of TypedList."""
            # Test good values - all should pass
            goodValues = [ -1, 0, 1, 2, -1L, 0L, 1L, 2L, -1.0, 0.0, 1.0, 2.0 ]
            # Test creation with an iterable
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ], goodValues )
            self.assertEquals( testObject, goodValues, 'Values set in a TypedList at creation should act like values set in a normal list at creation.' )
            # Test L = L + i structure
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ] )
            compareList = []
            for testValue in goodValues:
                compareList = compareList + [ testValue ]
                testObject = testObject + [ testValue ]
                self.assertEquals( testObject, compareList, 'Values added to a TypedList should act like values added to a normal list.' )
            # Test L = L += i structure
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ] )
            compareList = []
            for testValue in goodValues:
                compareList += [ testValue ]
                testObject += [ testValue ]
                self.assertEquals( testObject, compareList, 'Values added to a TypedList should act like values added to a normal list.' )
            # Test L.append equivalent
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ] )
            compareList = []
            for testValue in goodValues:
                compareList.append( testValue )
                testObject.append( testValue )
                self.assertEquals( testObject, compareList, 'Values appended to a TypedList should act like values appended to a normal list.' )
            # Test L.extend equivalent
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ] )
            compareList = []
            for testValue in goodValues:
                compareList.extend( [ testValue ] )
                testObject.extend( [ testValue ] )
                self.assertEquals( testObject, compareList, 'Values extended to a TypedList should act like values extended to a normal list.' )
            # Test L.insert equivalent
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ] )
            compareList = []
            for testValue in goodValues:
                compareList.insert( 0, testValue )
                testObject.insert( 0, testValue )
                self.assertEquals( testObject, compareList, 'Values inserted to a TypedList should act like values inserted to a normal list.' )
            # Test L[index] = value equivalent
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ], [ 0 ] )
            compareList = [ 0 ]
            for testValue in goodValues:
                compareList[0] = testValue
                testObject[0] = testValue
                self.assertEquals( testObject, compareList, 'Values set in a position in a TypedList should act like values set in a position in a normal list.' )
            # Test L[slice] = [ i, j ] equivalent
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ], [ 0, 0, 0 ] )
            compareList = [ 0, 0, 0 ]
            goodValues = [ [ -1, 0 ], [ 1, 2 ] ]
            for testValue in goodValues:
                compareList[0:1] = testValue
                testObject[0:1] = testValue
                self.assertEquals( testObject, compareList, 'Values set in a slice position in a TypedList should act like values set in a slice position in a normal list.' )
            # Test bad values - all should raise errors
            badValues = [ None, 'string', [ 'ook' ] ]
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ] )
            # Test L = L + i structure
            for testValue in badValues:
                try:
                    testObject = testObject + testValue
                    self.fail( 'Adding %s with "+" to a TypedList that doesn\'t allow that type should raise a TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Adding %s with "+" to a TypedList that doesn\'t allow that type should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            # Test L = L += i structure
            for testValue in badValues:
                try:
                    testObject += testValue
                    self.fail( 'Adding %s with "+=" to a TypedList that doesn\'t allow that type should raise a TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Adding %s with "+=" to a TypedList that doesn\'t allow that type should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            # Test L.append equivalent
            for testValue in badValues:
                try:
                    testObject.append( testValue )
                    self.fail( 'Appending %s to a TypedList that doesn\'t allow that type should raise a TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Appending %s to a TypedList that doesn\'t allow that type should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            # Test L.extend equivalent
            for testValue in badValues:
                try:
                    testObject.extend( [ testValue ] )
                    self.fail( 'Extending %s to a TypedList that doesn\'t allow that type should raise a TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Extending %s to a TypedList that doesn\'t allow that type should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            # Test L.insert equivalent
            for testValue in badValues:
                try:
                    testObject.insert( 0, testValue )
                    self.fail( 'Inserting %s to a TypedList that doesn\'t allow that type should raise a TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Inserting %s to a TypedList that doesn\'t allow that type should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            # Test L[index] = value equivalent
            for testValue in badValues:
                try:
                    testObject[ 0 ] = testValue
                    self.fail( 'Setting %s to a position in a TypedList that doesn\'t allow that type should raise a TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting %s to a position in a TypedList that doesn\'t allow that type should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            # Test L[slice] = [ i, j ] equivalent
            badValues = [ [ None, 'string' ], [ 'string', None ] ]
            testObject = TypedList( [ types.IntType, types.LongType, types.FloatType ], [ 0, 0, 0 ] )
            for testValue in badValues:
                try:
                    testObject[ 0:1 ] = testValue
                    self.fail( 'Setting %s to a slice-position in a TypedList that doesn\'t allow that type should raise a TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting %s to a slice-position in a TypedList that doesn\'t allow that type should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )

        # 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 testappend( self ):
            """Unit-tests the append method of the TypedList class."""
            pass # This is tested in testListBehavior, above.

        def testextend( self ):
            """Unit-tests the extend method of the TypedList class."""
            pass

        def testinsert( self ):
            """Unit-tests the insert method of the TypedList class."""
            pass # This is tested in testListBehavior, above.

        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.' )

    testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testTypedList ) )
Line(s)
48
Most of these tests will need a simple baseline list to work with, or to draw test-values from. The TypedList instance I'm going to work with will allow integers, floats amd long integers, so all of the "good" values must fall into one of those types. I also want to make sure that it handles positive, zero and negative values correctly, and it never hurts to test for even/odd differences either, which is why there's a value of 2 of each of those types as well.
50, 53, 60, 67, etc.
In order to make sure that each test has a fresh, clean value, I'm creating a new TypedList instance each time.
54, 61, 68, etc.
All of the "good" value tests are based around the concept that if we perform the same actions/operations (with the same value[s]) on a TypedList instance as we do on an equivalent list instance, the end results will be identical. If that proves not to be the case, then the code has broken core list-functionality. Again, in order to assure that there's a fresh comparison-list copy each time, a new list is created.
50, 51
Standard lists can be initialized with an iterable set of values, so a TypedList needs to be able to do that as well.
55-58
Tests the "+" operator as it applies to TypedLists
62-65
Tests the "+=" operator as it applies to TypedLists
69-72
Tests the append method - this could be broken out into the dedicated test-method created specifically for it (testappend), but it feels better to keep this test-code here along with the other list-behavior tests.
76-79
Tests the extend method - this could be broken out into the dedicated test-method created specifically for it (testextend), but it feels better to keep this test-code here along with the other list-behavior tests.
83-86
Tests the insert method - this could be broken out into the dedicated test-method created specifically for it (testinsert), but it feels better to keep this test-code here along with the other list-behavior tests.
90-93
Tests the ability to set a value at a specific index-location in the TypedList.
97
For the slice-value test, we need a slightly different good-values structure, so we reset it here before proceeding.
98-101
Tests the ability to set values to a slice-location in the TypedList.
102-169
These are the negative tests for the same TypedList functionality and capabilities that have been positively tested in the previous code. They are somewhat simpler, in that there is no comparison that needs to be made, though it might not be a bad idea to check at each iteration that the values in the underlying list haven't changed, but it doesn't feel necessary at this point - if they do, that feels to me like there's something greviously wrong at a level in the language itself that I won't be able to fix anyway.

That's a fair amount of code for today, even if it is all unit-tests, so I'll stop here, and finish up with the "real" development in my next post. Bear in mind that we still don't have the actual TypedList functionality built at all yet - and that the unit-tests will fail, because that functionality doesn't exist. At this point in time, that's OK - when all of the "real" code is writtem and all of our tests pass, we're done! That ought to go pretty quickly, though.

No comments:

Post a Comment