Showing posts with label unit-testing. Show all posts
Showing posts with label unit-testing. Show all posts

Thursday, May 11, 2017

Unit-testing vs. Development Disruptions

So, the short story about the disruption in implementing Tag that I alluded to is that I got sidetracked with other things, and couldn't spare the attention to Tag for the best part of two weeks. The specific details why aren't important, though — in a real world, dev-shop environment, similar derailments happen for any number of reasons: Temporary changes in priorities, critical bugs that need attention right now, the list of possible reasons is probably huge. The important part is what the effects of the derailment were:

  • Development was paused mid-stream;
  • It was set aside for a relatively long period, and despite making some efforts to at least try to keep track of where I'd left off, I ended up being away from it for too long to remember what I was thinking at the time;
  • I hadn't gotten through completely documenting (or in a few cases, even loosely defining) many of Tag's members;
  • I still had a fair number of unit-tests that I'd had to defer because of dependencies with other classes;
Note that second point: Normally, if I can, I like to try and at least take some time to scribble down a few simple notes about where I had to leave off, what I was doing, what the immediate concerns were when I had to shelve my efforts, that sort of thing. In this case, though I made an effort to do so, it simply wasn't enough. I'd completely lost track of my thoughts after getting as far as stubbing out all of the members of Tag after finishing the unit-testing of Namespace, minus some dependencies on Tag.

Some of this disruption could have been avoided, maybe, if I'd chosen to build out the concrete classes in a different order. As a result of my decision to take them in the order I did, I was left in the position of waiting to finish the unit-testing of Namespace until I was done with implementing Tag because of some dependencies between the two:

I'd rather get all of the unit-testing resolved at once after Tag is done, and the markup module is more complete.
In retrospect, that was maybe not the best decision I could've made. I haven't gone back yet to look at the interconnections of the markup classes to see if there was a better, less troublesome path I could have taken, so it may well be that there isn't one. I'm going to plan to do that after I finish the module, and if there is anything worthwile that I discover, I'll be sure to post it.

Where That Left Me

As a result, I spent a day or so thrashing about trying to figure out where I'd left off and what my next steps were. Not unlike coming into a project that some other developer has started in a real-word development position, really.

And there is where having solid unit-testing policies and practices came to the rescue, I think. With what had been completed, it was a matter of a few minutes to set up the testTag test-case class and fire off a test-run. That gave me a couple lists of missing test-methods, one for properties of Tag, and one for its methods. It took maybe another half hour or so to stub out all of those test-methods, so that all of them were returning failures, and then another couple of days to work my way through all of the failing tests and get them to pass. All told, there were three types of tasks I had to undertake, guided by those tests:

  • Implement unit-tests on properties and methods that had been completed;
  • Implement unit-tests on properties and methods that were implemented, but also broken in some fashion; and
  • Implement unit-tests that revealed that I hadn't even defined the Tag-member that they were supposed to test, then implement those missing members;
All in all, this process was similar to the sort of thing that is done regularly in TDD shops, if much less formally:
  • I had member-requirements enforced by the test-methods;
  • I had, at least in some cases, functional requirements for those members that were easily converted into usable test-methods;
  • In the cases where I didn't have solid functional requirements, I could refer to the JavaScript API that I was trying to maintain consistency with for most members; and
  • In the (few) remaining cases where I was creating new functionality, I had documented what those members were supposed to do, or had a very good idea what I wanted them to be capable of.
It was still very chaotic (and more than a little frustrating), but it was workable.

Because I was feeling pressed for time, I didn't think to capture a lot of the results of those initial test-runs. In fact, it wasn't until I'd gotten a fair way through them that it occurred to me that what I was going through might be worth posting about, and by that time my results looked like this:

########################################
Unit-test results
########################################
Tests were successful ... False
Number of tests run ..... 219
 + Tests ran in ......... 0.02 seconds
Number of errors ........ 0
Number of failures ...... 92
########################################

By the time Tag was complete, that had grown a bit:

########################################
Unit-test results
########################################
Tests were successful ..... False
Number of tests run ....... 225
 + Tests ran in ........... 0.12 seconds
Number of errors .......... 0
Number of failures ........ 16
Number of tests skipped ... 77
########################################
I'd also decided that I wanted to be able to see both a summary of the number of test-methods that I'd explicitly skipped, and some details about those skipped test-methods. A lot of them were skipped because they were the various getter-, setter- or deleter-methods for propeties that were completely tested in the test-method for the property. I'd also force-skipped the items that were dependent on an implementation of some other class that I hadn't gotten to yet (MarkupParser):
########################################
SKIPPED
#--------------------------------------#
test_DelParent (__main__.testBaseNode)
 - _DelParent is tested in testparent
test_GetParent (__main__.testBaseNode)
 - _GetParent is tested in testparent

...

test_SetParent (__main__.testBaseNode)
 - _SetParent is tested in testparent

...

test_DelinnerHTML (__main__.testTag)
 - ## DEFERRING until MarkupParser is implemented

...

test_GetinnerHTML (__main__.testTag)
 - ## DEFERRING until MarkupParser is implemented

...

test_SetinnerHTML (__main__.testTag)
 - ## DEFERRING until MarkupParser is implemented

...

testcloneNode (__main__.testTag)
 - ## DEFERRING until MarkupParser is implemented
testinnerHTML (__main__.testTag)
 - ## DEFERRING until MarkupParser is implemented

...

########################################
FAILURES

The take-away from this entire story, for me, boiled down to

Having a unit-testing policy, and processes that implement that policy, can help a developer resume their efforts after a disruption as well as ensuring that changes to code didn't break anything.

Even with that, though, Tag still ended up taking longer to finish than I'd expected — an argument, perhaps, for picking the sequence of classes for development more carefully...

Other Things that I Encountered or Thought Of

Here's a potentially-useful trick, with some back-story. BaseNode implements some concrete functionality that's inherited by CDATA, Comment, Tag and Text — I'll use the nextElementSibling property as the example, but there are seven other properties that have the same relationship to the same derived classes. In order to really test those properties, there needs to be a class that has a complete, concrete implementation that derived from BaseNode. Normally, in building out unit-tests for an abstract class like BaseNode, I'd also define a derived class as part of the unit-testing module (BaseNodeDerived, for example), and would use instances of that class as the test-objects in the various test-methods for those concrete items. At some point while I was working through the pile of unit-tests for Tag, it occurred to me that it would be possible (though maybe not desirable in this case) to have one of the test-methods for BaseNode require test-methods in the test-case classes for its derived classes instead. That didn't turn out to be very useful in this case, since BaseNode ended uup being an abstract class with no abstract members (something that I'll have to think on later), but the concept seemed, for a while, to be sound enough that I had code that would do just that:

def testnextElementSibling(self):
    """Unit-tests the nextElementSibling property of a BaseNode instance."""
    # It makes little sense to test here, since that would require 
    # spinning up derived (and potentially broken) classes and there 
    # are *actual* classes where the tests can be run, so require tests
    # in those test-case classes here instead.
    testCases = [ testCDATA, testComment, testTag, testText ]
    testName = 'testnextElementSibling'
    missingCases = [ 
        c.__name__ for c in testCases if not hasattr( c, testName )
    ]
    self.assertEqual( missingCases, [], 
        'testBaseNode requires "%s" test-cases in %s' % 
            ( testName, ', '.join( missingCases ) )
    )
Ultimately, since Tag (and CDATA, Comment and Text) derive from BaseNode, it was possible to build useful test-methods for all of the incomplete BaseNode test-methods using instance of those classes, so this approach wasn't actually needed. Probably just as well: Though I could show that it worked to my satisfaction, it still ended up relying a bit too much on a human making a decision (which items to require tests for, in this case) to maintain some certainty of the code-coverage I'm striving for. That said, I may well come back to that idea, perhaps implementing it as a decorator-method that can be applied to test-case classes, the way AddMethodTesting and AddPropertyTesting a work right now.

A Missing Property Example: ownerDocument

One of the missing members I discovered as a result of the big list of members of Tag was the ownerDocument property. In all honesty, I simply missed implementing, or even requiring it, so the unit-testing approach mentioned above didn't catch it — it was purely human observation and effort that revealed it. Since it was also a common property for all of the BaseNode-derived classes, and something that should be common to all node-types, even if they aren't derived from BaseNode, I required it as an abstract property in IsNode:

#-----------------------------------#
# Abstract Properties               #
#-----------------------------------#

nextElementSibling = abc.abstractproperty()
nextSibling = abc.abstractproperty()
nodeName = abc.abstractproperty()
nodeType = abc.abstractproperty()
ownerDocument = abc.abstractproperty()
parentElement = abc.abstractproperty()
parentNode = abc.abstractproperty()
previousElementSibling = abc.abstractproperty()
previousSibling = abc.abstractproperty()
textContent = abc.abstractproperty()
Once that was in place, though, the unit-testing policies picked up that it was missing immediately raising failures resembling this one, from testCDATA:
########################################
ERRORS
#--------------------------------------#
Traceback (most recent call last):
  File "test_markup.py", line 361, in test__init__
    testObject = CDATA( testValue )
TypeError: 
    Can't instantiate abstract class CDATA with 
      abstract methods ownerDocument

From there, implementing it in BaseNode was simple:

@describe.AttachDocumentation()
def _GetownerDocument( self ):
    """
Returns the top-most IsElement object in the instance's DOM tree"""
    if not self._parent:
        return self
    else:
        return self._parent.ownerDocument
This deviates from the JavaScript ownerDocument, though, and I'm not sure if I'll keep it as is, or alter it later when I get to implementing BaseDocument: In JavaScript, as far as I've ben able to determine, there is no way to create an element that is not a member of a document — even if that element hasn't been attached to the DOM of the document. All parsed tags are automatically document-members, and the createElement method is only available to the document. Taken together, these effectively prevent an element from not being a member of the document they were created in. In the idic framework (so far), it's possible to create Tags even if no document has been defined. I'll have to ponder on that, but for now, I'll let it stand.

Once again, the unit-testing policies picked up that there was still something missing:

