Showing posts with label BaseDataConnection. Show all posts
Showing posts with label BaseDataConnection. Show all posts

Wednesday, January 4, 2012

The MySQLConnector module

Happy New Year, readers! The bulk of today's code is shared at dropbox.com/u/1917253/site-packages/MySQLConnector.py.

So the last leg of our journey into Python-database connectivity is (finally) the MySQLConnector class. There's been a lot of what I'd call "support" development to get to this point:

  • Three interfaces (IsDataConnector, IsQuery, and IsResultSet);
  • One abstract class (BaseDataConnector); and
  • Three concrete classes (Query, ResultSet and Record).
These took considerably longer to write about than I'd expected, though I'm glad that they provided enough material for the number of posts that they generated. There's also a fair amount of code that had to be written: With unit-tests, the DataConnectors module is just barely closer to 2,400 lines of code than 2,300, in about a 40:60 split between "real" code and test-code.

One (hopefully significant) advantage of this approach is that the creation of the MySQLConnector class is going to be crazy-simple. As a derivative of both BaseDataConnector and IsConfigurable, there's really not a whole lot of code that's specific to the MySQLConnector to be written, and other similar classes (say, a hypothetical PostGreSQLConnector) would require a similarly small level of effort. Consider that MySQLConnector has the following properties and methods:

  • The Configure method, from IsConfigurable, which may need specific implementation, but that was implemented at a basic level in BaseDataConnector.
  • The Connect method, required by IsDataConnector, which will need implementation.
  • The Database property, from BaseDataConnector, that is complete and tested until or unless we need to make implementation-specific changes.
  • The Execute method, required by IsDataConnector, which will need implementation.
  • The Host property, from BaseDataConnector, also complete and tested, barring implementation-specific changes.
  • The Password property, from BaseDataConnector, also complete and tested, barring implementation-specific changes.
  • The User property, from BaseDataConnector, also complete and tested, barring implementation-specific changes.
  • And, of course, unit-tests for the new class. Ideally, there should also be some system tests to test/prove out functionality against an actual database.

As I was working on MySQLConnector, I discovered that I'd never finished figuring out how to unit-test the Configure method of it, and that I was missing a Configuration class:

Configuration (from the Configuration module):

class Configuration( ConfigParser.ConfigParser, object ):
    """Represents configuration data derived from a text-file."""

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

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

    def _GetConfigFile( self ):
        """Gets the config-file-path specified at object creation."""
        return self._configFile

    def _GetSections( self ):
        """Gets the available configuration-sections."""
        if self._configSections == None:
            try:
                self.read( self._configFile )
            except Exception, error:
                raise AttributeError( '%s.Sections: Could not read configuration from %s. Original error:\n\t%s' % ( self.__class__.__name__, self._configFile, error ) )
            self._configSections = {}
            for theSection in self.sections():
                self._configSections[ theSection ] = {}
                for theItem in self.items( theSection, 1 ):
                    itemName = theItem[0]
                    itemValue = theItem[1]
                    if itemValue[0] in [ '[', '{', '(' ]:
                        # Evaluate the value instead... Ugly, but viable...
                        itemValue = eval( itemValue )
                    self._configSections[ theSection ][ itemName ] = itemValue
        return self._configSections

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

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

    ConfigFile = property( _GetConfigFile, None, None, _GetConfigFile.__doc__ )
    Sections = property( _GetSections, None, None, _GetSections.__doc__ )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'filePath', None, '(String) The path to the configuration-file.' )
    def __init__( self, filePath ):
        """Object constructor."""
        ConfigParser.ConfigParser.__init__( self )
        self._configSections = None
        self._configFile = filePath

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

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

__all__ += [ 'Configuration' ]

This will likely need to be refactored at some point, since there are other configuration mechanisms than the ConfigParser base here. For now, though, I'll leave it be. Configuration instances are basically just wrappers around ConfigParser instances, that provide ConfigFile and Sections properties. The ConfigFile is probably pretty straightforward - it's a pointer to the configuration-file that the instance uses. Sections might be a little harder to understand, though - I wanted to have a simple access mechanism for the configuration-sections of a given configuration-file, in order to be able to retrieve section-items without having to make a function-call every time.

With that in place, I could finish the unit-tests for BaseDataConnector:

        def testConfigure( self ):
            """Unit-tests the Configure method of the BaseDataConnector abstract class."""
            # First, we need to build out a configuration-file
            hostName = 'hostName'
            databaseName = 'databaseName'
            userName = 'userName'
            userPassword = 'userPassword'
            configString = """[Default Section]
SectionName1: SectionValue1
SectionName2: SectionValue2

[Data Access]
host: %s
database: %s
user: %s
password: %s
""" % ( hostName, databaseName, userName, userPassword )
            fp = open( 'test.cfg', 'w' )
            fp.write( configString )
            fp.close()
            # Now we need to create the configuration object and read in the configuration values
            configParser = Configuration( 'test.cfg' )
            # Now, finally, we can test the configuration
            testObject = BaseDataConnectorDerived()
            testObject.Configure( configParser, 'Data Access' )
            self.assertEquals( testObject.Host, hostName, 'The host name should be retrievable from configuration' )
            self.assertEquals( testObject.Database, databaseName, 'The database name should be retrievable from configuration' )
            self.assertEquals( testObject.User, userName, 'The user name should be retrievable from configuration' )
            self.assertEquals( testObject.Password, userPassword, 'The user password should be retrievable from configuration' )
            os.unlink( 'test.cfg' )

All this is doing is creating a hard-coded (and very simple) configuration-file, with a Data Access section that contains the host, database and user names for the connector, and the password that it should use. Looking at lines 22 and 24-25, creation of a BaseDataConnector-derived instance should always be feasible with this sort of code-structure: Create an instance with no arguments, create a configuration instance, then call the Configure method of the data-connector instance.

It may bear noting that MySQLConnector doesn't have any sort of direct support for cursors - that may be something that I'll add in to BaseDataConnector at some point in the future, if there's support for it across MySQL (yes), PostgreSQL (not sure), and ODBC (also not sure), which are the three data-connection types I expect this to support in the long run.

MySQLConnector (Nominal final class):

#####################################
# Convenience imports               #
#####################################

# See To-do list on MySQLConnector.__init__

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

