Wednesday, November 9, 2011

The UnitTestUtilities Module

Shared-code location: dl.dropbox.com/u/1917253/site-packages/UnitTestUtilities.py

Earlier I promised to share the UnitTestUtilities and DocMeta modules. Today, it'll be UnitTestUtilities.

The rationale for this chunk of code was, quite simply, that I found I needed a way to make sure that all of the items that needed to be tested for a given class, abstract class, or interface would be automatically identified in as simple a way as possible.

The test-cases template in the module-template I shared yesterday provides just such hooks (for properties and methods, at least) in the testPropertyCountAndTests and testMethodCountAndTests test-methods, you may recall. Those, in turn depend on two functions provided by UnitTestUtilities. Here's what that code looks like at present.
# UnitTestUtilities.py
"""Provides some utility functions to assist in assuring code-coverage for unit-tests.

These functions are predicated on a test structure that looks something like so:

########################################

    class ClassNameDerived( ClassName ):
        def __init__( self ):
            pass
    
    class testClassName( unittest.TestCase ):
        \"\"\"Unit-tests the ClassName class.\"\"\"
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
#        def testDerived( self ):
#            \"\"\"Testing abstract nature of the ClassName class.\"\"\"
#            pass
    
#        def testFinal( self ):
#            \"\"\"Testing final nature of the ClassName class.\"\"\"
#            pass
    
        def testConstruction( self ):
            \"\"\"Testing construction of the ClassName class.\"\"\"
            pass
    
        def testPropertyCountAndTests( self ):
            \"\"\"Testing the properties of the ClassName class.\"\"\"
            items = getMemberNames( ClassName )[0]
            actual = len( items )
            expected = 0
            self.assertEquals( expected, actual, 'ClassName 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 ClassName class.\"\"\"
            items = getMemberNames( ClassName )[1]
            actual = len( items )
            expected = 0
            self.assertEquals( expected, actual, 'ClassName 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 ) )

########################################

The functions, as used in testPropertyCountAndTests and testMethodCountAndTests, 
assure that there is a test[MethodName] and test[PropertyName] for each *public* 
method and property, respectively, and raise assertion errors if they don't exist. 
It is up to the developer to generate those test-methods and to assure that they 
provide complete coverage (at least so far).

"""

__author__  = 'Brian D. Allbee'
__licence__ = 'http://creativecommons.org/licenses/by-sa/3.0/'
__version__ = '0.2'
__copyright__ = "Copyright 2011, Brian D. Allbee"
__credits__ = ["Brian D. Allbee"]
__maintainer__ = "Brian D. Allbee"
__email__ = "brian.allbee@gmail.com"
__status__ = "Development"

#####################################
# Build an "__all__" list to        #
# support                           #
# "from Namespace.blah import *"    #
# syntax                            #
#####################################
__all__ = []

#####################################
# Required imports                  #
#####################################

import inspect, types
from DocMeta import *

#####################################
# Functions defined in the module   #
#####################################

@ToDo( 'Unit-test the function' )
@ToDo( 'Document exceptions raised' )
@ToDo( 'Value-check arguments' )
@ToDo( 'Type-check arguments' )
@DocumentArgument( 'argument', 'theClass', None, '(Class, required) The class to get property and method names from.' )
@DocumentReturn( types.TupleType )
def getMemberNames( theClass ):
    """Returns a list of property- and method-names for the specified class."""
    classMembers = inspect.getmembers( theClass )
    properties = []
    methods = []
    for member in classMembers:
        if inspect.isdatadescriptor( member[1] ) and member[0][0] != '_':
            properties.append( member[0] )
        if inspect.ismethod( member[1] ) and member[0][0] != '_':
            methods.append( member[0] )
    return properties, methods

__all__ += [ 'getMemberNames' ]

@ToDo( 'Unit-test the function' )
@ToDo( 'Document exceptions raised' )
@ToDo( 'Value-check arguments' )
@ToDo( 'Type-check arguments' )
@DocumentArgument( 'argument', 'testCase', None, '(unittest.TestCase, required) The TestCase instance to check for the named test-method in.' )
@DocumentArgument( 'argument', 'itemName', None, '(Class, required) The name of the property or method to look for a test-method for ("test[itemName]").' )
@DocumentReturn( types.BooleanType )
def HasTestFor( testCase, itemName ):
    """Returns true if the testCase provided has a test-method named 'test[itemName]'."""
    classMembers = inspect.getmembers( testCase )
    nameToFind = 'test%s' % ( itemName )
    for member in classMembers:
        if member[0] == nameToFind:
            return True
    return False

__all__ += [ 'HasTestFor' ]

#####################################
# Interfaces defined in the module  #
#####################################

#####################################
# Abstract Classes defined in the   #
# module                            #
#####################################

#####################################
# Classes defined in the module     #
#####################################

#####################################
# Package sub-modules and -packages #
#####################################

#####################################
# Unit-test the module on main      #
#####################################