#--------------------------------------#
Traceback (most recent call last):
  File "unit_testing.py", line 332, in testMethodCoverage
    target.__name__, missingMethods
AssertionError: 
    Unit-testing policy requires test-methods to be created 
    for all public and protected methods, but testBaseNode 
    is missing the following test-methods: 
        ['test_GetownerDocument']
#--------------------------------------#
Traceback (most recent call last):
  File "unit_testing.py", line 373, in testPropertyCoverage
    'methods: %s' % ( target.__name__, missingMethods )
AssertionError: 
    Unit-testing policy requires test-methods to be created 
    for all public properties, but testBaseNode is missing 
    the following test-methods: 
        ['testownerDocument']
#--------------------------------------#
As annoying as this might seem, it really was a good thing — The unit-testing processes and policies set up back at the beginning of last month were catching that changes had been made, that unit-testing for those changes wasn't complete, and that work needed to be done because of those changes.

That feels like a validation of my unit-testing policies to me.

Two other properties came to my attention while organizing the big Tag members-table: While I'd set up tests for the nodeName and nodeType properties in the testHasTextData test-case class, there were no corresponding tests in testCDATA, testComment or testText.

Again, fixing that didn't take much effort. The example structure for the test-methods for all of the concrete classes looked almost the same as the test-methods for CDATA :

def testnodeName(self):
    """Unit-tests the nodeName property of a CDATA instance."""
    testObject = CDATA( 'test-instance' )
    self.assertEquals( testObject.nodeName, CDATA._nodeName,
        'CDATA does not have a defined _nodeName attribute, or is '
        'inheriting the default None value from HasTextData.'
    )

def testnodeType(self):
    """Unit-tests the nodeType property of a CDATA instance."""
    testObject = CDATA( 'test-instance' )
    self.assertEquals( testObject.nodeType, nodeTypes.CDATASection,
        'CDATA does not have a defined _nodeType attribute, or is '
        'inheriting the default None value from HasTextData.'
    )
Once the underlying class-attributes had been defined for all three concrete classes, and a few other minor things that I noticed that were buried in the 20+ failures from Namespace and BaseNode tests were cleaned up, things were back to a reasonable/expected number of failures and errors.

The moral of this story? Perhaps the idea of embedding the node-types and -names as class properties, then retrieving them with common getter-methods in HasTextData was... too clever, maybe? At the time it felt fairly elegant — Store the actual values in the classes themselves, keeping them nicely encapsulated, etc., etc. But, when push came to shove, the combination of that storage-approach and the coverage-testing routines left a hole that a bug slipped through. It was pure, dumb luck that I happened to notice it when I did.

Getting the Remaining Tests Running

Eventually, after I got done with Tag's implementation and had reconciled all of the expected missing tests, I got to a point where the test-run yielded only a handful of failures:

########################################
Unit-test results
########################################
Tests were successful ..... False
Number of tests run ....... 226
 + Tests ran in ........... 0.14 seconds
Number of errors .......... 0
Number of failures ........ 8
Number of tests skipped ... 77
########################################
Those failures included a variety of items, including:
#--------------------------------------#
testCodeCoverage (__main__.testmarkupCodeCoverage)
AssertionError: 
    Unit-testing policies require test-cases for all classes 
    and functions in the idic.markup module, but the following 
    have not been defined: 
        (testAttributesDict)
#--------------------------------------#
testMethodCoverage (__main__.testNamespace)
AssertionError: 
    Unit-testing policy requires test-methods to be 
    created for all public and protected methods, but 
    testNamespace is missing the following test-methods: 
        ['testGetNamespaceByName', 'testGetNamespaceByURI', 
        'testRegisterNamespace', 
        'test_DelDefaultRenderingModel', 'test_DelName', 
        'test_DelTagRenderingModels', 'test_DelnamespaceURI', 
        'test_GetDefaultRenderingModel', 'test_GetName', 
        'test_GetTagRenderingModels', 'test_GetnamespaceURI', 
        'test_SetDefaultRenderingModel', 'test_SetName', 
        'test_SetTagRenderingModels', 'test_SetnamespaceURI']
#--------------------------------------#
testPropertyCoverage (__main__.testNamespace)
AssertionError: 
    Unit-testing policy requires test-methods to be created 
    for all public properties, but testNamespace is missing 
    the following test-methods: 
        ['testDefaultRenderingModel', 'testName', 
        'testTagRenderingModels', 'testnamespaceURI']
#--------------------------------------#
I handled all of these failures with the normal unit-test-definition process.

Since I was already in a unit-testing frame of mind, I went ahead and dealt with all of the remaining outstanding test-failures that weren't part of Tag's test-case as well. That leaves me with a clean slate, more or less, for the next post, with the following results:

########################################
Unit-test Results: idic.markup
#--------------------------------------#
Tests were SUCCESSFUL
Number of tests run ....... 253
Number of tests skipped ... 95
Tests ran in .......... 0.140 seconds
#--------------------------------------#
########################################
Unit-test Results: idic
#--------------------------------------#
Tests were SUCCESSFUL
Number of tests run ....... 276
Number of tests skipped ... 95
Tests ran in .......... 0.318 seconds
#--------------------------------------#

The unit_testing module had some minor modifications, not much more than some restructuring of the test-results reporting, really, with the addition of the code that counted and displayed details on skipped tests. The current test-results for test_markup is long enough that I didn't want to just dump it into the post, but I still want to share it, so it's downloadable as well.

Tuesday, May 9, 2017

Generating and Parsing Markup in Python [6]

Though there are fewer methods in the Tag class than properties, there are still a good number of them that are, in my opinion, worth taking a detailed look at, so I'll just dive right in.

Method Implementations

There are three groups of methods that have a common theme between them, and that share a fair amount of logic as a result:

  • Methods relating to manipulating a Tag's childNodes (variations of adding children to and removing children from a Tag, ultimately);
  • Methods for getting various logical groupings or sets of child Tags from a Tag instance; and
  • Methods for working with a the attributes of a Tag
The majority of the other methods of Tag are either already implemented in BaseNode, are very simple implementations (in my opinion), or need to be deferred until some other class in the markup module is implemented.

Methods Relating to Manipulating Child Nodes