class MySQLConnector( BaseDataConnector, IsConfigurable, object ):
    """Class doc-string."""

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

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetConnection( self ):
        """Gets the connection to the specified MySQL database."""
        if self._connection == None:
            self.Connect()
        return self._connection

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The name of the database the connection will be made to.' )
    def _SetDatabase( self, value ):
        if self._connection != None:
            raise AttributeError( '%s.Database cannot be reset once a connection is established' % ( self.__class__.__name__ ) )
        BaseDataConnector._SetDatabase( self, value )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The name or IP address of the host that the database being connected to lives on.' )
    def _SetHost( self, value ):
        if self._connection != None:
            raise AttributeError( '%s.Host cannot be reset once a connection is established' % ( self.__class__.__name__ ) )
        BaseDataConnector._SetHost( self, value )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The password that will be used to connect to the database.' )
    def _SetPassword( self, value ):
        if self._connection != None:
            raise AttributeError( '%s.Password cannot be reset once a connection is established' % ( self.__class__.__name__ ) )
        BaseDataConnector._SetPassword( self, value )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The user-name that will be used to connect to the database.' )
    def _SetUser( self, value ):
        if self._connection != None:
            raise AttributeError( '%s.User cannot be reset once a connection is established' % ( self.__class__.__name__ ) )
        BaseDataConnector._SetUser( self, value )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelConnection( self ):
        self._connection = None

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

    Connection = property( _GetConnection, None, None, 'Gets the connection to the specified MySQL database.' )
    Database = property( BaseDataConnector._GetDatabase, _SetDatabase, BaseDataConnector._DelDatabase, BaseDataConnector._GetDatabase.__doc__ )
    Host = property( BaseDataConnector._GetHost, _SetHost, BaseDataConnector._DelHost, BaseDataConnector._GetHost.__doc__ )
    Password = property( BaseDataConnector._GetPassword, _SetPassword, BaseDataConnector._DelPassword, BaseDataConnector._GetPassword.__doc__ )
    User = property( BaseDataConnector._GetUser, _SetUser, BaseDataConnector._DelUser, BaseDataConnector._GetUser.__doc__ )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'keyword', 'parameters', 'host', 'The name or IP-address of the host that the database resides upon.' )
    @DocumentArgument( 'keyword', 'parameters', 'database', 'The name of the database that the connection will be made to.' )
    @DocumentArgument( 'keyword', 'parameters', 'user', 'The user-name used to connect to the database.' )
    @DocumentArgument( 'keyword', 'parameters', 'password', 'The password used to connect to the database.' )
    @ToDo( 'Figure out what items from DataConnectors should be imported for convenience in using the MySQLConnector class.' )
    def __init__( self, **parameters ):
        """Object constructor."""
        # Nominally final: Don't allow any class other than this one
        if self.__class__ != MySQLConnector:
            raise NotImplementedError( 'MySQLConnector is (nominally) a final class, and is not intended to be derived from.' )
        self._connection = None
        BaseDataConnector.__init__( self, **parameters )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def __del__( self ):
        self._Close()

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _Close( self ):
        if self._connection:
            self._connection.close()

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def Connect( self ):
        """Connects to the database specified by Database, residing on Host, using User and Password to connect."""
        self._connection = MySQLdb.connect( 
            host=self._host, 
            db=self._database, 
            user=self._user, 
            passwd=self._password
            )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'query', None, 'The IsQuery object that will be executed against the object\'s Connection.' )
    def Execute( self, query ):
        """Executes the provided IsQuery object's query against the object's connection, returning one or more results."""
        if not isinstance( query, IsQuery ):
            raise TypeError( '%s.Execute expects an instance of IsQuery as it\'s query argument.' % ( self.__class__.__name__ ) )
        self.Connection.query( query.Sql )
        resultSets = []
        resultSet = []
        results = self.Connection.store_result()
        while results:
            row = list( results.fetch_row( 1, 1 ) )
            while row:
                resultSet += row
                row = results.fetch_row( 1, 1 )
            if self.Connection.next_result() == 0:
                resultSets.append( resultSet )
                resultSet = []
                results = self.Connection.store_result()
            else:
                break
        resultSets.append( resultSet )
        return resultSets

__all__ += [ 'MySQLConnector' ]
Line(s)
5
This is a fine-tuning item that will almost certainly evolve in an obvious way as I start writing code that actually uses MySQLConnector: There will almost certainly be some (small?) set of imports from the base DataConnectors module that will be needed frequently enough to warrant importing them as part of the MySQLConnector import. Off the top of my head, I'd expect at least Query and maybe ResultSet to be needed, but I'll wait to see what actually crops up as I start using the module...
23-27
An almost-typical property getter method, the main difference being that the underlying MySQLdb Connection object isn't instantiated until it's needed.
33-59
Overridden property-setter methods. The only additional functionality that they provide is to prevent the modification of any connection-control property (the host, database- and user-name, and password used for the connection) once the connection has been instantiated. The rationale for this is, basically, that once a connection has been established, changes to that connection should not be allowed. Chances are good that an error of some type would be thrown anyway, but since I'd rather raise errors as close to their "real" source as possible, I'm explicitly causing that to happen here.
73-77
The actual property declarations. Since most of these are a mix of the functionality from BaseDataConnector (for the _Get... methods) and local implementations (_Set... methods), there's a mix of complete external method-references and "normal" local references to accommodate.
101-103, 109-112
I'm not sure that a formal object-destructor's actually needed, since the underlying MySQLdb Connection object probably closes the database connection all on it's own, but to be safe, I'm providing one that calls to the _Close method to make sure that the connection is closed cleanly when the object is destroyed.
The _Close method is necessary because when the destructor fires, it doesn't recognize object properties. I think I mentioned this same pattern somewhere before, but I cannot recall where, exactly.
114-122
A public method to open the database-connection with the instance's parameters.
124-144
The method that actually queries against the database, returning a list of lists of dictionaries, ready to be passed to ResultSet constructors.
128-129
Typical type-checking of the supplied query argument.
130
Run the provided Query's SQL against the database, readying the results for formatting/digestion.
131, 132
Prep the lists of all results, and the first list of rows for use.
133
Get (and store) the first result-set returned.
This uses store_result instead of use_result, in the belief that a well-designed application (and database) should rarely return so many records that storing them wouldn't provide faster throughput than keeping the database-connection open and retrieving result-sets one at a time.
134
For so long as there are results:
135
Get the first row of the current result-set
136-137
For so long as there's a valid row, add the row to the current result-set, and get the next row.
139-142
Once the current result's row-set has been processed, if there is another result-set after the current one, append the current results to the list to be returned, reset the row-list, and get/store the next result-set.
143-144
If there are no other result-sets, then exit the loop.
145
Make sure that the last set of rows gets attached to the results to be returned before returning them.

Unit-tests

Some of the unit-tests (the ones that are very basic system-tests, all in the testExecute method) require some actual tables on the test_database database:

CREATE TABLE `test_database`.`test_table_1` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'The record-id',
  `first_name` VARCHAR(30)  NOT NULL COMMENT 'A first name',
  `last_name` varchar(30)  NOT NULL COMMENT 'A last-name',
  PRIMARY KEY (`id`)
)
ENGINE = MyISAM
COMMENT = 'A test-table';

INSERT INTO test_table_1 ( first_name, last_name )
VALUES ( 'Brian', 'Allbee' ), ( 'Joe', 'Smith' );