if __name__ == '__main__':
    import inspect, os, sys, unittest

    testSuite = unittest.TestSuite()
    testResults = unittest.TestResult()

    #################################
    # Unit-test Constants           #
    #################################

    class testConstants( unittest.TestCase ):
        """Unit-tests the constants defined in the module."""
        # No constants to test
        pass
    
    testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testConstants ) )

    #################################
    # Unit-test Functions           #
    #################################

    class Parent( object ):
        ParentPublic1 = property()
        ParentPublic2 = property()
        _ParentProtected1 = property()
        _ParentProtected2 = property()
        __ParentPrivate1 = property()
        __ParentPrivate2 = property()
        ParentOverrideMe1 = property()
        def __init__( self ): pass
        def ParentMethod1( self ): pass
        def ParentMethod2( self ): pass
        def _ParentMethod1( self ): pass
        def _ParentMethod2( self ): pass
        def __ParentMethod1( self ): pass
        def __ParentMethod2( self ): pass
        def ParentOverrideMe1( self ): pass

    class Child( Parent, object ):
        ChildPublic1 = property()
        ChildPublic2 = property()
        _ChildProtected1 = property()
        _ChildProtected2 = property()
        __ChildPrivate1 = property()
        __ChildPrivate2 = property()
        ChildOverrideMe1 = property()
        def __init__( self ): pass
        def ChildMethod1( self ): pass
        def ChildMethod2( self ): pass
        def _ChildMethod1( self ): pass
        def _ChildMethod2( self ): pass
        def __ChildMethod1( self ): pass
        def __ChildMethod2( self ): pass
        def ParentOverrideMe1( self ): pass

    class BadParentTestCase( object ): pass

    class GoodParentTestCase( object ):
        def testParentPublic1( self ): pass
        def testParentPublic2( self ): pass
        def testParentMethod1( self ): pass
        def testParentMethod2( self ): pass
        def testParentOverrideMe2( self ): pass

    class BadChildTestCase( object ): pass

    class GoodChildTestCase( object ):
        def testParentPublic1( self ): pass
        def testParentPublic2( self ): pass
        def testParentMethod1( self ): pass
        def testParentMethod2( self ): pass
        def testChildPublic1( self ): pass
        def testChildPublic2( self ): pass
        def testChildMethod1( self ): pass
        def testChildMethod2( self ): pass
        def testParentOverrideMe2( self ): pass

    class testFunctions( unittest.TestCase ):
        """Unit-tests the module's functions."""
        
        def testgetMemberNames( self ):
            """Tests the getMemberNames function."""
            self.assertEquals( getMemberNames( Parent ), 
                (
                    ['ParentPublic1', 'ParentPublic2'], 
                    ['ParentMethod1', 'ParentMethod2', 'ParentOverrideMe1']
                )
            )
            self.assertEquals( getMemberNames( Child ), 
                (
                    ['ChildOverrideMe1', 'ChildPublic1', 'ChildPublic2', 'ParentPublic1', 'ParentPublic2'], 
                    ['ChildMethod1', 'ChildMethod2', 'ParentMethod1', 'ParentMethod2', 'ParentOverrideMe1']
                )
            )
        
        def testHasTestFor( self ):
            """Tests the HasTestFor function."""
            goodCases = {
                GoodParentTestCase:[
                    'ParentPublic1',
                    'ParentPublic2',
                    'ParentMethod1',
                    'ParentMethod2',
                    'ParentOverrideMe2',
                    ],
                GoodChildTestCase:[
                    'ParentPublic1',
                    'ParentPublic2',
                    'ParentMethod1',
                    'ParentMethod2',
                    'ParentOverrideMe2',
                    'ChildPublic1',
                    'ChildPublic2',
                    'ChildMethod1',
                    'ChildMethod2',
                    ],
                }
            for testCase in goodCases:
                caseItems = goodCases[ testCase ]
                for item in caseItems:
                    self.assertTrue( HasTestFor( testCase, item ), 'There should be a test-method in %s for %s (test%s)' % ( testCase.__name__, item, item ) )
            badCases = {
                BadParentTestCase:[
                    'ParentPublic1',
                    'ParentPublic2',
                    'ParentMethod1',
                    'ParentMethod2',
                    'testParentOverrideMe2',
                    ],
                BadChildTestCase:[
                    'ParentPublic1',
                    'ParentPublic2',
                    'ParentMethod1',
                    'ParentMethod2',
                    'ParentOverrideMe2',
                    'ChildPublic1',
                    'ChildPublic2',
                    'ChildMethod1',
                    'ChildMethod2',
                    ],
                }
            for testCase in badCases:
                caseItems = badCases[ testCase ]
                for item in caseItems:
                    self.assertFalse( HasTestFor( testCase, item ), 'There should not be a test-method in %s for %s (test%s)' % ( testCase.__name__, item, item ) )

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

    #################################
    # Unit-test Interfaces          #
    #################################

    #################################
    # Unit-test Abstract Classes    #
    #################################

    #################################
    # Unit-test Classes             #
    #################################

    # Run unit-tests and install
    print '#'*79
    print 'Running unit-tests'
    testSuite.run( testResults )
    print '#'*79
    if testResults.errors == [] and testResults.failures == []:
        print 'All unit-tests passed! Ready for build/copy!'
        import shutil
        sitePackagesDirs = []
        for path in sys.path:
            if path.split( os.sep )[-1].lower() == 'site-packages':
                sitePackagesDirs.append( path )
        for path in sitePackagesDirs:
            destinationFile = path + os.sep + __file__
            try:
                shutil.copyfile( './%s' % __file__, destinationFile )
                print 'Copied %s to %s' % ( __file__, path )
            except:
                print 'Could not copy file %s to location %s' % ( __file__, path )
    else:
        print 'Unit-tests failed (see items below)'
        print '#'*79
        if testResults.errors:
            print 'Errors'
            print '#' + '-'*77 + '#'
            for error in testResults.errors:
                print error[1]
                print '#' + '-'*77 + '#'
            print
        if testResults.failures:
            if testResults.errors:
                print '#'*79
            print 'Failures'
            print '#' + '-'*77 + '#'
            for failure in testResults.failures:
                print failure[1]
                print '#' + '-'*77 + '#'
            print
    print '#'*79

You might note that there's several @ToDo annotations here — I'm planning on getting to each of those in the near future, but didn't want to hold up sharing this code for want of those items, particularly as the main unit-tests are functional and pass...

No comments:

Post a Comment