There are five different methods that add child nodes to a Tag instance in some fashion, three that involve removing a child, and two that relate to replacing a child (methods that I'm adding are indicated like this):

appendChild:
Adds a child to the instance's childNodes at the end of the collection
insertAfter:
Adds a child to the instance's childNodes after the position of a specified existing child
insertBefore:
Adds a child to the instance's childNodes before the position of a specified existing child
insertChildAt:
Adds a child to the instance's childNodes at a specified index/position in the colelction
prependChild:
Adds a child to the instance's childNodes at the beginning of the collection
removeChild:
Removes the specified child from the childNodes collection
removeChildAt:
Removes the child at a specified index/position from the childNodes collection
removeSelf:
Removes the instance from its parent's childNodes collection
replaceChild:
Replaces the specified child in the instance's childNodes collection with a new child
replaceChildAt:
Replaces the child at a specified index/position in the instance's childNodes collection with a new child
Experience with a previous incarnation of this module (the one I am re-creating from the ground up) led me to the conclusion that any method that alters a Tag's childNodes should return a relevant node-value. That is, for example:
# Creating a <table> to display values in a list of dicts:
listOfDicts = [
    { 'name':'row1 - name', 'value':'row1 - value' },
    { 'name':'row2 - name', 'value':'row2 - value' },
    { 'name':'row3 - name', 'value':'row3 - value' },
    { 'name':'row4 - name', 'value':'row4 - value' },
    ]

table = Tag( 'table', border='1' )
thead = table.appendChild( Tag( 'thead' ) )
tbody = table.appendChild( Tag( 'tbody' ) )
tr = thead.appendChild( Tag( 'tr' ) )
for key in sorted( listOfDicts[ 0 ] ):
    th = tr.appendChild( Tag( 'th' ) )
    th.appendChild( Text( key ) )
for row in listOfDicts:
    tr = tbody.appendChild( Tag( 'tr' ) )
    for key in sorted( row ):
        td = tr.appendChild( Tag( 'td' ) )
        td.appendChild( Text( str( row[ key ] ) ) )
generates the following <table> quickly and easily because, in part, each appendChild returns the Tag appended, which can then be used to append other nodes to the new element:
name value
row1 - name row1 - value
row2 - name row2 - value
row3 - name row3 - value
row4 - name row4 - value

It didn't occur to me to check what the JavaScript methods I'm copying did until I wrote this post, but a cursory check indicates that they do the same thing — returning an appended child — so in that respect, at least, I feel this decision is solid. Similarly, anything that removes a child should return the child being removed, which is also what the JavaScript equivalents do. The lone JavaScript replace* method (replaceChild) returns the node being replaced, so I followed that convention in all of the remove* methods of Tag in the interests of consistency.

All of these methods are also responsible for making certain that the nodes being manipulated have their parent updated as part of the process. That is:

  • Any method that adds a child to a Tag assures that those child nodes' parent doesn't already exist, and that they are set to the Tag that they are being added to; and
  • Any method that removes a child also clears that child's parent (which makes it available to be added to a different Tag if needed);
Since the replace* methods are, essentially, just a removal of an existing child and the addition of a new one, they are responsible for performing both of those tasks if they aren't handled by calling some other methods.

The carry-through of nodes added to a Tag is shown in the implementation of appendChild, as is the checking of a child's parent before executing the addition to the Tag:

@describe.AttachDocumentation()
@describe.argument( 'child', 
    'the child to append to the instance\'s childNodes', 
    BaseNode
)
@describe.raises( TypeError, 
    'if the specified child is not an instance of BaseNode'
)
@describe.raises( MarkupError, 
    'if the child to be appended is already a child of another element'
)
@describe.returns( 'the child that was appended to the instance\'s '
    'childNodes' )
def appendChild( self, child ):
    if not isinstance( child, BaseNode ):
        raise TypeError( '%s.appendChild expects an instance of BaseNode, '
            'but was passed "%s" (%s)' % ( 
                self.__class__.__name__, child, type( child ).__name__ )
            )
    # Checking for the child's current parent
    if child.parent:
        raise MarkupError( '%s.appendChild cannot append "%s" to "%s" '
            'because the node to be appended is already a child of '
            'another element (%s)' % ( 
                self.__class__.__name__, child, self, child.parent )
            )
    # Append the child
    self._childNodes.append( child )
    # Set the child's new parent
    child._SetParent( self )
    # Return the child
    return child
The return of a removed item is shown in removeChild:
@describe.AttachDocumentation()
@describe.argument( 'child', 
    'the child node to remove', 
    BaseNode
)
@describe.raises( TypeError, 
    'if the specified child is not a BaseNode instance'
)
@describe.raises( MarkupError, 
    'if the specified child is not a child of the instance'
)
@describe.returns( 'the child node removed' )
def removeChild( self, child ):
    """
Removes the specified child from the instance's childNodes"""
    if not isinstance( child, BaseNode ):
        raise TypeError( '%s.replaceChild expects a BaseNode-derived object '
            'for its child argument, but was passed "%s" (%s)' % ( 
                self.__class__.__name__, child, 
                type( child ).__name__
            )
        )
    try:
        del self._childNodes[ self._childNodes.index( child ) ]
    except ValueError:
        raise MarkupError( '%s.removeChild could not remove %s because it '
            'is not a childNode of %s' % ( self.__class__.__name__, 
                child, self )
            )
    return child
Finally, the return of the old child is shown in replaceChildAt (which is called by replaceChild):
@describe.AttachDocumentation()
@describe.argument( 'index', 
    'the position of the child to be replaced in the instance\'s childNodes', 
    int, long
)
@describe.argument( 'newChild', 
    'the new child to replace the specified child with', 
    BaseNode
)
@describe.raises( TypeError, 
    'if the newChild specified is not a BaseNode-derived object'
)
@describe.returns( 'the child being replaced in the instance\'s '
    'childNodes' )
def replaceChildAt( self, index, newChild ):
    """
Replaces the child at the specified index/position in the instance's childNodes 
with a new child"""
    if not isinstance( newChild, BaseNode ):
        raise TypeError( '%s.replaceChild expects a BaseNode-derived object '
            'for its newChild argument, but was passed "%s" (%s)' % ( 
                self.__class__.__name__, newChild, 
                type( newChild ).__name__
            )
        )
    oldChild = self._childNodes[ index ]
    self._childNodes[ index ] = newChild
    oldChild._DelParent()
    newChild._SetParent( self )
    return oldChild

The Collected getElement* Methods

JavaScript provides a number of methods that can be used to retrieve zero-to-many tag-elements based on various criteria, and I've added three more to the mix in Tag:

getElementById:
Returns the first Tag child with an id-attribute whose value matches the id supplied
getElementsByAttributeValue:
Returns a list of Tag children that have a specific attribute whose value exactly matches the value supplied
getElementsByClassName:
Returns a list of Tag children that have a class-attribute (using the classList property) containing the value supplied
getElementsByNamespace:
Returns a list of Tag children whose namespaces match the one provided, including children of children that inherit their parent's namespace
getElementsByPath:
Returns a list of Tag children whose DOM-paths relative to the instance match the path specified
getElementsByTagName:
Returns a list of Tag children whose tag-names match the tag-name specified
Many of these methods rely on being able to start with a list of all of an instance's children, so the implementation of getElementsByTagName (which can fulfil that need) is important enough to show in some detail before discussing the remainder.

getElementsByTagName in JavaScript allows * to be provided as a wild-card tag-name. If that wild-card is provided, then the method returns all child tags. That behavior is mirrored in Tag.getElementsByTagName, as is the return of a null (None) value if there are no matches.

@describe.AttachDocumentation()
@describe.argument( 'tagName',
    'the tag-name to search for in child elements. Using "*" will return '
    'all children',
    str, unicode
)
@describe.returns( 'list of child IsElement objects whose tag-name matches '
    'the tagName provided, or None' )
@describe.raises( TypeError, 
    'if the tagName provided is not a str or unicode value'
)
def getElementsByTagName( self, tagName ):
    """
Gets all child tags whose name matches the tag-name provided"""
    if type( tagName ) not in ( str, unicode ):
        raise TypeError( '%s.getElementsByTagName expects a string or '
            'unicode value for the tag-name it\'s to search for but was '
            'passed "%s" (%s)' % ( 
                self.__class__.__name__, tagName, type( tagName ).__name__ )
            )
    results = []
    for child in self.children:
        if child.tagName == tagName or tagName == '*':
            results.append( child )
        subResults = child.getElementsByTagName( tagName )
        if subResults:
            results += subResults
    if results:
        return results
    return None
getElementsByTagName makes use of recursion, by calling itself again for each child found in the current execution and appending the results of that recursive call to the results at the current level of execution. I suspect that there may be a better way of implementing this method, perhaps using a generator in some fashion, but as of this writing, I simply haven't dug into the idea enough to see if it would be worth pursuing.

With getElementsByTagName available, several of the remaining getElement* methods become fairly simple candidate-filtering problems in their implementation:

getElementsByAttributeValue:
Each result is a candidate that has the specified attribute with the specified value
getElementsByClassName:
Each result is a candidate that has the specified value in its classList
getElementsByNamespace:
Each result is a candidate whose namespace matches the one provided
Each, then, follows the same pattern as getElementsByAttributeValue:
@describe.AttachDocumentation()
@describe.argument( 'name',
    'the name of the attribute whose value is to be checked',
    str, unicode
)
@describe.argument( 'value',
    'the value in the specified attribute that must be matched',
    str, unicode
)
@describe.raises( TypeError, 
    'if the specified name is not a str or unicode value' )
@describe.raises( TypeError, 
    'if the specified value is not a str or unicode value' )
@describe.returns( 'a list of Tag instance matching the '
    'attribute-name/-value criteria' )
def getElementsByAttributeValue( self, name, value ):
    """
Gets the child elements that have the attribute specified containg the value 
specified"""
    if type( name ) not in ( str, unicode ):
        raise TypeError( '%s.getElementsByAttributeValue expects a str or '
            'unicode value for its name, but was passed "%s" (%s)' % 
                ( self.__class__.__name__, name, 
                    type( name ).__name__
                )
            )
    if type( value ) not in ( str, unicode ):
        raise TypeError( '%s.getElementsByAttributeValue expects a str or '
            'unicode value for its value, but was passed "%s" (%s)' % 
                ( self.__class__.__name__, value, 
                    type( value ).__name__
                )
            )
    results = [ 
            c for c in self.getElementsByTagName( '*' ) 
            if c.attributes.get( name ) == value
        ]
    if results: 
        return results
    return None
The variations of the other two are, ultimately, just in the generation of the results being returned:
# getElementsByClassName
results = [ 
            c for c in self.getElementsByTagName( '*' ) 
            if className in c.classList 
        ]
# getElementsByNamespace
    results = [ 
        c for c in self.getElementsByTagName( '*' ) 
        if c.namespace == namespace
    ]

getElementById uses getElementsByAttributeValue as a helper-method, but also provides an optional strict argument (defaulting to False) that allows it to raise a MarkupError if more than one result is found:

# ...
results = self.getElementsByAttributeValue( 'id', value )
if strict and len( results ) > 1:
    raise MarkupError( '%s.getElementById, with strict enforcement, '
        'found more than one child with the specified id' % ( 
            self.__class__.__name__, value )
        )
if results:
    return results[ 0 ]
return None

The last remaining method in this group, getElementsByPath, may take some explanation. Consider a web-page that has a fair amount of content, including a lot contained in <div> tags. The page also has two <form>s in it, and within one form are a number of rows, constructed with <div>s. As part of the application's requirements, there is a need to apply some CSS classes to every form row <div>, without altering any of the others, and it has to be done, for whatever reason, server-side in the code. The form to be altered can be identified by a specific DOM path relative to the document, as can the <div>s that need to be altered. That path, to each of those <div>s, might look something like /div/form/fieldset/div from the <body> of the page.

That is what getElementsByPath is built to do. Like getElementsByTagName it uses recursion, but in this case it uses it to drill down through the DOM tree, matching tag-names (and allowing the same wild-card capabilities) in order to find all of the children in the right position relative to the Tag that the method was called from. Its implementation is simpler than might be expected, given the complexity of what it's doing:

@describe.AttachDocumentation()
@describe.argument( 'path', 
    'the path to find matching elements for, delimited by "/", and allowing '
    'wild-cards ("*")',
    str, unicode
)
@describe.raises( TypeError, 'if the supplied path is not a str or '
    'unicode value' )
@describe.returns( 'list of elements whose dom-path from the instance '
    'matched the one specified, or None' )
def getElementsByPath( self, path ):
    """
Gets all child tags that can be identified by following matching tag-names 
down the tree"""
    if type( path ) not in ( str, unicode ):
        raise TypeError( '%s.getElementsByPath expects a string or '
            'unicode value for the path it\'s to search for but was '
            'passed "%s" (%s)' % ( 
                self.__class__.__name__, path, type( path ).__name__ )
            )
    results = []
    try:
        tagName, subPath = path.split( '/', 1 )
    except:
        tagName = path
        subPath = None
    for child in self.children:
        if tagName == '*' or child.tagName == tagName:
            if subPath != None:
                subPathResults = child.getElementsByPath( subPath )
                if subPathResults:
                    results += subPathResults
            else:
                results.append( child )
    if results:
        return results
    return None

Attribute-Related Methods

getAttribute:
Returns the value of the named attribute of the instance, or None if it doesn't exist
hasAttribute:
Checks for the existence of a specific attribute in the instance's attributes collection
hasAttributes:
Checks forthe existence of any attributes in the instance's attributes collection
removeAttribute:
Removes the specified attribute from the instance's attributes collection
setAttribute:
Sets the value of an attribute in the instance's attributes collection

Minus the type-checking of the name argument, getAttribute is really nothing more than:

return self.attributes.get( name )

Similarly, hasAttribute is:

if self.attributes.get( name ):
        return True
    return False
and hasAttributes is:
if self.attributes:
        return True
    return False

Setting attributes is a bit more detailed, but only because the name and value of the inbound attribute are both checked before the set actually occur, and the special handling for data-* attributes:

@describe.AttachDocumentation()
@describe.argument( 'name', 
    'the name of the attribute to set', 
    str, unicode
)
@describe.argument( 'value', 
    'the value of the attribute to set', 
    str, unicode, None
)
@describe.raises( TypeError, 
    'if the supplied name is not a str or unicode value'
)
@describe.raises( TypeError, 
    'if the supplied value is not a str or unicode value or None'
)
@describe.raises( ValueError, 
    'if the supplied name is not a valid attribute-name'
)
@describe.raises( ValueError, 
    'if the supplied name is not a valid attribute-value'
)
def setAttribute( self, name, value ):
    if type( name ) not in ( str, unicode ):
        raise TypeError( '%s.setAttribute expects a str or unicode value '
            'that is a valid attribute-name for the name of the attribute '
            'to be set, but was passed "%s" (%s)' % ( 
                self.__class__.__name__, name, type( name ).__name__
            )
        )
    if not self.attributes.IsValidName( name ):
        raise ValueError( '%s.setAttribute expects a str or unicode value '
            'that is a valid attribute-name for the name of the attribute '
            'to be set, but was passed "%s" which is not valid' % ( 
                self.__class__.__name__, name
            )
        )
    if value == None:
        try:
            del self._attributes[ name ]
        except KeyError:
            pass
        return
    if type( value ) not in ( str, unicode ):
        raise TypeError( '%s.setAttribute expects a str or unicode value '
            'that is a valid attribute value for the value of the attribute '
            'to be set, but was passed "%s" (%s)' % ( 
                self.__class__.__name__, name, type( name ).__name__
            )
        )
    if not self.attributes.IsValidValue( value ):
        raise ValueError( '%s.setAttribute expects a str or unicode value '
            'that is a valid attribute value for the value of the attribute '
            'to be set, but was passed "%s" (%s)' % ( 
                self.__class__.__name__, name, type( name ).__name__
            )
        )
    if name[0:5] == 'data_':
        name = name.replace( 'data_', 'data-' )
    self._attributes[ name ] = value
Attribute removal, though, is also very simple — barring the type-checking of the name, it's basically just a fail-safe deletion of an item from the attributes collection:
try:
    del self.attributes[ name ]
except KeyError:
    pass

The balance of Tag's methods are pretty straightforward, and I won't go into any depth on them:

cloneNode:
Returns a copy of the Tag with the option of returning copies of all of the its childNodes as well
contains:
Determines if a Tag contains another specified node
hasChildNodes:
Determines if a Tag has any childNodes members
The cloneNode method, like the innerHTML property, had to wait until I've got MarkupParser implemented. My plan for implementing it centers around either creating a new Tag instance for shallow copies, or using the MarkupParser class to generate complete copies of a markup-tree from the __str__ and/or __unicode__ methods of Tag, since nodes in general, and tags in particular, cannot have multiple parents (as noted earlier).

In light of how much code there actually is behind the implementation of Tag, and how much of it I didn't do any sort of deep dive into in this post or the last one, I figured I'd share the Tag class code, as well as its unit-test code before I signed off for the day. These are not the complete markup or test_markup modules, so they won't actually execute for lack of various dependencies, but all the code for both (as of this post) is there:

92.9kB
120.1kB

Thursday, April 27, 2017

Generating and Parsing Markup in Python [3]

With two interfaces and one abstract class done, there's one more abstract class that needs attention before I can get to some concrete implementation (finally): the HasTextData abstract class.

HasTextData is a common super-class for the CDATA, Comment and Text concrete classes, and completion of those three classes is my goal for today's post.

What's Common Between These Classes?

Any time there's an abstract class that multiple concrete classes derive from, there's some common basis of functionality that the concrete classes share. That is, after all, one of the reasons that an abstract class gets defined. In the case of HasTextData, that commonality is that all of the derived classes have a text-data property (data in JavaScript, though the textContent property will also return that inner text in JavaScript).

Another common factor, though it may be less obvious, is that all three of these node-types have some rules about what their data can contain. Those rules aren't necessarily active in a client-side JavaScript implementation (likely because some sort of action is taken to prevent destructive or counterproductive content-manipulation — like setting the inner text of a comment to -->). Since the markup being created has to be issued out to a client browser in some fashion after it's been created and/or altered on the server side, though, there is a need to enforce at least some basic content-protection or escaping for all three of those concrete classes, though the specifics of what they are or do will probably vary pretty significantly.

All three of those concrete classes also have to be able to be rendered in some fashion and returned to the client browser as text-data. It could be str- or unicode-typed text, and should probably support both on basic principle, but somewhere along the line whatever any individual instances exist as part of a response, they should contribute something to the source-code of the response. I'll dig in to the rendering aspects of all nodes later on in this post — first, some actual implementation!

The HasTextData Abstract Class

Because, once again, the JavaScript entities that I'm trying to stay consistent with allow more than one property or method entry-point into the underlying data — in this case, both data and textContent properties being capable of getting, setting, or deleting the text-data of a comment- or text-node — I don't have any better option than to have multiple properties defined that allow the same capability.

#-----------------------------------#
# Instance Properties (abstract OR  #
# concrete!)                        #
#-----------------------------------#

data = describe.makeProperty(
    _GetTextData, _SetTextData, _DelTextData, 
    'the raw text-data of the instance', 
    str, unicode
)
textContent = describe.makeProperty(
    _GetTextData, _SetTextData, _DelTextData, 
    'the raw text-data of the instance', 
    str, unicode
)
The property-methods are pretty straightforward, though there is something new in the _SetTextData setter-method:
@describe.AttachDocumentation()
    @describe.argument( 'value',
        'the raw text data to set in the instance',
        str, unicode
    )
    @describe.raises( TypeError, 
        'if a value is passed that is not a str or unicode type'
    )
    # self._SanitizeInput can raise ValueError
    @describe.raises( ValueError, 
        'if a value is passed that cannot be sanitized by the '
        'instance to make rendering safely viable'
    )
    def _SetTextData( self, value ):
        """
Gets the raw text-data of the instance"""
        if type( value ) not in ( str, unicode ):
            raise TypeError( '%s.TextData expects a str or '
                'unicode value, but was passed "%s" (%s)' % ( 
                    self.__class__.__name__, 
                    value, type( value ).__name__
                )
            )
        # Make sure that the supplied raw text-data is safe to 
        # store before storing it.
        value = self._SanitizeInput( value )
        self._textData = value
This method makes an attempt to sanitize the supplied input, with the intention being that the sanitized input will alter anything that could result in rendering issues on the client side. The specifics of the sanitization will vary at least a little bit in the concrete class implementations, so _SanitizeInput is abstracted in HasTextData:
@abc.abstractmethod
@describe.raises( ValueError, 
    'if a value is passed that cannot be sanitized by the '
    'instance to make rendering safely viable'
)
def _SanitizeInput( self, value ):
    raise NotImplementedError( '%s._SanitizeInput is not implemented as '
        'required by HasTextData' % self.__class__.__name__ )

Rendering Considerations

All of the concrete classes, not just CDATA, Comment and Text, will eventually need to be able to be rendered and returned as part of a web request-response cycle. As things stand right now, having done some cursory examination of both mod_python and wsgi response-functionality, I'm inclined to handle that by using the __str__ and/or __unicode__ magic methods that are available to all Python objects.

The main rationale for this is that both mod_python and basic wsgi response-functionality, ultimately, just need to return the text of a response. The specific mechanisms how that response is returned may vary, maybe even vary a lot, but that returning-some-text is the key.

That more or less requires that __str__ and __unicode__ be defined as abstract methods somewhere in the class-hierarchy. Since they should be available to all nodes (and because it'd keep that functional requirement in a single place), I'm going to add them all the way back up in IsNode:

@abc.abstractmethod
def __str__( self ):
    raise NotImplementedError( '%s.__str__ is not implemented as '
        'required by IsNode' % self.__class__.__name__ )

@abc.abstractmethod
def __unicode__( self ):
    raise NotImplementedError( '%s.__unicode__ is not implemented as '
        'required by IsNode' % self.__class__.__name__ )

Unit-testing HasTextData

Of the required test-methods for HasTextData, only one can really be implemented at this point: test_SanitizeInput, following the usual structure for testing that an abstract member is abstract in the class. The rest, all of the property-methods, would all rely on a concrete implementation of _SanitizeInput. There are a couple of different approaches that could be taken at this point to resolve this conundrum:

  • Skip the tests in testHasTextData and make sure that the derived-class tests test them adequately; or
  • Create a HasTextData-derived test-class that has a concrete implementation of _SanitizeInput, then test against that test-class.
Of the two, I prefer the second approach. It requires less testing later on in the test-cases for the concrete classes, doesn't rely on someone remembering that the properties need to be individually tested there later on.

Setting up a test-class is simple in this case:

class HasTextDataDerived( HasTextData ):
    def __init__( self ):
        HasTextData.__init__( self )
    def _SanitizeInput( self, value ):
        return '[Sanitized] %s' % value
Since part of the test-process is to assure that all of the setter-methods are calling the _SanitizeInput of the test-class, it actually needs to alter the value submitted, hence the [Sanitized] addition to the submitted value.

Since the data and textContent properties should both use the same getter-, setter- and deleter-methods, only one of the test-methods between the two required for those properties actually needs to check the functionality. The other one can be tested by asserting that the underlying methods are identical:

def testdata(self):
    """Unit-tests the data property of a HasTextData instance."""
    testObject = HasTextDataDerived()
    # test default state
    self.assertEquals( testObject.data, '', 
        'The default data value for a newly-created instance should '
        'be an empty string' )
    # Test setting then getting all "good" values
    for testValue in UnitTestValuePolicy.Text:
        testObject.data = testValue
        expected = '[Sanitized] %s' % testValue
        actual = testObject.data
        self.assertEquals( actual, expected, 
            'instance.data should equal %s if it was set to %s, '
            'but %s was returned instead' % ( 
                expected, testValue, actual
            )
        )
    # Test setting all "bad" values and keeping the previous state
    testObject.data = 'original value'
    expected = testObject.data
    for testValue in ( 
        UnitTestValuePolicy.Numeric + 
        UnitTestValuePolicy.Boolean.Strict + [ object() ] ):
        try:
            testObject.data = testValue
            self.fail( 'Setting instance.data to a non-string value '
                '("%s" [%s]) should raise a TypeError' % ( 
                    testValue, type( testValue ).__name__
                )
            )
        except TypeError:
            self.assertEquals( testObject.data, expected,
                'Failure to set instance.data should have left it '
                    'set to "%s", but it was re-set to "%s"' %
                    ( expected, testObject.data )
            )

def testtextContent(self):
    """Unit-tests the textContent property of a HasTextData instance."""
    self.assertEquals( 
        HasTextData.textContent.fget, HasTextData.data.fget, 
        'HasTextData.textContent and HasTextData.data '
        'are expected to use the same property-getter method'
    )
    self.assertEquals( 
        HasTextData.textContent.fset, HasTextData.data.fset, 
        'HasTextData.textContent and HasTextData.data '
        'are expected to use the same property-setter method'
    )
    self.assertEquals( 
        HasTextData.textContent.fdel, HasTextData.data.fdel, 
        'HasTextData.textContent and HasTextData.data '
        'are expected to use the same property-deleter method'
    )
Note that we're finally putting the UnitTestValuePolicy constant, defined about a month ago, to use.

With those property-tests in place and passing, the question arises of whether there's any useful testing that can be done of the underlying methods of the properties. This was one of the items that came up when I posted the Unit-Testing Walk-through a couple of weeks back, that I didn't want to get too far into the weeds about at the time, but it's probably a good time to address it in some detail now that there's a more detailed example to look at for context.

In general, and in keeping with the thoroughly tested goal in my coding standards, the unit-testing policy requires that test-methods be defined for all public and protected members. The implication, I hope, is that all the required test-methods should also be implemented — otherwise why have the requirement for their definition. That may well break down in the case of properties and their underlying methods, though. Consider the testdata test-method above. It:

  • Calls the _DelTextData property-deleter method (at least indirectly, during initialization of the HasTextDataDerived test-class, which calls HasTextData.__init__, which calls self._DelTextData);
  • Calls _SetTextData during every property-value assignment; and
  • Calls _GetTextData during every property-value retrieval.
Since the value-assignment calls also use both good values (that should not raise errors) and bad values (that should), every path through every underlying property-method has been executed and shown to behave as expected. Since that is the goal of unit-testing, it follows that the test-methods for the property-methods aren't really needed if the properties that use them test completely and successfully.

I'd rather not try to work out a way to automatically skip, or otherwise remove property-methods from required test-methods, though. Even if it were possible to determine the relationship (my initial tests against that idea lead me to think it's not), doing so feels... fragile, maybe? Although I can't think of a case where I'd expect to need separate tests for the property-methods, I can't rule out that such cases could exist (at least not yet).

Taking all of that together, I think this is sufficient justification for skipping the test-methods of the underlying property-methods, so long as the reason for skipping them is noted:

@unittest.skip( 'Adequately tested by the testdata method' )
def test_DelTextData(self):
    """Unit-tests the _DelTextData method of a HasTextData instance."""
    self.fail( 'test_DelTextData is not implemented' )

@unittest.skip( 'Adequately tested by the testdata method' )
def test_GetTextData(self):
    """Unit-tests the _GetTextData method of a HasTextData instance."""
    self.fail( 'test_GetTextData is not implemented' )

@unittest.skip( 'Adequately tested by the testdata method' )
def test_SetTextData(self):
    """Unit-tests the _SetTextData method of a HasTextData instance."""
    self.fail( 'test_SetTextData is not implemented' )
That leaves the unit-test results:
########################################
Unit-test results
########################################
Tests were successful ... False
Number of tests run ..... 67
 + Tests ran in ......... 0.01 seconds
Number of errors ........ 0
Number of failures ...... 15
########################################

Implementing CDATA, Comment and Text Classes

Since I don't have a complete class-diagram (with all of the members of the items I've defined so far), I started by creating stub-classes for CDATA, Comment and Text, then created a test-case for one of them (I picked testCDATA) to get a list of all of the members that will need to be defined for all three concrete classes. The test-case returned:

TypeError: Can't instantiate abstract class CDATA with 
abstract methods 
    _SanitizeInput, __str__, __unicode__, cloneNode, 
    isEqualNode, nodeName, nodeType, textContent, 
    toString
Since all three of these concrete classes derive from BaseNode and HasTextData, this list should hold true for all of them, at least as a starting-point.

Or it would, except that I noticed that textContent was appearing in the list. And I just got finished implementing textContent in HasTextData! As it turns out, the reason this happened was pretty simple, I'd just forgotten some of the rules about Python's MRO. To explain, let me start by showing the original definition of CDATA I had:

@describe.InitClass()
class CDATA( BaseNode, HasTextData, object ):
    """
Represents a CDATA section in a markup tree"""
# ...
BaseNode and HasTextData both define the textContent property of an instance — one (BaseNode) as an abstract property requirement that it inherits from IsNode, the other (HasTextData) as a concrete property that, in theory, should be fulfilling that interface contract. The problem is that when Python reads super-classes, they are handled last-to-first, so that BaseNode.textContent ends up overriding HasTextData.textContent. This can be shown by switching the order of those super-classes...
@describe.InitClass()
class CDATA( HasTextData, BaseNode, object ):
    """
Represents a CDATA section in a markup tree"""
# ...
...and re-running the unit-test results, yielding:
TypeError: Can't instantiate abstract class CDATA with 
abstract methods 
    _SanitizeInput, __str__, __unicode__, cloneNode, 
    isEqualNode, nodeName, nodeType, toString