CREATE TABLE `test_database`.`test_table_2` (
  `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Record-ID',
  `name_id` int  NOT NULL 
      COMMENT 'ID of the record from test_table_1 that relates to this record',
  `email` varchar(60)  NOT NULL COMMENT 'An email address',
  PRIMARY KEY (`id`)
)
ENGINE = MyISAM
COMMENT = 'Another test table';

INSERT INTO test_table_2 ( name_id, email )
VALUES ( 1, 'brian.allbee@somedomain.com' ), ( 2, 'joe.smith@somedomain.com' );
    class MySQLConnectorDerived( MySQLConnector ):
        def __init__( self, **parameters ):
            MySQLConnector.__init__( self, **parameters )
        def __del__( self ):
            try:
                MySQLConnector.__del__( self )
            except:
                pass
    
    class testMySQLConnector( unittest.TestCase ):
        """Unit-tests the MySQLConnector class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass

        def getLiveConnection( self ):
            """Returns a MySQLConnector instance pointing to a common local test-database."""
            return MySQLConnector( host='localhost', database='test_database', user='test_user', password='test_password' )
        
        def testFinal( self ):
            """Testing final nature of the MySQLConnector class."""
            try:
                testObject = MySQLConnectorDerived()
                self.fail( 'MySQLConnector is nominally a final class, and should not be extendable.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Instantiating a derivation of MySQLConnection should raise NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
    
        def testConstruction( self ):
            """Testing construction of the MySQLConnector class."""
            testObject = MySQLConnector()
            self.assertTrue( isinstance( testObject, MySQLConnector ), 'Instances of MySQLConnector should be instances of MySQLConnector.' )
            self.assertEquals( testObject.Host, None, 'With no host supplied at construction, it\'s property value should be None' )
            self.assertEquals( testObject.Database, None, 'With no database supplied at construction, it\'s property value should be None' )
            self.assertEquals( testObject.User, None, 'With no user supplied at construction, it\'s property value should be None' )
            self.assertEquals( testObject.Password, None, 'With no password supplied at construction, it\'s property value should be None' )
            testHost = 'testHost'
            testDatabase = 'database'
            testUser = 'testUser'
            testPassword = 'testPassword'
            testObject = MySQLConnector( host=testHost, database=testDatabase, user=testUser, password=testPassword )
            self.assertEquals( testObject.Host, testHost, 'The host supplied at construction should be present in the object\'s properties' )
            self.assertEquals( testObject.Database, testDatabase, 'The database supplied at construction should be present in the object\'s properties' )
            self.assertEquals( testObject.User, testUser, 'The user supplied at construction should be present in the object\'s properties' )
            self.assertEquals( testObject.Password, testPassword, 'The password supplied at construction should be present in the object\'s properties' )
    
        def testPropertyCountAndTests( self ):
            """Testing the properties of the MySQLConnector class."""
            items = getMemberNames( MySQLConnector )[0]
            actual = len( items )
            expected = 5
            self.assertEquals( expected, actual, 'MySQLConnector 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 MySQLConnector class."""
            items = getMemberNames( MySQLConnector )[1]
            actual = len( items )
            expected = 3
            self.assertEquals( expected, actual, 'MySQLConnector 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 ) )

        # Unit-test properties

        def testConnection( self ):
            """Unit-tests the Connection property of the MySQLConnector class."""
            testObject = MySQLConnector( host='localhost', database='no_such_database', user='test_user', password='test_password' )
            try:
                testObject.Connect()
                self.fail( 'Trying to connect to a non-existant database should raise OperationalError.' )
            except MySQLdb.OperationalError:
                pass
            except Exception, error:
                self.fail( 'Trying to connect to a non-existant database should raise OperationalError, but %s was raised instead:\n  %s.' % ( error.__class__.__name__, error ) )

        def testDatabase( self ):
            """Unit-tests the Database property of the MySQLConnector class."""
            testObject = self.getLiveConnection()
            oldDatabase = testObject.Database
            testObject.Database = 'ook'
            self.assertEquals( testObject.Database, 'ook', 'Before a connection is made, the object should allow modification of the Database property.' )
            testObject.Database = oldDatabase
            testObject.Connect()
            try:
                testObject.Database = 'ook'
                self.fail( 'Once a connection is established, the Database property should be immutable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Modifying the Database property after connection is establilshed should raise AttributeError, bu %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testHost( self ):
            """Unit-tests the Host property of the MySQLConnector class."""
            testObject = self.getLiveConnection()
            oldHost = testObject.Host
            testObject.Host = 'ook'
            self.assertEquals( testObject.Host, 'ook', 'Before a connection is made, the object should allow modification of the Host property.' )
            testObject.Host = oldHost
            testObject.Connect()
            try:
                testObject.Host = 'ook'
                self.fail( 'Once a connection is established, the Host property should be immutable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Modifying the Host property after connection is establilshed should raise AttributeError, bu %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testPassword( self ):
            """Unit-tests the Password property of the MySQLConnector class."""
            testObject = self.getLiveConnection()
            oldPassword = testObject.Password
            testObject.Password = 'ook'
            self.assertEquals( testObject.Password, 'ook', 'Before a connection is made, the object should allow modification of the Password property.' )
            testObject.Password = oldPassword
            testObject.Connect()
            try:
                testObject.Password = 'ook'
                self.fail( 'Once a connection is established, the Password property should be immutable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Modifying the Password property after connection is establilshed should raise AttributeError, bu %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

#        def testQueue( self ):
#            """Unit-tests the Queue property of the MySQLConnector class."""
#            pass

        def testUser( self ):
            """Unit-tests the User property of the MySQLConnector class."""
            testObject = self.getLiveConnection()
            oldUser = testObject.User
            testObject.User = 'ook'
            self.assertEquals( testObject.User, 'ook', 'Before a connection is made, the object should allow modification of the User property.' )
            testObject.User = oldUser
            testObject.Connect()
            try:
                testObject.User = 'ook'
                self.fail( 'Once a connection is established, the User property should be immutable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Modifying the User property after connection is establilshed should raise AttributeError, bu %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        # Unit-test methods

        def testConfigure( self ):
            """Unit-tests the Configure method of the MySQLConnector class"""
        def testConfigure( self ):
            """Unit-tests the Configure method of the BaseDataConnector abstract class."""
            # First, we need to build out a configuration-file
            hostName = 'localhost'
            databaseName = 'test_database'
            userName = 'test_user'
            userPassword = 'test_password'
            configString = """[Default Section]
SectionName1: SectionValue1
SectionName2: SectionValue2

[Data Access]
host: %s
database: %s
user: %s
password: %s
""" % ( hostName, databaseName, userName, userPassword )
            fp = open( 'test.cfg', 'w' )
            fp.write( configString )
            fp.close()
            # Now we need to create the configuration object and read in the configuration values
            configParser = Configuration( 'test.cfg' )
            # Now, finally, we can test the configuration
            testObject = MySQLConnector()
            testObject.Configure( configParser, 'Data Access' )
            self.assertEquals( testObject.Host, hostName, 'The host name should be retrievable from configuration' )
            self.assertEquals( testObject.Database, databaseName, 'The database name should be retrievable from configuration' )
            self.assertEquals( testObject.User, userName, 'The user name should be retrievable from configuration' )
            self.assertEquals( testObject.Password, userPassword, 'The user password should be retrievable from configuration' )
            os.unlink( 'test.cfg' )

        def testConnect( self ):
            """Unit-tests the Connect method of the MySQLConnector class."""
            pass # Tested in the various property-tests above...

        def testExecute( self ):
            """Unit-tests the Execute method of the MySQLConnector class."""
            testObject = self.getLiveConnection()
            # Single-table, single row, single field
            MyQuery = Query( testObject, "SELECT first_name FROM test_table_1 LIMIT 1;" )
            testResults = testObject.Execute( MyQuery )
            self.assertEquals( len( testResults ), 1, '%s should return a result-set' % ( MyQuery.Sql ) )
            self.assertEquals( len( testResults[0] ), 1, '%s should return a single row' % ( MyQuery.Sql ) )
            self.assertEquals( testResults[0][0].keys(), [ 'first_name' ], '%s should return only the first_name field' % ( MyQuery.Sql ) )
            # Single-table, single row, all fields
            MyQuery = Query( testObject, "SELECT * FROM test_table_1 LIMIT 1;" )
            testResults = testObject.Execute( MyQuery )
            self.assertEquals( len( testResults ), 1, '%s should return a result-set' % ( MyQuery.Sql ) )
            self.assertEquals( len( testResults[0] ), 1, '%s should return a single row' % ( MyQuery.Sql ) )
            self.assertEquals( len( testResults[0][0] ), 3, '%s should return three fields' % ( MyQuery.Sql ) )
            self.assertEquals( testResults[0][0].keys(), [ 'first_name', 'last_name', 'id' ], '%s should return first_name, last_name and id fields' % ( MyQuery.Sql ) )
            # Single-table, all rows, one field
            MyQuery = Query( testObject, "SELECT first_name FROM test_table_1;" )
            testResults = testObject.Execute( MyQuery )
            self.assertEquals( len( testResults ), 1, '%s should return a result-set' % ( MyQuery.Sql ) )
            self.assertEquals( len( testResults[0] ), 2 )
            self.assertEquals( testResults[0][0].keys(), [ 'first_name' ] )
            self.assertEquals( testResults[0][0].keys(), testResults[0][1].keys(), 'Field-names in different rows of the result-set should be identical' )
            # Single-table, all rows, all fields
            MyQuery = Query( testObject, "SELECT * FROM test_table_1;" )
            testResults = testObject.Execute( MyQuery )
            self.assertEquals( len( testResults ), 1, '%s should return a result-set' % ( MyQuery.Sql ) )
            self.assertEquals( len( testResults[0] ), 2, '%s should return two rows' % ( MyQuery.Sql ) )
            self.assertEquals( len( testResults[0][0] ), 3, '%s should return three fields' % ( MyQuery.Sql ) )
            self.assertEquals( testResults[0][0].keys(), [ 'first_name', 'last_name', 'id' ], '%s should return first_name, last_name and id fields' % ( MyQuery.Sql ) )
            self.assertEquals( testResults[0][0].keys(), testResults[0][1].keys(), 'Field-names in different rows of the result-set should be identical' )
            # Multiple tables (joined), all fields
            MyQuery = Query( testObject, "SELECT t1.*, t2.* FROM test_table_1 t1 LEFT JOIN test_table_2 t2 ON t1.id=t2.name_id;" )
            testResults = testObject.Execute( MyQuery )
            self.assertEquals( len( testResults ), 1, '%s should return a result-set' % ( MyQuery.Sql ) )
            self.assertEquals( len( testResults[0] ), 2, '%s should return two rows' % ( MyQuery.Sql ) )
            self.assertEquals( testResults[0][0].keys(), ['first_name', 'last_name', 't2.id', 'email', 'name_id', 'id'] )
            self.assertEquals( testResults[0][0].keys(), testResults[0][1].keys(), 'Field-names in different rows of the result-set should be identical' )
            # Multiple tables, multiple result-sets
            MyQuery = Query( testObject, "SELECT * FROM test_table_1;SELECT * FROM test_table_2;" )
            testResults = testObject.Execute( MyQuery )
            self.assertEquals( len( testResults ), 2, '%s should return two result-sets' % ( MyQuery.Sql ) )
            self.assertEquals( len( testResults[0] ), 2, '%s should return two rows in it\'s first result-set' % ( MyQuery.Sql ) )
            self.assertEquals( len( testResults[1] ), 2, '%s should return two rows in it\'s second result-set' % ( MyQuery.Sql ) )
            self.assertEquals( testResults[0][0].keys(), [ 'first_name', 'last_name', 'id' ], '%s should return first_name, last_name and id fields' % ( MyQuery.Sql ) )
            self.assertEquals( testResults[0][0].keys(), testResults[0][1].keys(), 'Field-names in different rows of the result-set should be identical' )
            self.assertEquals( testResults[1][0].keys(), ['name_id', 'id', 'email'], '%s should return name_id, id and email fields' % ( MyQuery.Sql ) )
            self.assertEquals( testResults[1][0].keys(), testResults[1][1].keys(), 'Field-names in different rows of the result-set should be identical' )
            # Query that returns no results
            MyQuery = Query( testObject, "SELECT * FROM test_table_1 WHERE first_name='ook';" )
            testResults = testObject.Execute( MyQuery )
            self.assertEquals( len( testResults ), 1 )
            self.assertEquals( len( testResults[0] ), 0 )

    testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testMySQLConnector ) )
Line(s)
1-8
An almost-typical derived-test class. The main difference here is the inclusion of an explicit object-destructor that cannot throw errors.
Unit-testing the object-destructor in any sort of meaningful way has eluded me still - I don't like not having such a test, but I'm recdonciled to having to take it on faith that the destructor is doing what it's supposed to do...
19-21
A convenience method to return a valid MySQLConnector instance pointing to the test database.
23-31
Standard final-class testing.
33-49
Object-construction testing. Note that the properties being passed as arguments to the constructor are being explicitly verified, though they shouldn't have to be as long as MySQLConnector remains true to the interface of BaseDataConnector. These are in place as much to make sure that potential future breaking changes underneath the MySQLConnector class would be caught before introducing bugs to a production system.

With the exception of the testConfigure and testExecute methods, most of the rest of the tests provided here are pretty typical. The testConfigure method is almost identical to the same method in the test-suite for BaseDataConnector, but is provided as a separate test-set in case of future modification to the Configure method of either BaseDataConnector or MySQLConnector.

testExecute, on the other hand, is an entirely different flavor of unicorn. This is really more of a system/integration test than a "real" unit-test, though I think it may straddle the line to some degree. It's necessary because the Execute method of MySQLConnector exists, but at the same time, there's no good way to really test it without making a connection to the database, and running queries against that connection. I'll hammer out some additional (and more detailed) integration tests that will eventually be added to the test-suite for MySQLConnector, but for now, this feels pretty good: Given the two tables of two rows present in the test database, I'm pretty confident that I've hit all of the meaningful variations of number of results, rows and fields that could come up against that data-set.

Friday, December 9, 2011

The DataConnectors module (part 3)

So, now that the interface-definitions are out of the way, I'm going to tackle the nominal abstract classes. There are only two, BaseDataConnector that will provide structure for concrete data-connector classes like MySQLConnector (and others down the line, potentially), and BaseDataObject, which will provide CRUD capabilities for objects whose state is going to be persisted in a back-end data-store.

BaseDataConnector provides standard connection parameters (host, database, user and password) as properties for connector objects. It does not define the connection property, or the mechanism used to connect to the database, however, since those will vary from database-engine to database-engine. A typical usage is expected to look something like the code below, assuming that Connector is a class derived from BaseDataConnector:

connection = Connector( 
    host='host', 
    database='database', 
    user='user', 
    password='password'
    )
# Optionally, the "Connect" method can be called before executing a query:
# connection.Connect()
results = connection.Query( 'some SQL code' ).Results

This looks pretty simple, though there's a few things I still need to think out (specifically, what I want the items in the results to look like). But even at this point, there's a lot going on "under the hood," as it were, and some possibilities that I'm contemplating for the future:

  • The creation of the Connector handles all of the connection-information for the back-end datasource - that much we pretty much have to assume.
  • Whether the code written against the Connector explicitly calls Connect or not, there has to be an active connection before queries can be run, so the first call to a query's Results property would perhaps have to tell the connector-object that it's ready to query, and to create the connection.
    • I have to admit that I like the idea of lazily-instantiating queries, results, etc. - Provided that some care is taken to keep things logical, I think that using a lazy instantiation would potentially save some database-connection time.
    • I also contemplated the idea of having one or more queues of queries associated with the connector-object: One for queries to be executed at object destruction, and one to allow queries to be generated and stored before they were actually needed, which would allow the first "real" need for a query's results to run all of the pending queries in one connection-batch, maybe. I'm not sure how much (if any) difference this would actually make, and it's potentially more complicated than I want to deal with right this minute, though, so I'm filing that away for future reference/use.
  • The results returned could/should be allowed to support multiple assignments (e.g., results1, results2 = connection.Query( 'some SQL code' ).Results) as well as single results. I'm not quite sure how I'm going to handle that just yet...

At the nominal-abstract-class level, there's not a lot that can realistically be implemented - mostly standard properties and the like - so the code is pretty short:

BaseDataConnector (Nominal abstract class):

class BaseDataConnector( IsDataConnector, IsConfigurable, object ):
    """Provides baseline functionality, interface requirements and type-identity for objects that can represent a connection to a back-end database."""

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

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetDatabase( self ):
        """Gets the name of the database that the connection will be made to."""
        return self._database

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetHost( self ):
        """Gets the name or IP-address of the host that the database resides upon."""
        return self._host

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetPassword( self ):
        """Gets the password used to connect to the database."""
        return self._password

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetUser( self ):
        """Gets the user-name used to connect to the database."""
        return self._user

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, '(String, required) The name of the database that the connection will be made to.' )
    def _SetDatabase( self, value ):
        """Sets the name of the database that the connection will be made to."""
        if type( value ) != types.StringType:
            raise TypeError( '%s.Database expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
        if value == '' or '\n' in value or '\r' in value or '\t' in value:
            raise ValueError( '%s.Database expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
        self._database = value

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, '(String, required) The name of the Host that the connection will be made to.' )
    def _SetHost( self, value ):
        """Sets the name of the Host that the connection will be made to."""
        if type( value ) != types.StringType:
            raise TypeError( '%s.Host expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
        if value == '' or '\n' in value or '\r' in value or '\t' in value:
            raise ValueError( '%s.Host expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
        self._host = value

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, '(String, required) The password that the connection will be made with.' )
    def _SetPassword( self, value ):
        """Sets the password that the connection will be made with."""
        if type( value ) != types.StringType:
            raise TypeError( '%s.Password expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
        if value == '' or '\n' in value or '\r' in value or '\t' in value:
            raise ValueError( '%s.Password expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
        self._password = value

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, '(String, required) The name of the User that the connection will be made with.' )
    def _SetUser( self, value ):
        """Sets the name of the User that the connection will be made with."""
        if type( value ) != types.StringType:
            raise TypeError( '%s.User expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
        if value == '' or '\n' in value or '\r' in value or '\t' in value:
            raise ValueError( '%s.User expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
        self._user = value

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelDatabase( self ):
        """Deletes the name of the database that the connection will be made to."""
        self._database = None

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelHost( self ):
        """Deletes the name or IP-address of the host that the database resides upon."""
        self._host = None

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelPassword( self ):
        """Deletes the password used to connect to the database."""
        self._password = None

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelUser( self ):
        """Deletes the user-name used to connect to the database."""
        self._user = None

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

    Database = property( _GetDatabase, _SetDatabase, _DelDatabase, 'Gets, sets or deletes the name of the database that the connection will be made to.' )
    Host = property( _GetHost, _SetHost, _DelHost, 'Gets, sets or deletes the name or IP-address of the host that the database resides upon.' )
    Password = property( _GetPassword, _SetPassword, _DelPassword, 'Gets, sets or deletes the password used to connect to the database.' )
    User = property( _GetUser, _SetUser, _DelUser, 'Gets, sets or deletes the user-name used to connect to the database.' )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'keyword', 'parameters', 'host', 'The name or IP-address of the host that the database resides upon.' )
    @DocumentArgument( 'keyword', 'parameters', 'database', 'The name of the database that the connection will be made to.' )
    @DocumentArgument( 'keyword', 'parameters', 'user', 'The user-name used to connect to the database.' )
    @DocumentArgument( 'keyword', 'parameters', 'password', 'The password used to connect to the database.' )
    def __init__( self, **parameters ):
        """Object constructor."""
        # Nominally abstract: Don't allow instantiation of the class
        if self.__class__ == BaseDataConnector:
            raise NotImplementedError( 'BaseDataConnector is (nominally) an abstract class, and is not intended to be instantiated.' )
        self._DelDatabase()
        if parameters.has_key( 'database' ):
            self._SetDatabase( parameters[ 'database' ] )
        self._DelHost()
        if parameters.has_key( 'host' ):
            self._SetHost( parameters[ 'host' ] )
        self._DelPassword()
        if parameters.has_key( 'password' ):
            self._SetPassword( parameters[ 'password' ] )
        self._DelUser()
        if parameters.has_key( 'user' ):
            self._SetUser( parameters[ 'user' ] )

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

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

    ##################################
    # IsConfigurable Methods         #
    ##################################

    @ToDo( 'Figure out how to approach unit-testing of this method.' )
    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'configProvider', None, 'A configuration provider object, an instance of one of IsConfigurable.ConfigurationTypes' )
    @DocumentArgument( 'argument', 'configSection', None, 'String, required) The configuration-section name that the object\'s configuration-state should be retrieved from.' )
    @DocumentConfiguration( 'Section Name', 'host', 'The name or IP-address of the host that the database resides upon.' )
    @DocumentConfiguration( 'Section Name', 'database', 'The name of the database that the connection will be made to.' )
    @DocumentConfiguration( 'Section Name', 'user', 'The user-name used to connect to the database.' )
    @DocumentConfiguration( 'Section Name', 'password', 'The password used to connect to the database.' )
    def Configure( self, configProvider, configSection ):
        """Configures the object using configuration data from the specified section of the specified provider."""
        parameters = configProvider[ configSection ]
        if parameters.has_key( 'database' ):
            self._SetDatabase( parameters[ 'database' ] )
        if parameters.has_key( 'host' ):
            self._SetHost( parameters[ 'host' ] )
        if parameters.has_key( 'password' ):
            self._SetPassword( parameters[ 'password' ] )
        if parameters.has_key( 'user' ):
            self._SetUser( parameters[ 'user' ] )

    ##################################
    # IsDataConnector Methods        #
    ##################################

    # Leaving Connect method abstracted from IsDataConnector

    # Leaving Query method abstracted from IsDataConnector

__all__ += [ 'BaseDataConnector' ]
Line(s)
12-30
Typical (by now) property-getter methods.
36-74
Typical (by now) property-setter methods.
80-98
I'm generating explicit Deleter methods as well, since I want the properties to be "deletable," but without allowing actual deletion - instead I want them to be set to None. Partly this is so that I've got an easy way to set them to their default values during construction or configuration, but mostly it's because I anticipate that in actual implementations I'll need to check whether they're set or not, and I'd rather that they always have at least a None value, rather than raising AttributeErrors if they get accidentally deleted.
104-107
The "real" property declarations.
113-134
The object constructor. Note that we're using a keyword-list style argument, documenting the recognized names, and using the class' _Set... methods to actually set the values. We could use the properties that those methods map to, but by going directly to the methods themselves, if the "settability" of the property is changed, that won't introduce errors.
148-166
Object configuration. To keep things simple, it uses the same names as the object constructor.
I'm not sure how I want to approach the unit-testing of this method just yet (as noted in the ToDo on 148). I'll have to give that some thought, and I'll likely generate a "clean-up" post once everything else is done that will address that and any other items that seem relevant, but for now, I'm going to leave this untested.

Unit-tests

    class BaseDataConnectorDerived( BaseDataConnector ):
        def __init__( self, **parameters ):
            BaseDataConnector.__init__( self, **parameters )
    
    class testBaseDataConnector( unittest.TestCase ):
        """Unit-tests the BaseDataConnector class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
        def testDerived( self ):
            """Testing abstract nature of the BaseDataConnector class."""
            try:
                testObject = BaseDataConnector()
                self.fail( 'BaseDataConnector is nominally abstract, and should not be instantiable.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'BaseDataConnector is nominally abstract, and should raise NotImplementedError when instantiated, but %s was raised instead\n  %s' % ( error.__class__.__name__, error ) )
            testObject = BaseDataConnectorDerived()
            self.assertTrue( isinstance( testObject, BaseDataConnector ) , 'BaseDataConnector-derived objects should be instances of BaseDataConnector' )
    
        def testPropertyCountAndTests( self ):
            """Testing the properties of the BaseDataConnector class."""
            items = getMemberNames( BaseDataConnector )[0]
            actual = len( items )
            expected = 5
            self.assertEquals( expected, actual, 'BaseDataConnector 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 BaseDataConnector class."""
            items = getMemberNames( BaseDataConnector )[1]
            actual = len( items )
            expected = 3
            self.assertEquals( expected, actual, 'BaseDataConnector 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 ) )

        # Unit-test properties

        def testConnection( self ):
            """Unit-tests the Connection property of the BaseDataConnector abstract class."""
            self.assertEquals( BaseDataConnector.Connection, IsDataConnector.Connection, 'BaseDataConnector implements IsdataConnector, but should not define the Connection property.' )

        def testDatabase( self ):
            """Unit-tests the Database property of the BaseDataConnector abstract class."""
            goodValues = [ 'databasename', 'database_name', 'a' ]
            for testValue in goodValues:
                testObject = BaseDataConnectorDerived()
                testObject.Database = testValue
                self.assertEquals( testObject.Database, testValue, 'The database property, if set, should get the set value.' )
                testObject = BaseDataConnectorDerived( database=testValue )
                self.assertEquals( testObject.Database, testValue, 'The database property, if set, should get the set value.' )
            badTypes = [ None, True, 0, [] ]
            for testValue in badTypes:
                testObject = BaseDataConnectorDerived()
                try:
                    testObject.Database = testValue
                    self.fail( 'Setting Database to %s should raise a TypeError.' )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting Database to %s should raise a TypeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
                try:
                    testObject = BaseDataConnectorDerived( database=testValue)
                    self.fail( 'Creating a BaseDataConnector with a database value of %s should raise a TypeError.' )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Creating a BaseDataConnector with a database value of %s should raise a TypeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            badValues = [ '', 'bad\nvalue', 'bad\tvalue', 'bad\rvalue' ]
            for testValue in badValues:
                testObject = BaseDataConnectorDerived()
                try:
                    testObject.Database = testValue
                    self.fail( 'Setting Database to %s should raise a ValueError.' )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Setting Database to %s should raise a ValueError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
                try:
                    testObject = BaseDataConnectorDerived( database=testValue)
                    self.fail( 'Creating a BaseDataConnector with a database value of %s should raise a ValueError.' )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Creating a BaseDataConnector with a database value of %s should raise a ValueError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testHost( self ):
            """Unit-tests the Host property of the BaseDataConnector abstract class."""
            goodValues = [ 'Hostname', 'Host_name', 'a' ]
            for testValue in goodValues:
                testObject = BaseDataConnectorDerived()
                testObject.Host = testValue
                self.assertEquals( testObject.Host, testValue, 'The Host property, if set, should get the set value.' )
                testObject = BaseDataConnectorDerived( host=testValue )
                self.assertEquals( testObject.Host, testValue, 'The Host property, if set, should get the set value.' )
            badTypes = [ None, True, 0, [] ]
            for testValue in badTypes:
                testObject = BaseDataConnectorDerived()
                try:
                    testObject.Host = testValue
                    self.fail( 'Setting Host to %s should raise a TypeError.' )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting Host to %s should raise a TypeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
                try:
                    testObject = BaseDataConnectorDerived( host=testValue)
                    self.fail( 'Creating a BaseDataConnector with a Host value of %s should raise a TypeError.' )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Creating a BaseDataConnector with a Host value of %s should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            badValues = [ '', 'bad\nvalue', 'bad\tvalue', 'bad\rvalue' ]
            for testValue in badValues:
                testObject = BaseDataConnectorDerived()
                try:
                    testObject.Host = testValue
                    self.fail( 'Setting Host to %s should raise a ValueError.' % ( testValue ) )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Setting Host to %s should raise a ValueError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
                try:
                    testObject = BaseDataConnectorDerived( host=testValue)
                    self.fail( 'Creating a BaseDataConnector with a Host value of %s should raise a ValueError.' )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Creating a BaseDataConnector with a Host value of %s should raise a ValueError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )

        def testPassword( self ):
            """Unit-tests the Password property of the BaseDataConnector abstract class."""
            goodValues = [ 'password', 'pass_word', 'a' ]
            for testValue in goodValues:
                testObject = BaseDataConnectorDerived()
                testObject.Password = testValue
                self.assertEquals( testObject.Password, testValue, 'The Password property, if set, should get the set value.' )
                testObject = BaseDataConnectorDerived( password=testValue )
                self.assertEquals( testObject.Password, testValue, 'The Password property, if set, should get the set value.' )
            badTypes = [ None, True, 0, [] ]
            for testValue in badTypes:
                testObject = BaseDataConnectorDerived()
                try:
                    testObject.Password = testValue
                    self.fail( 'Setting Password to %s should raise a TypeError.' )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting Password to %s should raise a TypeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
                try:
                    testObject = BaseDataConnectorDerived( password=testValue )
                    self.fail( 'Creating a BaseDataConnector with a password value of %s should raise a TypeError.' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Creating a BaseDataConnector with a password value of %s should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            badValues = [ '', 'bad\nvalue', 'bad\tvalue', 'bad\rvalue' ]
            for testValue in badValues:
                testObject = BaseDataConnectorDerived()
                try:
                    testObject.Password = testValue
                    self.fail( 'Setting Password to %s should raise a ValueError.' % ( testValue ) )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Setting Password to %s should raise a ValueError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
                try:
                    testObject = BaseDataConnectorDerived( password=testValue)
                    self.fail( 'Creating a BaseDataConnector with a password value of %s should raise a ValueError.' )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Creating a BaseDataConnector with a password value of %s should raise a ValueError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testUser( self ):
            """Unit-tests the User property of the BaseDataConnector abstract class."""
            goodValues = [ 'User', 'user_name', 'a' ]
            for testValue in goodValues:
                testObject = BaseDataConnectorDerived()
                testObject.User = testValue
                self.assertEquals( testObject.User, testValue, 'The User property, if set, should get the set value.' )
                testObject = BaseDataConnectorDerived( user=testValue )
                self.assertEquals( testObject.User, testValue, 'The User property, if set, should get the set value.' )
            badTypes = [ None, True, 0, [] ]
            for testValue in badTypes:
                testObject = BaseDataConnectorDerived()
                try:
                    testObject.User = testValue
                    self.fail( 'Setting User to %s should raise a TypeError.' )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting User to %s should raise a TypeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
                try:
                    testObject = BaseDataConnectorDerived( user=testValue )
                    self.fail( 'Creating a BaseDataConnector with a User value of %s should raise a TypeError.' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Creating a BaseDataConnector with a User value of %s should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            badValues = [ '', 'bad\nvalue', 'bad\tvalue', 'bad\rvalue' ]
            for testValue in badValues:
                testObject = BaseDataConnectorDerived()
                try:
                    testObject.User = testValue
                    self.fail( 'Setting User to %s should raise a ValueError.' % ( testValue ) )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Setting User to %s should raise a ValueError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
                try:
                    testObject = BaseDataConnectorDerived( user=testValue)
                    self.fail( 'Creating a BaseDataConnector with a User value of %s should raise a ValueError.' )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Creating a BaseDataConnector with a User value of %s should raise a ValueError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        # Unit-test methods

        def testConfigure( self ):
            """Unit-tests the Configure method of the BaseDataConnector abstract class."""
            # Not sure how I want to test this yet. See ToDo on Configure.
            pass

        def testConnect( self ):
            """Unit-tests the Connect method of the BaseDataConnector abstract class."""
            self.assertEquals( BaseDataConnector.Connect, IsDataConnector.Connect, 'BaseDataConnector should not define the Connect method.' )

        def testQuery( self ):
            """Unit-tests the Query method of the BaseDataConnector abstract class."""
            self.assertEquals( BaseDataConnector.Query, IsDataConnector.Query, 'BaseDataConnector should not define the Query method.' )

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

With the exception of the method-tests, this should be pretty straightforward by now - it's my typical structure...

The method-tests, though, may deserve some explanation. I noted above that I wasn't sure how I wanted to test the Configure method (as much as anything else because it, ideally, should be tested against real configuration-files, and I'm not sure I want to generate the sheer number of files that would be needed). For now, I'm going to leave that method untested, though I'll have to get back to that fairly soon to maintain my code-coverage goals for code tied to my posts.

The tests for Connect and Query are pretty simple, but what they do may not be readily apparent. Each of these is really just making sure that there is no local method (e.g., that BaseDataConnector doesn't define them, but inherits the nominally-abstract stubs from IsDataConnector).

The next nominally abstract class to tackle is BaseDataObject. The intention behind BaseDataObject is to provide common CRUD functionality for derived objects, as noted previously. In order to try to keep the actual read/write/update/delete process as simple as possible, creation of and update to a state-data record is going to be wrapped in a Save method that pays attention to various flag-value properties:

IsDeleted
Indicates whether an object's state-data record is pseudo-deleted. It's not uncommon, at least in my experience, to save even "deleted" records in a system for any of several reasons, practical and legal alike. This flag exists in order to assure that data-objects can be saved while being easily flagged as deleted.
IsDirty
Indicates whether an object's state-data record needs to be updated in the back-end data-store. This means that objects deriving from BaseDataObject will have to make sure that when a persistent state value changes, the entire object is flagged as "dirty".
IsNew
Indicates whether an object's state-data record needs to be created in the back-end data-store. When a record is created, the unique idenitifier of the record will need to be returned in some fashion, and stored in the object's state if at all possible.

These properties have implications on the design of database-tables that store object state-data: They should ideally have a unique identifier (which is generally desirable anyway), and a flag-field that indicates whether the state-record is "deleted" (or is active, if you prefer that approach). Other common fields (creation date, last modified date) could be added easily enough to BaseDataObject, but they feel less... globally used, I guess, to me.

The IsDirty and IsNew flags raise some questions as well. Ideally, I think, no given object should ever be considered both New and Dirty, since one implies that the object-record hasn't been created, while the other implies that it has, and on top of that, that it's been modified since it was created. An object that is created in an application and that will be saved to a database should, therefore, be considered "new" until it has been saved (at which point both flags should be False). Changes to that object's state before it's saved should also make the object "dirty," logically, but we don't want both a creation and an update to happen on the same object when it's being saved. I believe that all of this should be handled at the Save-method level, with logic that would look something like this:

  • When an object is created, it is new and not dirty; Saving it should create a state-record.
  • When that object is modified, it is both new and dirty; Saving it should create a state-record.
  • When that object is pseudo-deleted, it is both new and deleted; Saving it should create a state-record, and that record should indicate that it is "deleted".
  • When an object is retrieved from the database, it is not new, nor is it dirty; Saving it should do nothing, if only to avoid unneeded database connections/interaction.
  • When a retrieved object is modified, it is dirty but still not new; Saving it should cause an update.
  • When a retrieved object is pseudo-deleted, it is both dirty and deleted; Saving it should cause an update.
  • Any of these changes should be accounted for when the object is destroyed, in order to assure that state-data is correctly persisted.

BaseDataObject (Nominal abstract class):

class BaseDataObject( object ):
    """Provides baseline functionality, interface requirements and type-identity for objects whose state can be persisted in a back-end database."""

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

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetDataSource( self ):
        """Gets the data-source (an IsDataConnector instance) that the data-object will use to perform it's queries against."""
        return self._dataSource

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetId( self ):
        """Gets the unique ID of the object's state-data-record in the back-end database."""
        return self._id

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetIsDeleted( self ):
        """Gets the flag indicating whether the object's state-data record is "deleted"."""
        return self._isDeleted

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetIsDirty( self ):
        """Gets the flag indicating whether the object's state-data record is "Dirty"."""
        return self._isDirty

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetIsNew( self ):
        """Gets the flag indicating whether the object's state-data record is "New"."""
        return self._isNew

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The data-source (an IsDataConnector instance) that the data-object will use to perform it\'s queries against.' )
    def _SetDataSource( self, value ):
        """Sets the data-source (an IsDataConnector instance) that the data-object will use to perform it's queries against."""
        if self._dataSource:
            raise NotImplementedError( '%s.DataSource cannot be changed after it has been set.' % ( self.__class__.__name__ ) )
        if not isinstance( value, IsDataConnector ):
            raise TypeError( '%s.DataSource expects an instance of IsDataConnector.' % ( self.__class__.__name__ ) )
        self._dataSource = value

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The unique ID of the object\'s state-data-record in the back-end database.' )
    def _SetId( self, value ):
        """Sets the unique ID of the object's state-data-record in the back-end database."""
        self._id = value

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The flag indicating whether the object\'s state-data record is "deleted".' )
    def _SetIsDeleted( self, value ):
        """Sets the flag indicating whether the object's state-data record is "deleted"."""
        if value != True and value != False and value != 1 and value != 0:
            raise TypeError( '%s.IsDeleted expects a True, False, 1 or 0 value.' % ( self.__class__.__name__ ) )
        self._isDeleted = value

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The flag indicating whether the object\'s state-data record is "dirty" (in need of an update).' )
    def _SetIsDirty( self, value ):
        """Sets the flag indicating whether the object's state-data record is "dirty" (in need of an update)."""
        if value != True and value != False and value != 1 and value != 0:
            raise TypeError( '%s.IsDirty expects a True, False, 1 or 0 value.' % ( self.__class__.__name__ ) )
        self._isDirty = value
        if value:
            self._isNew = False

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The flag indicating whether the object\'s state-data record is "New".' )
    def _SetIsNew( self, value ):
        """Sets the flag indicating whether the object's state-data record is "New"."""
        if value != True and value != False and value != 1 and value != 0:
            raise TypeError( '%s.IsNew expects a True, False, 1 or 0 value.' % ( self.__class__.__name__ ) )
        self._isNew = value

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelDataSource( self ):
        """Deletes the data-source that the data-object will use to perform it's queries against."""
        self._dataSource = None

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelId( self ):
        """Deletes the data-source that the data-object will use to perform it's queries against."""
        self._id = None

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelIsDeleted( self ):
        """Deletes the flag indicating whether the object's state-data record is "Deleted"."""
        self._isDeleted = None

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelIsDirty( self ):
        """Deletes the flag indicating whether the object's state-data record is "dirty" (in need of an update)."""
        self._isDirty = None

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelIsNew( self ):
        """Deletes the flag indicating whether the object's state-data record is "New" (in need of creation)."""
        self._isNew = None

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

    DataSource = property( _GetDataSource, _SetDataSource, None, 'Gets or sets the data-source (an IsDataConnector instance) that the data-object will use to perform it\'s queries against.' )
    Id = property( _GetId, None, None, 'Gets the unique identifier of the object\'s state-data record.')
    IsDeleted = property( _GetIsDeleted, _SetIsDeleted, None, 'Gets or sets the flag indicating that the object\'s state-data record is pseudo-deleted.' )
    IsDirty = property( _GetIsDirty, _SetIsDirty, None, 'Gets or sets the flag indicating that the object\'s state-data record is "dirty," and in need of an update.' )
    IsNew = property( _GetIsNew, None, None, 'Gets or sets the flag indicating that the object\'s state-data record is "New," and needs to be created.' )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def __init__( self ):
        """Object constructor."""
        # Set new and dirty flags before checking for instantiability, in order to 
        # avoid raising errors at destruction.
        self._isDirty = None
        self._isNew = None
        # Nominally abstract: Don't allow instantiation of the class
        if self.__class__ == BaseDataObject:
            raise NotImplementedError( 'BaseDataObject is (nominally) an abstract class, and is not intended to be instantiated.' )
        self._DelDataSource()
        self._DelId()
        self.IsDeleted = False
        self.IsDirty = False
        self._isNew = True

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

    def __del__( self ):
        """Object destructor.

Saves the object's state-data before destruction."""
        self.Save()

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _Create( self ):
        """(Nominal abstract method) Creates a record in the back-end database for the object's state-data, updating the object's Id property with the identity of the created record."""
        raise NotImplementedError( '%s._Create has not been implemented as required by BaseDataObject.' % ( self.__class__.__name__ ) )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def Delete( self ):
        """(Nominal abstract method) Performs a physical record-deletion of a specific object's state-data record from the back-end database."""
        raise NotImplementedError( '%s.Delete has not been implemented as required by BaseDataObject.' % ( self.__class__.__name__ ) )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'id', None, 'The unique identifier for the state-data record of the object to be retrieved from the database.' )
    def Read( self, id ):
        """(Nominal abstract method) Reads a specific object's state-data record from the back-end database, and returns an object-instance with the retrieved state-data."""
        raise NotImplementedError( '%s.Read has not been implemented as required by BaseDataObject.' % ( self.__class__.__name__ ) )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def Save( self ):
        """Creates or updates the object's state-data record based on whether the object's IsNew or IsDirty flag is set."""
        if self._isNew:
            self._Create()
        elif self._isDirty:
            self._Update()
        self.IsDirty = False
        self._isNew = False

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _Update( self ):
        """(Nominal abstract method) Updates the object's state-data record in the back-end database."""
        raise NotImplementedError( '%s._Update has not been implemented as required by BaseDataObject.' % ( self.__class__.__name__ ) )

__all__ += [ 'BaseDataObject' ]

Commentary

Line(s)
13-35
Typical property-getter structures.
42-49
Standard property-setter structure.
51-55
The property-setter for the Id property is slightly different than the typical pattern I've shown in previous code, in that it just sets the value, performing no type- or value-checking. Id could, arguably, be left abstract at the BaseDataObject level, but there doesn't seem to me to be any significant advantage to doing so - I'd rather have a single point of definition, and (if necessary) override the setter method in derived classes to provide type- and/or value-checking when or if necessary. I have a lingering suspicion that I'll regret that design decision somewhere along the line, but for the time being, I'll hold to it, and seee what happens.
58-63, 66-72, 75-80
Since the properties behind these setters are boolean values, there's no real need to do a type-check them - a simple check of all of the allowed values should suffice.
71-73
Since setting the object's state to "dirty" should reset it's "new" status, we're doing that here, but only if the "dirty" status is True (or equivalent).
88-110
Typical property-deleter method-structure.
116-120
Typical property definitions.
146-150
By default, any object derived from BaseDataObject should save it's state before it is destroyed. Since the _Create and _Update methods are still abstract (and thus need to be defined by the derived classes), but still exist at the BaseDataObject level, the destructor can take advantage of the methods' existence, define the Save method, and call it in the object destructor.
157-159, 162-164, 168-170, 183-185
The nominally-abstract methods for state-data-record creation, deletion, read/retrieval and update, respectively. These must be abstract at this level, because there is no way to anticipate exactly how they will work, but we know that they need to exist.
173-180
The concrete method that creates or updates a state-data record for the derived object, based on the object's "dirty" or "new" state.

For the most part, the unit-tests are pretty much what you'd expect, particularly if you've been following this blog for any length of time:

    class BaseDataObjectDerived( BaseDataObject ):
        def __init__( self ):
            BaseDataObject.__init__( self )
        def __del__( self ):
            pass
    
    class BaseDataObjectImplemented( BaseDataObject ):
        def __init__( self ):
            BaseDataObject.__init__( self )
        def __del__( self ):
            BaseDataObject.__del__( self )
        def _Create( self ):
            fp = open( '/tmp/create', 'w' )
            fp.write( '_Create' )
            fp.close()
        def _Update( self ):
            fp = open( '/tmp/update', 'w' )
            fp.write( '_Update' )
            fp.close()
    
    class testBaseDataObject( unittest.TestCase ):
        """Unit-tests the BaseDataObject class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            try:
                os.unlink( '/tmp/create' )
            except:
                pass
            try:
                os.unlink( '/tmp/update' )
            except:
                pass
        
        def testDerived( self ):
            """Testing abstract nature of the BaseDataObject class."""
            try:
                testObject = BaseDataObject()
                self.fail( 'BaseDataObject is nominally an abstract class, and should not be instantiable.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Attempting to instantiate a BaseDataObject should raise NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            testObject = BaseDataObjectDerived()
            self.assertTrue( isinstance( testObject, BaseDataObject ), 'Objects derived from BaseDataObject should be instances of BaseDataObject.' )

        def testDestruction( self ):
            """Testing destruction of the BaseDataObject class."""
            testObject = BaseDataObjectImplemented()
            del testObject
            try:
                fp = open( '/tmp/create', 'r' )
                fp.close()
            except IOError:
                self.fail( 'Destruction of a new object should result in it\'s _Create method being called.' )
            except Exception, error:
                self.fail( 'Destruction of a new object should result in it\'s _Create method being called, but %s was raised"\n  %s' % ( error.__class__.__name__, error ) )
            testObject = BaseDataObjectImplemented()
            testObject.IsDirty = True
            del testObject
            try:
                fp = open( '/tmp/update', 'r' )
                fp.close()
            except IOError:
                self.fail( 'Destruction of a modified object should result in it\'s _Update method being called.' )
            except Exception, error:
                self.fail( 'Destruction of a modified object should result in it\'s _Update method being called, but %s was raised"\n  %s' % ( error.__class__.__name__, error ) )
    
        def testPropertyCountAndTests( self ):
            """Testing the properties of the BaseDataObject class."""
            items = getMemberNames( BaseDataObject )[0]
            actual = len( items )
            expected = 5
            self.assertEquals( expected, actual, 'BaseDataObject 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 BaseDataObject class."""
            items = getMemberNames( BaseDataObject )[1]
            actual = len( items )
            expected = 3
            self.assertEquals( expected, actual, 'BaseDataObject 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 properties

        def testDataSource( self ):
            """Unit-tests the DataSource property of the BaseDataObject nominal abstract class."""
            testValue = IsDataConnectorDerived()
            testObject = BaseDataObjectDerived()
            testObject.DataSource = testValue
            self.assertEquals( testObject.DataSource, testValue, 'Setting the DataSource should return the set value on a get.' )
            badValues = [ None, True, 1, 'ook' ]
            for testValue in badValues:
                try:
                    testObject = BaseDataObjectDerived()
                    testObject.DataSource = testValue
                    self.fail( 'Setting DataSource to a value of %s should raise a TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting DataSource to a value of %s should raise a TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )
            testObject = BaseDataObjectDerived()
            testValue = IsDataConnectorDerived()
            testValue2 = IsDataConnectorDerived()
            testObject.DataSource = testValue
            try:
                testObject.DataSource = testValue2
                self.fail( 'Changing an established DataSource of a BaseDataObject should not be allowed' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Changing an established DataSource of a BaseDataObject should raise NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testId( self ):
            """Unit-tests the Id property of the BaseDataObject nominal abstract class."""
            # There really isn't any useful test that can be done here without
            # mandating an ID type or structure, so this test really doesn't do
            # anything except get counted as a testable property.
            pass

        def testIsDeleted( self ):
            """Unit-tests the IsDeleted property of the BaseDataObject nominal abstract class."""
            testObject = BaseDataObjectDerived()
            goodValues = [ True, False, 1, 0 ]
            for testValue in goodValues:
                testObject.IsDeleted = testValue
                self.assertEquals( testObject.IsDeleted, testValue, 'Setting the IsDeleted value should returnt he same value when gotten.' )
            badValues = [ None, 2, 'ook', -1, object() ]
            for testValue in badValues:
                try:
                    testObject.IsDeleted = testValue
                    self.fail( 'Setting IsDeleted to a value of %s should raise TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting IsDeleted to a value of %s should raise TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )

        def testIsDirty( self ):
            """Unit-tests the IsDirty property of the BaseDataObject nominal abstract class."""
            testObject = BaseDataObjectDerived()
            goodValues = [ True, False, 1, 0 ]
            for testValue in goodValues:
                testObject.IsDirty = testValue
                self.assertEquals( testObject.IsDirty, testValue, 'Setting the IsDirty value should returnt he same value when gotten.' )
            badValues = [ None, 2, 'ook', -1, object() ]
            for testValue in badValues:
                try:
                    testObject.IsDirty = testValue
                    self.fail( 'Setting IsDirty to a value of %s should raise TypeError' % ( testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting IsDirty to a value of %s should raise TypeError, but %s was raised instead:\n  %s' % ( testValue, error.__class__.__name__, error ) )

        def testIsNew( self ):
            """Unit-tests the IsNew property of the BaseDataObject nominal abstract class."""
            testObject = BaseDataObjectDerived()
            self.assertEquals( testObject.IsNew, True )
#            self.assertEquals( testObject.IsNew, True, 'A default BaseDataObject-derived object should start with IsNew of True' )

        # Test methods

        def testDelete( self ):
            """Unit-tests the Delete method of the BaseDataObject nominal abstract class."""
            testObject = BaseDataObjectDerived()
            try:
                testObject.Delete()
                self.fail( 'BaseDataObject should not implement Delete.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'BaseDataObject.Delete should raise NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testRead( self ):
            """Unit-tests the Read method of the BaseDataObject nominal abstract class."""
            testObject = BaseDataObjectDerived()
            try:
                testObject.Read( None )
                self.fail( 'BaseDataObject should not implement Read.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'BaseDataObject.Read should raise NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testSave( self ):
            """Unit-tests the Save method of the BaseDataObject nominal abstract class."""
            testObject = BaseDataObjectImplemented()
            testObject.Save()
            try:
                fp = open( '/tmp/create', 'r' )
                fp.close()
            except IOError:
                self.fail( 'Save called on a new object should result in it\'s _Create method being called.' )
            except Exception, error:
                self.fail( 'Save called on a new object should result in it\'s _Create method being called, but %s was raised:\n  %s.' % ( error.__class__.__name__, error ) )
            testObject.IsDirty = True
            testObject.Save()
            try:
                fp = open( '/tmp/create', 'r' )
                fp.close()
            except IOError:
                self.fail( 'Save called on an updated object should result in it\'s _Create method being called.' )
            except Exception, error:
                self.fail( 'Save called on an updated object should result in it\'s _Create method being called, but %s was raised:\n  %s.' % ( error.__class__.__name__, error ) )

    testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testBaseDataObject ) )
Line(s)
1-5, 7-19
Since there's a need to test both the default, unimplemented methods as well as whether those methods get called as expected, there are actually two derived test-classes that are used. The first, BaseDataObjectDerived is used to make sure that any unimplemented abstract methods raise the exceptions that they should (a key part of making sure that the nominally-abstract class will respond with expected abstract-like behavior). The second, BaseDataObjectImplemented, provides implementations for _Create and _Update that write to files that the test-methods can access - these are a fair, but low-weight representation of the basic kind of data-persistence process that would be implemented (storing data to some external source), though in this case, it's file-based instead of being tied to a database (which we'd want to abstract away anyway).
27-35
Since some of the test-methods are writing files, and a failure in a test-method could leave a written file that would generate a false positive result in/for later tests, the tearDown method is used to make sure that if the expected files exist, they are deleted.
The potential for false positives in this test is particularly worrisome, since it would be possible for a bad test-run one day to leave an undeleted test-result file that another test-run could pick up minutes, hours, or days later. The fact that the files in question are being generated in /tmp would potentially help, but not prevent this from happening.
37-51
Fairly typical construction/derivation
49-69
Tests the destruction of a BaseDataObject-derived class with _Create and _Update implemented.
51
Creates a new test-object, with an expected IsNew of True and IsDirty of False. This is the default state for such a derived object.
52
Deletes the newly-created object, which should fire the _Create method.
53-59
Checks for the existence of the expected file that _Create should create
60-69
With the exception of setting IsDirty on the test-object (which should cause the _Update method to fire on destruction) and the file expected, the tests for a "dirty" object

With the exception of the unit-test for the Id property, which really can't usefully test the property anyway, the property- and method tests are, I hope, what would be expected given the typical structure/pattern of such tests from previous posts.