With that change, textContent no longer appears in the list of abstract members that need to be implemented in the concrete class.

If I haven't mentioned it before, I'll say it now: One of the reasons that I like Python is that it allows multiple inheritance. That makes a lot of class-structure design cleaner, I think, since it's possible to keep all functionality relating to a single aspect of multiple classes' functionality in a single place in the code. That usually eliminates, but at a minimum reduces the likelihood of needing and implementing duplicate code across multiple classes. There are some trade-offs that arise, though, and this sort of inheritance-order dependency is an example of one of them — the code becomes more sensitive to the specific order of inheritance. There are a at least two different ways this could be dealt with.

The first is the simple reversal that I already showed. The caveat with that approach is that the class-definitions are a bit more fragile — particularly if yet another class gets added into the mix for any of the concrete classes. That's a minimal risk at this point, though, I thnk — while there are two places that textContent is defined, and there may be other properties that will have similar duplicated definitions, I don't expect that there are any more that have the kind of combination that textContent has: and abstract requirement and a concrete implementation originating from different places in the inheritance tree.

The other would be to change the ineritance-tree. Right now the problem is that BaseNode (with its IsNode parent) lives in a completely separate branch of the tree than HasTextData does. If HasTextData were moved so that it's derived from BaseNode, then the textContent of BaseNode would be overridden by HasTextData:

If there is a caveat with this approach, it'd be that the resulting inheritance-structure is, perhaps, starting to get too deep. That, ultimately, is a matter of opinion, but I feel that three parent inheritance levels is about as deep as I'm comfortable with, at least in this particular case. I like this approach, apart from my reservations about the depth of the class-hierarchy. It keeps the inheritance path cleaner, and just... feels better, really. The only other change that it will require will be adding a bunch of dummy methods (all the ones that didn't exist as requirements before) in the HasTextDataDerived derived class in test_markup, but those don't need to be anything more complex than:
class HasTextDataDerived( HasTextData ):
    def __init__( self ):
        HasTextData.__init__( self )
    def _SanitizeInput( self, value ):
        return '[Sanitized] %s' % value
    def __str__( self ):
        pass
    def __unicode__( self ):
        pass
    def cloneNode( self ):
        pass
    def isEqualNode( self ):
        pass
    def nodeName( self ):
        pass
    def nodeType( self ):
        pass
    def toString( self ):
        pass

Some Commonalities in these Classes

Looking at the list of dummy methods above, it occured to me that most of the methods listed there, all of them from cloneNode on, could be moved to HasTextData as concrete implementations.

The cloneNode method really doesn't need to do anything more than create and return a new instance of the class, populated with the data of the instance being cloned. That can be done with something pretty simple:

@describe.AttachDocumentation()
@describe.argument( 'deep', 
    'indicates whether to make a "deep" copy (True) or '
    'not (False); irrelevant for HasTextData nodes',
    bool
)
@describe.returns( 'a new instance of the class, populated '
    'with the data of the current instance' )
def cloneNode( self, deep=False ):
    """
Clones the instance."""
    return self.__class__( self._textData )
Testing it is pretty simple:
def testcloneNode(self):
    """Unit-tests the cloneNode method of a HasTextData instance."""
    # Test instances using all "good" values
    for testValue in UnitTestValuePolicy.Text:
        instance1 = HasTextDataDerived( testValue )
        instance2 = instance1.cloneNode()
        self.assertEquals( instance1.__class__, instance2.__class__, 
            'cloneNode should return the same type of object, '
            'but instance2 was a %s, not a %s' % ( 
                instance2.__class__.__name__, 
                instance1.__class__.__name__
            )
        )
        self.assertEquals( instance1.data, instance2.data, 
            'an instance returned by cloneNode should have the same data '
            'as the original instance, but the cloned instance had "%s" '
            'instead of "%s"' % ( instance2.data, instance1.data )
        )

isEqualNode is similarly simple:

@describe.AttachDocumentation()
@describe.argument( 'deep', 
    'indicates whether to make a "deep" copy (True) or '
    'not (False); irrelevant for HasTextData nodes',
    bool
)
@describe.returns( 'True if the other node is the same type and '
    'has the same data as the instance, False otherwise.' )
def isEqualNode( self, other ):
    """
Compares the instance against another item."""
    return ( 
        self.__class__ == other.__class__
        and self.data == other.data
    )
This approach also makes the original criteria-list for isEqualNode from the w3schools site moot — If the instance and the other object are of the same type, they'll have all of the same values common to any instance of the class, so there's no need to do anything more than compare the classes of self and other and the data values of them. The test-method requires the creation of another class derived from HasTextData, but it's pretty much a carbon copy of the original derived test-class (HasTextDataDerived), and is also very simple:
def testisEqualNode( self ):
    """Unit-tests the isEqualNode method of a HasTextData instance."""
    # Test instances using all "good" values
    for testValue in UnitTestValuePolicy.Text:
        instance1 = HasTextDataDerived( testValue )
        # same class, same content
        instance2 = HasTextDataDerived( testValue )
        self.assertTrue( instance1.isEqualNode( instance2 ), 
            'Same class and same content should return isEqualNode '
            'of True' )
        # different class, same content
        instance2 = HasTextDataDerived2( testValue )
        self.assertFalse( instance1.isEqualNode( instance2 ), 
            'Different class and same content should return isEqualNode '
            'of False' )
        # same class, different content
        instance2 = HasTextDataDerived( 'other content' )
        self.assertFalse( instance1.isEqualNode( instance2 ), 
            'Same class and different content should return isEqualNode '
            'of False' )
        # different class and different content
        instance2 = HasTextDataDerived2( 'other content' )
        self.assertFalse( instance1.isEqualNode( instance2 ), 
            'Different class and different content should return '
            'isEqualNode of False' )

The nodeName and nodeType propeties can be defined to return a class-level attribute that is defined as None in HasTextData, and that will set to a different value in the concrete classes:

#-----------------------------------#
# Class attributes (and instance-   #
# attribute default values)         #
#-----------------------------------#

_nodeName = None
_nodeType = None

#-----------------------------------#
# Instance property-getter methods  #
#-----------------------------------#

@describe.AttachDocumentation()
def _GetnodeName( self ):
    """
Gets the (class-constant) name of the node"""
    try:
        result = self.__class__._nodeName
        if result == None:
            raise AttributeError()
    except AttributeError:
        raise AttributeError( '%s does not have a class-level '
            'definition of _nodeName, or it is inheriting the None '
            'value defined by HasTextData' % ( 
                self.__class__.__name__
                )
            )
    return result

@describe.AttachDocumentation()
def _GetnodeType( self ):
    """
Gets the (class-constant) type of the node"""
    try:
        result = self.__class__._nodeType
        if result == None:
            raise AttributeError()
    except AttributeError:
        raise AttributeError( '%s does not have a class-level '
            'definition of _nodeType, or it is inheriting the None '
            'value defined by HasTextData' % ( 
                self.__class__.__name__
                )
            )
    return result

# ...

#-----------------------------------#
# Instance Properties (abstract OR  #
# concrete!)                        #
#-----------------------------------#

nodeName = describe.makeProperty(
    _GetnodeName, None, None, 
    'the (class-constant) name of the node', 
    str, unicode
)
nodeType = describe.makeProperty(
    _GetnodeType, None, None, 
    'the (class-constant) type of the node', 
    str, unicode
)
The test-methods for those:
def testnodeName(self):
    """Unit-tests the nodeName property of a HasTextData instance."""
    instance = HasTextDataDerived()
    actual = instance.nodeName
    expected = HasTextDataDerived._nodeName
    self.assertEquals( actual, expected,
        'An instance of HasTextData with a defined _nodeName '
        'should return that value in its nodeName proeprty, but '
        '"%s" (%s) was returned instead' % ( 
            actual, type( actual ).__name__
        )
    )
    instance = HasTextDataDerived2()
    try:
        actual = instance.nodeName
        expected = HasTextDataDerived._nodeName
        self.fail( 'An instance of HasTextData that does not have '
            'a _nodeName class-propery defined should raise an '
            'AttributeError if nodeName is retrieved' )
    except AttributeError:
        pass

def testnodeType(self):
    """Unit-tests the nodeType property of a HasTextData instance."""
    instance = HasTextDataDerived()
    actual = instance.nodeType
    expected = HasTextDataDerived._nodeType
    self.assertEquals( actual, expected,
        'An instance of HasTextData with a defined _nodeType '
        'should return that value in its nodeType proeprty, but '
        '"%s" (%s) was returned instead' % ( 
            actual, type( actual ).__name__
        )
    )
    instance = HasTextDataDerived2()
    try:
        actual = instance.nodeType
        expected = HasTextDataDerived._nodeType
        self.fail( 'An instance of HasTextData that does not have '
            'a _nodeType class-propery defined should raise an '
            'AttributeError if nodeType is retrieved' )
    except AttributeError:
        pass
with the modified derived test-classes as:
class HasTextDataDerived( HasTextData ):
    _nodeName = '#hasTextDataDerived'
    _nodeType = 1024
    def __init__( self, textData=None ):
        HasTextData.__init__( self, textData )
    def _SanitizeInput( self, value ):
        if value[ 0:12 ] != '[Sanitized] ':
            return '[Sanitized] %s' % value
        else:
            return value
    def __str__( self ):
        pass
    def __unicode__( self ):
        pass
    def toString( self ):
        pass

class HasTextDataDerived2( HasTextData ):
    def __init__( self, textData=None ):
        HasTextData.__init__( self, textData )
    def _SanitizeInput( self, value ):
        if value[ 0:12 ] != '[Sanitized] ':
            return '[Sanitized] %s' % value
        else:
            return value
    def __str__( self ):
        pass
    def __unicode__( self ):
        pass
    def toString( self ):
        pass

Finally, the toString methods. In JavaScript, toString returns a decription of the node rather than its content:

comment = document.createComment( 'comment-node' );
text = document.createTextNode( 'text-node' );
comment.toString();
text.toString();
yields
"[object Comment]"
"[object Text]"
That strikes me as being directly equivalent to the built-in __repr()__ method, which returns something looking like this:
<[module].[class-name] object at [hex-number]>
I'll use that, then. Since all that will do is return the __repr__() results for the instance, I see no reason not to just skip the unit-test for it. The actual implementation of HasTextData.toString is dead simple:
@describe.AttachDocumentation()
@describe.returns( 'A string description of the instance' )
def toString( self ):
    """
Returns a description of the instance"""
    return self.__repr__()

I hadn't expected to do all the shuffling of functionality into HasTextData that I've done and show, so this is getting long, but I really want to get the concrete classes that derive from it finished before I call it a day. Fortunately, all that movement of functionality doesn't leave much to implement in them: All that they really need is implementation of _SanitizeInput, __str__ and __unicode__.

Final Implementation of CDATA

The main purposes that the remaining required methods of CDATA serve are to ensure that the data, when sent to a client browser, won't be broken (_SanitizeInput) and to provide rendered output of the instance, allowing for normal string-values and unicode values both (__str__ and __unicode__). None of these are particularly difficult operations:

@describe.AttachDocumentation()
@describe.argument( 'value', 
    'the text-value to sanitize',
    str, unicode
)
@describe.raises( ValueError, 
    'if a value is passed that cannot be sanitized by the '
    'instance to make rendering safely viable'
)
@describe.returns( 'A sanitized str or unicode value' )
def _SanitizeInput( self, value ):
    """
Sanitizes the provided input-value to make sure it's safe to store and issue 
to a client browser"""
    if ']]>' in value:
        raise TypeError( '%s cannot contain "]]>" as a literal value '
            'in its text-content.' % ( self.__class__.__name__ ) )
    return value

@describe.AttachDocumentation()
@describe.returns( 'The instance rendered as a str' )
def __str__( self ):
    """
Renders the instance as a string value"""
    return '<![CDATA[ %s ]]>' % ( self.data )

@describe.AttachDocumentation()
@describe.returns( 'The instance rendered as a unicode' )
def __unicode__( self ):
    """
Renders the instance as a unicode value"""
    return u'<![CDATA[ %s ]]>' % ( self.data )

Of the three, _SanitizeInput probably requires the most explanation. If it were to allow data values that contained ]]> then it would be possible for a CDATA to render as something like <![CDATA[ This is my CDATA content.]]> ]]> — and that would cause rending issues in the client browser that the rendered CDATA was handed off to.

While there is provision through the __unicode__ method for unicode content-output, I may still need to work out some sort of mechanism or process that will allow a __str__ call to call an instance's __unicode__ instead, if there is reason for doing so. I suspect that will involve checking for various unicode errors (UnicodeDecodeError and UnicodeEncodeError, perhaps?), but I'm not sure yet how that's going to work, or even if it'll be necessary.

Final Implementation of Comment

The same three methods in Comment look very much like their counterparts in CDATA, and for much the same reasons:

@describe.AttachDocumentation()
@describe.argument( 'value', 
    'the text-value to sanitize',
    str, unicode
)
@describe.raises( ValueError, 
    'if a value is passed that cannot be sanitized by the '
    'instance to make rendering safely viable'
)
@describe.returns( 'A sanitized str or unicode value' )
def _SanitizeInput( self, value ):
    """
Sanitizes the provided input-value to make sure it's safe to store and issue 
to a client browser"""
    if '-->' in value:
        raise TypeError( '%s cannot contain "-->" as a literal value '
            'in its text-content.' % ( self.__class__.__name__ ) )
    return value

@describe.AttachDocumentation()
@describe.returns( 'The instance rendered as a str' )
def __str__( self ):
    """
Renders the instance as a string value"""
    return '<!-- %s -->' % ( self.data )

@describe.AttachDocumentation()
@describe.returns( 'The instance rendered as a unicode' )
def __unicode__( self ):
    """
Renders the instance as a unicode value"""
    return u'<!-- %s -->' % ( self.data )

Final Implementation of Text

The only consideration for sanitizing the data of a Text is to make sure that it isn't going to accidentally include any tag-items in its rendered output. Ensuring that is a sinple mater of escaping any < characters during the process of setting its data. Technically, that should be all that's required, since browsers are usually pretty good about just rendering > characters if they aren't part of a detectable tag-structure, but in the interests of making sure that tag-delimiter characters are all escaped, I'm going to escape both < and >.

@describe.AttachDocumentation()
@describe.argument( 'value', 
    'the text-value to sanitize',
    str, unicode
)
@describe.raises( ValueError, 
    'if a value is passed that cannot be sanitized by the '
    'instance to make rendering safely viable'
)
@describe.returns( 'A sanitized str or unicode value' )
def _SanitizeInput( self, value ):
    """
Sanitizes the provided input-value to make sure it's safe to store and issue 
to a client browser"""
    sanitized = value.replace( '<', '&lt;' )
    sanitized = sanitized.replace( '>', '&gt;' )
    return sanitized

@describe.AttachDocumentation()
@describe.returns( 'The instance rendered as a str' )
def __str__( self ):
    """
Renders the instance as a string value"""
    return str( self.data )

@describe.AttachDocumentation()
@describe.returns( 'The instance rendered as a unicode' )
def __unicode__( self ):
    """
Renders the instance as a unicode value"""
    return unicode( self.data )

That gets me just under 50% of the way done with the markup module's classes:

It's a bit early for a full snapshot of the current idic package, but since I didn't show all of the code for the work done today, it seems fair to set up downloads of the current markup.py and test_markup.py files before I sign off for the day:

40.6kB

Tuesday, April 4, 2017

A Unit-testing Walk-through — from the Ground Up

If you've been following this blog for any length of time, it should come as no great surprise to you that I have set up template-files/code-snippets for writing unit-test modules and individual test-cases. I won't show them all at once this time — they will be exposed sufficiently, I think, as I write the tests for the serialization module — but they are written with an eye towards making my normal unit-testing process as smooth and painless as possible:

  • Create a test-module from the UnitTestsTemplate.py file, named test_{module being tested}.py, in a project directory named test_{project name};
  • Set up the various namespace- and module-name-values that the test-module needs to be able to find the source-module being tested;
  • Run the test-module until there are no failed tests.
    • Generate test-case classes to address each failure that stems from a requirement reported from the code-coverage test;
    • Generate test-methods to address each failure that stems from a requirement reported from the tests generated by the AddMethodTesting and AddPropertyTesting decorators;
    • Resolve any other failures reported;
    • Resolve any errors reported;
    • If test-methods cannot be usefully generated, apply the unittest.skip decorator to those.
  • If the test-module should be part of a larger set of tests (say, for an entire package), then add the test-module to the relevant parent module- or package-test file, then run it as above;
Since the serialization module is part of the idic package, the end-result of todays post should be a test_idic.py file that calls the tests from a test_serialization.py file. I'll create the test_serialization.py file first, then work my way up to test_idic.py.

Starting with the UnitTestsTemplate.py file

So, following my process, the first things I need to do are create a copy of the unit-test template file in the appropriate location, and change all of the namespace- and module-name strings in it to match what I'm testing. The most relevant chunk of that happens at the start of the file, which looks like this initially:

#!/usr/bin/env python
"""Defines unit-tests for the module at PackagePath.ModuleName."""

# Python unit-test-module template. Copy the template to a new 
# unit-test-module location, and start replacing names as needed:
#
# PackagePath  ==> The path/namespace of the parent of the module/package 
#                  being tested in this file.
# ModuleName   ==> The name of the module being tested
#
# Then remove this comment-block

#-----------------------------------#
# Standard-library imports.         #
#-----------------------------------#

import os
import sys
import unittest

#-----------------------------------#
# Imports of other third-party      #
# libraries and functionality.      #
#-----------------------------------#

#-----------------------------------#
# idic-library imports.             #
#-----------------------------------#
# - Local development path
sys.path.insert( 1, os.path.expanduser( 
    '~/path/to/local/project/lib/project_name' ) )
# - Installed location
sys.path.insert( 1, '/usr/local/lib/idic' )

from idic.unit_testing import *

#-----------------------------------#
# Import the module being tested    #
#-----------------------------------#
LocalSuite = unittest.TestSuite()

#-----------------------------------#
# Import the module being tested    #
#-----------------------------------#
import PackagePath.ModuleName as ModuleName

#-----------------------------------#
# Code-coverage test-case and       #
# decorator-methods                 #
#-----------------------------------#

class testModuleNameCodeCoverage( moduleCoverageTest ):
    _testNamespace = 'PackagePath'
    _testModule = ModuleName

LocalSuite.addTests( 
    unittest.TestLoader().loadTestsFromTestCase( 
        testModuleNameCodeCoverage
    )
)

#-----------------------------------#
# Test-cases in the module          #
#-----------------------------------#
A couple of quick search-and-replaces in the file are all I need to do to get started:
  • Replacing every instance of PackagePath with the namespace path to the module being tested (in this case, idic, since the full namespace of the serialization module would be idic.serialization); and
  • Replacing every instance of ModuleName with the module name of the module being tested (serialization in this case).
After those replacements (and removing the comments near the top as they instruct), the unit-test module starts with:
#!/usr/bin/env python
"""Defines unit-tests for the module at idic.serialization."""

#-----------------------------------#
# Standard-library imports.         #
#-----------------------------------#

import os
import sys
import unittest

#-----------------------------------#
# Imports of other third-party      #
# libraries and functionality.      #
#-----------------------------------#

#-----------------------------------#
# idic-library imports.             #
#-----------------------------------#
# - Local development path
sys.path.insert( 1, os.path.expanduser( 
    '~/path/to/local/project/lib/project_name' ) )
# - Installed location
sys.path.insert( 1, '/usr/local/lib/idic' )

from idic.unit_testing import *

#-----------------------------------#
# Import the module being tested    #
#-----------------------------------#
LocalSuite = unittest.TestSuite()

#-----------------------------------#
# Import the module being tested    #
#-----------------------------------#
import idic.serialization as serialization

#-----------------------------------#
# Code-coverage test-case and       #
# decorator-methods                 #
#-----------------------------------#

class testserializationCodeCoverage( moduleCoverageTest ):
    _testNamespace = 'idic'
    _testModule = serialization

LocalSuite.addTests( 
    unittest.TestLoader().loadTestsFromTestCase( 
        testserializationCodeCoverage
    )
)

#-----------------------------------#
# Test-cases in the module          #
#-----------------------------------#
Right now, with those changes made, it's executable as long as all the import-paths are correct. Running the test-module generates the following output, which tells what the next steps are:
############################################################
Unit-test results
############################################################
Tests were successful ... False
Number of tests run ..... 2
Number of errors ........ 0
Number of failures ...... 1
############################################################
FAILURES
#----------------------------------------------------------#
Traceback (most recent call last):
  File "unit_testing.py", line 160, in testCodeCoverage
    ', '.join( self._missingTestCases )
AssertionError: Unit-testing policies require test-cases 
    for all classes and functions in the idic.serialization 
    module, but the following have not been defined: 
    (testHasSerializationDict, testIsJSONSerializable, 
    testUnsanitizedJSONWarning)
############################################################
Unit-test results
############################################################
Tests were successful ... False
Number of tests run ..... 2
Number of errors ........ 0
Number of failures ...... 1
############################################################
The output of PrintTestResults includes two copies of the results because the output may well be long enough that the data at the top may be lost off the scrollable area in command-line output. I've toyed with the idea of only generating the end output-information if a certain number of errors or failures has occurred, but that's never really been a high enough priority for me to follow through with it.

That first set of failure-results is nothing more than a list of test-case classes that need to be defined in order for the test-module to provide the required code-coverage.

Creating TestCase Classes

The next step is to generate test-case classes for each of the items noted in the initial failure: testHasSerializationDict, testIsJSONSerializable, and testUnsanitizedJSONWarning. For each of those, I start with the code in my TestCaseTemplate.py file. Removing some of the common helper-methods that use at least occastionally, and the optional set-up and tear-down, there's not a lot to that file:

@testModuleNameCodeCoverage.AddMethodTesting
@testModuleNameCodeCoverage.AddPropertyTesting
class testClassName( unittest.TestCase ):
    """Unit-tests the ClassName class."""

    #--------------------------------------#
    # Unit-tests for class constants,      #
    # if any                               #
    #--------------------------------------#

    #--------------------------------------#
    # Unit-tests the object constructor,   #
    # including any property-values set    #
    # during construction of an instance.  #
    #--------------------------------------#

    def test__init__(self):
        """Unit-tests the initialization of a ClassName instance."""
        self.fail( 'test__init__ is not yet implemented' )

    #--------------------------------------#
    # Unit-tests the object destructor, if #
    # one is provided.                     #
    #--------------------------------------#

#    def test__del__(self):
#        """Unit-tests the destruction of a ClassName instance."""
#        self.fail( 'test__del__ is not yet implemented' )

    #--------------------------------------#
    # Unit-tests of object properties      #
    #--------------------------------------#

#    def testPROPERTYNAME(self):
#        """Unit-tests the PROPERTYNAME property of a ClassName instance."""
#        self.fail( 'testPROPERTYNAME is not yet implemented' )

    #--------------------------------------#
    # Unit-tests of object methods         #
    #--------------------------------------#

#    def testMETHODNAME(self):
#        """Unit-tests the METHODNAME method of a ClassName instance."""
#        self.fail( 'testMETHODNAME is not yet implemented' )


LocalSuite.addTests( 
    unittest.TestLoader().loadTestsFromTestCase( 
        testClassName 
    )
)
Since I usually use template-files rather than snippets, I generally copy the test-case template, paste it into the test-file, then replace all the instances of ClassName with the name of the class that the test-case relates to. The AddMethodTesting and AddPropertyTesting decorators also need to be bound to the initial code-coverage test-case (testserializationCodeCoverage here), in order to avoid any potential requirements contamination across test-modules. By way of example, after adding a test-case to be pointed at HasSerializationDict, the test-module has a test-case the looks like this:
@testserializationCodeCoverage.AddMethodTesting
@testserializationCodeCoverage.AddPropertyTesting
class testHasSerializationDict( unittest.TestCase ):
    """Unit-tests the HasSerializationDict class."""

    #--------------------------------------#
    # Unit-tests for class constants,      #
    # if any                               #
    #--------------------------------------#

    #--------------------------------------#
    # Unit-tests the object constructor,   #
    # including any property-values set    #
    # during construction of an instance.  #
    #--------------------------------------#

    def test__init__(self):
        """Unit-tests the initialization of a HasSerializationDict instance."""
        self.fail( 'test__init__ is not yet implemented' )

    #--------------------------------------#
    # Unit-tests the object destructor, if #
    # one is provided.                     #
    #--------------------------------------#

#    def test__del__(self):
#        """Unit-tests the destruction of a HasSerializationDict instance."""
#        self.fail( 'test__del__ is not yet implemented' )

    #--------------------------------------#
    # Unit-tests of object properties      #
    #--------------------------------------#

#    def testPROPERTYNAME(self):
#        """Unit-tests the PROPERTYNAME property of a HasSerializationDict instance."""
#        self.fail( 'testPROPERTYNAME is not yet implemented' )

    #--------------------------------------#
    # Unit-tests of object methods         #
    #--------------------------------------#

#    def testMETHODNAME(self):
#        """Unit-tests the METHODNAME method of a HasSerializationDict instance."""
#        self.fail( 'testMETHODNAME is not yet implemented' )


LocalSuite.addTests( 
    unittest.TestLoader().loadTestsFromTestCase( 
        testHasSerializationDict 
    )
)
Running the test-module now yields different output (I've removed the end-of-test information for brevity):
############################################################
Unit-test results
############################################################
Tests were successful ... False
Number of tests run ..... 5
Number of errors ........ 0
Number of failures ...... 3
############################################################
FAILURES
#----------------------------------------------------------#
Traceback (most recent call last):
  File "unit_testing.py", line 160, in testCodeCoverage
    ', '.join( self._missingTestCases )
AssertionError: Unit-testing policies require test-cases 
    for all classes and functions in the idic.serialization 
    module, but the following have not been defined: 
    (testIsJSONSerializable, testUnsanitizedJSONWarning)
#----------------------------------------------------------#
Traceback (most recent call last):
  File "unit_testing.py", line 348, in testMethodCoverage
    target.__name__, missingMethods
AssertionError: Unit-testing policy requires test-methods 
    to be created for all public and protected methods, but 
    testHasSerializationDict is missing the following 
    test-methods: 
    ['testFromDict', 'testGetSerializationDict']
#----------------------------------------------------------#
Traceback (most recent call last):
  File "testserialization.py", line 122, in test__init__
    self.fail( 'test__init__ is not yet implemented' )
AssertionError: test__init__ is not yet implemented
############################################################

There are a couple of noteworthy items in this output:

  • The original list of test-cases required has gone down to two items: testIsJSONSerializable and testUnsanitizedJSONWarning, because the testHasSerializationDict test-case class exists now.
  • The testMethodCoverage that was attached by the AddMethodTesting decorator is working, and has identified that two test-methods need to be generated: testFromDict and testGetSerializationDict. Those are both abstract (or at least nominally-abstract) methods, but they live in the scope of the class, and can be usefully tested, I think, so the fact that tests are being required is a good thing in my opinion.
  • Finally, there is a forced failure for the __init__ method of HasSerializationDict.

After adding test-case classes for testIsJSONSerializable and testUnsanitizedJSONWarning, the failures change yet again:

############################################################
Unit-test results
############################################################
Tests were successful ... False
Number of tests run ..... 11
Number of errors ........ 0
Number of failures ...... 6
############################################################
FAILURES
#----------------------------------------------------------#
Traceback (most recent call last):
  File "unit_testing.py", line 348, in testMethodCoverage
    target.__name__, missingMethods
AssertionError: Unit-testing policy requires test-methods 
    to be created for all public and protected methods, 
    but testHasSerializationDict is missing the following 
    test-methods: 
    ['testFromDict', 'testGetSerializationDict']
#----------------------------------------------------------#
Traceback (most recent call last):
  File "testserialization.py", line 122, in test__init__
    self.fail( 'test__init__ is not yet implemented' )
AssertionError: test__init__ is not yet implemented
#----------------------------------------------------------#
Traceback (most recent call last):
  File "unit_testing.py", line 348, in testMethodCoverage
    target.__name__, missingMethods
AssertionError: Unit-testing policy requires test-methods 
    to be created for all public and protected methods, but 
    testIsJSONSerializable is missing the following 
    test-methods: 
    ['testFromJSON', 'testRegisterLoadable', 
    'testSanitizeDict', 'test_GetPythonNamespace', 
    'test_GetSanitizedJSON', 'testwrapjsondump', 
    'testwrapjsondumps', 'testwrapjsonload', 
    'testwrapjsonloads']
#----------------------------------------------------------#
Traceback (most recent call last):
  File "unit_testing.py", line 389, in testPropertyCoverage
    'methods: %s' % ( target.__name__, missingMethods )
AssertionError: Unit-testing policy requires test-methods 
    to be created for all public properties, but 
    testIsJSONSerializable is missing the following 
    test-methods: 
    ['testPythonNamespace', 'testSanitizedJSON']
#----------------------------------------------------------#
Traceback (most recent call last):
  File "testserialization.py", line 174, in test__init__
    self.fail( 'test__init__ is not yet implemented' )
AssertionError: test__init__ is not yet implemented
#----------------------------------------------------------#
Traceback (most recent call last):
  File "testserialization.py", line 226, in test__init__
    self.fail( 'test__init__ is not yet implemented' )
AssertionError: test__init__ is not yet implemented
############################################################

The set-up for the top-level test-module, for the entire idic project-namespace follows the same structure, and uses the same starting-point, but has some minor differences. With all of the empty sections stripped out, this is what it boils down to:

#!/usr/bin/env python
"""Defines unit-tests for the package at idic."""

#-----------------------------------#
# Standard-library imports.         #
#-----------------------------------#

import os
import sys
import unittest

#-----------------------------------#
# idic-library imports.             #
#-----------------------------------#
# - Local development path
sys.path.insert( 1, os.path.expanduser( 
    '~/IDreamInCode/idic/usr/local/lib/idic' ) )
# - Installed location
sys.path.insert( 1, '/usr/local/lib/idic' )

from idic.unit_testing import *

#-----------------------------------#
# Import the module being tested    #
#-----------------------------------#
LocalSuite = unittest.TestSuite()

#-----------------------------------#
# Import the module being tested    #
#-----------------------------------#
import idic

#-----------------------------------#
# Code-coverage test-case and       #
# decorator-methods                 #
#-----------------------------------#

class testidicCodeCoverage( moduleCoverageTest ):
    _testNamespace = 'idic'
    _testModule = idic

LocalSuite.addTests( 
    unittest.TestLoader().loadTestsFromTestCase( 
        testidicCodeCoverage
    )
)

#-----------------------------------#
# Test-cases in the module          #
#-----------------------------------#

#-----------------------------------#
# Child test-cases to run           #
#-----------------------------------#

import test_serialization
LocalSuite.addTests( test_serialization.LocalSuite._tests )

#-----------------------------------#
# Code to execute if file is called #
# or run directly.                  #
#-----------------------------------#
if __name__ == '__main__':
    import time
    results = unittest.TestResult()
    testStartTime = time.time()
    LocalSuite.run( results )
    results.runTime = time.time() - testStartTime
    PrintTestResults( results )
    if not results.errors and not results.failures:
        SaveTestReport( results, 'idic', 
            'idic-test-results.txt' )
The main differences are:
  • The namespace change (idic), allowing it to test the idic package-header file;
  • The lack of test-case classes, because the package-header file for the idic namespace is currently an empty file; and
  • The inclusion of the test_serialization module's test-cases in the LocalSuite test-suite.
Taken together, these allow the test_idic test-module, when executed, to test the idic and idic.serialization namespaces:
###########################################################
Unit-test results
###########################################################
Tests were successful ... True
Number of tests run ..... 23
 + Tests ran in ......... 0.20 seconds
Number of errors ........ 0
Number of failures ...... 0
###########################################################
One additional test runs, the testCodeCoverage provided by the testidicCodeCoverage test-case class. The test_serialization test-module can still be run individually:
###########################################################
Unit-test results
###########################################################
Tests were successful ... True
Number of tests run ..... 22
 + Tests ran in ......... 0.19 seconds
Number of errors ........ 0
Number of failures ...... 0
###########################################################
As long as a similar set-up is put in place for each child module in the idic namespace (importing whichever test_module and adding the LocalSuite._tests from it), the test_idic module can be used to test the entire> idic namespace. Sub-packages inside the idic namespace, if any are eventually built out, can also use the same kind of structure, and will also be included in the tests run by test_idic in those cases.

Creating the Required Test-methods

I had originally planned to go into considerable depth in this post, including a step-by-step walk-through of generating the actual test-methods for all of the classes in the serialization module, and all of the members of those classes. When I'd finished writing it all up, it was a lot longer than I wanted. Also, perhaps because it was about unit-testing, it was really dry stuff.

Long and dry together sounds like a recipe for boring, so I'm going to shelve the discussion of detailed test-method implementation for now. I did generate unit-tests for everything in serialization, though they aren't complete, and if you're curious about them, they can be found in the idic.zip [Snapshot] download at the end of the page.

After I've had some time to think on a better way to present that level of detail, the in-the-weeds unit-testing, I'll come back to those, and probably revisit the current serialization tests.

But I think that's enough on unit-testing for the time being. It gets me where I needed to be to have standard testing policies, and ways to enforce them on my own code. It's also a key component in what I think of as a repeatable build-process — even if that's not a full-on Continuous Integration set-up, it's still in my list of things to accomplish for what I'd consider the minimum viable/bare-bones repeatable build-process:

  • Run automated tests and stop if any tests fail;
  • Generate notifications if the tests fail;
  • Package the build(s) in some fashion so that it's ready to be deployed;
  • Generate notifications if a build fails for reasons other than test-failures; and
  • Deploy to an environment where the current build can be executed.
Since I'm to the point where I need to be able to generate snapshots, it feels to me like it's about time to start looking at that repeatable build process idea.