Thursday, March 30, 2017

Unit-testing and Code-coverage in Python [5]

I hadn't actually shown this aspect of the TestValues class yet, but it's an extension of the built-in Python list type:

@describe.InitClass()
class TestValues( list, object ):
    """
Represents a collection of standard test-values, and provides filtering of 
those values."""
    #-----------------------------------#
    # Class attributes (and instance-   #
    # attribute default values)         #
    #-----------------------------------#
As such, it's got a lot of functionality already available that originates with the underlying list class that it extends. The decision to extend TestValues from list was one that I went back and forth on for a while — On one hand, I didn't want to have to implement all of the functionality that I expected TestValues to need from scratch if basing it off of list. Chances are good that I'd have ended up using a list as an internal data-storage for TestValues values anyway, and just wrapping the TestValues class around it.

On the other hand, I wasn't really sure what functionality a list brought to the table that I didn't want. A list has 30-odd methods for various purposes, and I ended up having to take a substantial look at a fair number of them before deciding which ones were still valid and which ones weren't. Here's what that process looked like...

What does a list do already?

Before I could really determine what (if any) properties and methods of the base list type I wanted to override or otherwise tweak, I needed to know what it's got, and what all it's members do. Fortunately, between the documentation about emulating container types and some bits and pieces from the __doc__s of each member, that turned out to be a pretty easy task. The properties and methods that I evaluated are:

__add__
Called to implement the + operator, used when concatenating instances.
Will need to be examined in order to assure that concatenation yields an instance of TestValues rather than a generic list, but that should be the case by default as long as the object being added to is a TestValues instance.
__contains__
Called to perform membership-test operations. Shouldn't need to be altered.
__delitem__
Called to delete individual items or slices from the instance. Shouldn't need to be altered.
__delslice__
Called to delete a slice from the instance. Shouldn't need to be altered.
__eq__
Called to implement the == operator, testing for equality.
Will need to be examined to determine whether comparison will work as-is, and whether it needs to be altered for comparison of instances with equivalent non-instance sequence-objects.
__ge__
Called to implement the >= operator, testing for relative size. Will need to be examined to determine whether comparison will work as-is, and whether it needs to be altered for comparison of instances with equivalent non-instance sequence-objects.
__getitem__
Called to get individual items or slices from the instance. Shouldn't need to be altered.
__getslice__
Called to get a slice of the instance. Shouldn't need to be altered.
__gt__
Called to implement the > operator, testing for relative size. Will need to be examined to determine whether comparison will work as-is, and whether it needs to be altered for comparison of instances with equivalent non-instance sequence-objects.
__iadd__
Called to implement the += operator, used when concatenating instances.
Will need to be examined in order to assure that concatenation yields an instance of TestValues rather than a generic list, but that should be the case by default as long as the object being added to is a TestValues instance.
__imul__
Called to implement the *= operator. Since this normally yields a number of duplicates of the members of the instance in a list, and that's not something that seems useful (all test-values should ideally be distinct), I may want to override this to have it throw an error.
__iter__
Called to return an iterator for the instance. Shouldn't need to be altered.
__le__
Called to implement the <= operator, testing for relative size. Will need to be examined to determine whether comparison will work as-is, and whether it needs to be altered for comparison of instances with equivalent non-instance sequence-objects.
__len__
Called to implement the built-in len function. Shouldn't need to be altered.
__lt__
Called to implement the < operator, testing for relative size. Will need to be examined to determine whether comparison will work as-is, and whether it needs to be altered for comparison of instances with equivalent non-instance sequence-objects.
__mul__
Called to implement the * operator. Since this normally yields a number of duplicates of the members of the instance in a list, and that's not something that seems useful (all test-values should ideally be distinct), I may want to override this to have it throw an error.
__ne__
Called to implement the != and <> operators, testing for inequality.
Will need to be examined to determine whether comparison will work as-is, and whether it needs to be altered for comparison of instances with equivalent non-instance sequence-objects.
__reversed__
Called to return a reversed iterator for the instance. Shouldn't need to be altered.
__rmul__
Called to implement the * operator. Since this normally yields a number of duplicates of the members of the instance in a list, and that's not something that seems useful (all test-values should ideally be distinct), I may want to override this to have it throw an error.
__setitem__
Called to set individual items or slices in the instance. Shouldn't need to be altered.
__setslice__
Called to set a slice in the instance. Shouldn't need to be altered.
append
Called to append a member to an instance. Shouldn't need to be altered.
count
Called to count the number of occurrences of a value in the members of an instance. Shouldn't need to be altered.
extend
Called to extend the instance by appending elements from the supplied iterable. Shouldn't need to be altered.
index
Called to return the index-position of the first occurrence of the value in the members of the instance. Shouldn't need to be altered.
insert
Called to insert a value into the members of the instance at a given index-position. Shouldn't need to be altered.
pop
Called to remove and return a member-item from the instance, at an optional index-position. Shouldn't need to be altered.
remove
Called to remove the first occurrence of a member value from the members of the instance.
This will need to be altered, if only to allow an iterable of values to be passed, and TextValues.remove should remove all occurrences of the specified value(s) from the member collection.
reverse
Called to reverse the sequence of members in the instance. Shouldn't need to be altered.
sort
Called to sort the members of the instance. Shouldn't need to be altered.
Over half of these (18 of the 30 items noted) fell into the shouldn't need to be altered category, and only one item solidly fell into the will need to be altered category (remove). There were three categories of list members that needed alteration or examination at a minimum:
Comparison-operator-related (various "magic methods")
__eq__, __ge__, __gt__, __le__, __lt__ and __ne__
In these cases, all I felt I really needed to do was make sure that they behaved the same with all of the possible comparisons between TestValues and list instances.
Mutation-operator-related (also various "magic methods")
__add__ and __iadd__
These methods, relating to the + and += operators as applied to lists, really just needed verification that when a TestValues instance was being added to, that the result was still a TestValues instance. My expectation was that it would be the case, but I had to make sure. Though I don't expect much use of either during unit-testing, I can't rule them out either.
__imul__, __mul__ and __rmul__
These relate to the * and *= operators as they apply to list instances, though I'm not quite certain when __rmul__ gets called as opposed to __mul__.
Since a TestValues instance is really intended to provide a reasonably-distinct set of values, and these operations effectively duplicate members in the list they are applied to, I had to think through whether I wanted them to override their inherited implementations in order to, perhaps, raise an error of some sort, thus preventing their use.
As a side note: A truly distinct set of values isn't really possible in a TestValues' member-list because str and unicode values containing the same text will evaluate as equal.
Mutation methods
remove
As noted above, I want to alter TestValues.remove to facilitate the removal of multiple values in a single pass.

Amusingly enough, the first two groups are exactly the kinds of items that I'd be looking to unit-test if they cropped up in the implementation of other classes. Because of the way the AddMethodTesting and AddPropertyTesting decorators work, though, they would not be automatically detected as testable members unless they were actively overridden in the derived class. It would be feasible to simply write overriding methods for each of them that call the parent list methods, though, and the decorators would pick up on those as local members that required testing. That feels kind of... wasteful, maybe... so I don't know if I'll take that approach, but even without doing so, just knowing that they should be tested is enough to prompt writing those test-methods.

While testing those, at first I believed that I'd need to create an empty _UnitTestValuePolicy instance — one whose All value had no values — which turned out not to be possible because of the way the default values were being populated:

    def __init__( self, **values ):
        """
Instance initializer"""
        # ...

        # Set _defaults values from **values members, if they are provided
        # - bools
        bools = values.get( 'bools' )
        if bools:
            self._defaults[ 'bools' ] = bools

        # - falseish

        # ...
In order to allow defaults for the various value-categories to be defined as empty, I had to alter _UnitTestValuePolicy.__init__ to specifically check for None values, as opposed to empty lists (which is what I specified while trying to create that empty instance:
    def __init__( self, **values ):
        """
Instance initializer"""
        # - bools
        bools = values.get( 'bools' )
        if bools == None:
            self._defaults[ 'bools' ] = self.__class__._defaults[ 'bools' ]
        else:
            self._defaults[ 'bools' ] = bools

        # - falseish

        # ...
That allowed the creation of the empty instance I thought I needed:
emptySource = _UnitTestValuePolicy( bools=[], 
    falseish=[], floats=[], ints=[], longs=[], none=[], 
    objects=[], strings=[], trueish=[], unicodes=[] )

print emptySource.All
[]

Checking the Comparison-operator-related Members

For all of the comparison-operation checks, I basically just needed two lists and equivalent TestValues instances:

testValuesList1 = [ 1, 2, 3, 4 ]
testValuesInst1 = TestValues( emptySource, testValuesList1 )
testValuesList2 = [ 4, 3, 2, 1 ]
testValuesInst2 = TestValues( emptySource, testValuesList2 )
Once those were created, the basic checks were fairly simple: Perform the comparison against all of the relevant list and TestValues instance-combinations and make sure that what happened in comparing two lists also happened when comparing the list with its equivalent TestValues instance. Since each pair of checks should return the same result, it was an easy matter to just skim downthe list and look for mismatched result-pairs:

print 'Testing __eq__:'
print 'testValuesList1 == testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 == testValuesList1 )
print 'testValuesList1 == testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 == testValuesInst1 )
Testing __eq__:
testValuesList1 == testValuesList1 .... True
testValuesList1 == testValuesInst1 .... True
print 'Testing __ge__:'
print 'testValuesList1 >= testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 >= testValuesList1 )
print 'testValuesList1 >= testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 >= testValuesInst1 )
print 'testValuesList1 >= testValuesList2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 >= testValuesList2 )
print 'testValuesList1 >= testValuesInst2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 >= testValuesInst2 )
print 'testValuesList2 >= testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 >= testValuesList1 )
print 'testValuesList2 >= testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 >= testValuesInst1 )
print 'testValuesList2 >= testValuesList2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 >= testValuesList2 )
print 'testValuesList2 >= testValuesInst2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 >= testValuesInst2 )
Testing __ge__:
testValuesList1 >= testValuesList1 .... True
testValuesList1 >= testValuesInst1 .... True
testValuesList1 >= testValuesList2 .... False
testValuesList1 >= testValuesInst2 .... False
testValuesList2 >= testValuesList1 .... True
testValuesList2 >= testValuesInst1 .... True
testValuesList2 >= testValuesList2 .... True
testValuesList2 >= testValuesInst2 .... True
print 'Testing __gt__:'
print 'testValuesList1 > testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 > testValuesList1 )
print 'testValuesList1 > testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 > testValuesInst1 )
print 'testValuesList1 > testValuesList2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 > testValuesList2 )
print 'testValuesList1 > testValuesInst2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 > testValuesInst2 )
print 'testValuesList2 > testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 > testValuesList1 )
print 'testValuesList2 > testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 > testValuesInst1 )
print 'testValuesList2 > testValuesList2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 > testValuesList2 )
print 'testValuesList2 > testValuesInst2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 > testValuesInst2 )
Testing __gt__:
testValuesList1 > testValuesList1 ..... False
testValuesList1 > testValuesInst1 ..... False
testValuesList1 > testValuesList2 ..... False
testValuesList1 > testValuesInst2 ..... False
testValuesList2 > testValuesList1 ..... True
testValuesList2 > testValuesInst1 ..... True
testValuesList2 > testValuesList2 ..... False
testValuesList2 > testValuesInst2 ..... False
print 'Testing __le__:'
print 'testValuesList1 <= testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 <= testValuesList1 )
print 'testValuesList1 <= testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 <= testValuesInst1 )
print 'testValuesList1 <= testValuesList2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 <= testValuesList2 )
print 'testValuesList1 <= testValuesInst2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 <= testValuesInst2 )
print 'testValuesList2 <= testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 <= testValuesList1 )
print 'testValuesList2 <= testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 <= testValuesInst1 )
print 'testValuesList2 <= testValuesList2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 <= testValuesList2 )
print 'testValuesList2 <= testValuesInst2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 <= testValuesInst2 )
Testing __le__:
testValuesList1 <= testValuesList1 .... True
testValuesList1 <= testValuesInst1 .... True
testValuesList1 <= testValuesList2 .... True
testValuesList1 <= testValuesInst2 .... True
testValuesList2 <= testValuesList1 .... False
testValuesList2 <= testValuesInst1 .... False
testValuesList2 <= testValuesList2 .... True
testValuesList2 <= testValuesInst2 .... True
print 'Testing __lt__:'
print 'testValuesList1 < testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 < testValuesList1 )
print 'testValuesList1 < testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 < testValuesInst1 )
print 'testValuesList1 < testValuesList2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 < testValuesList2 )
print 'testValuesList1 < testValuesInst2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 < testValuesInst2 )
print 'testValuesList2 < testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 < testValuesList1 )
print 'testValuesList2 < testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 < testValuesInst1 )
print 'testValuesList2 < testValuesList2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 < testValuesList2 )
print 'testValuesList2 < testValuesInst2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList2 < testValuesInst2 )
Testing __lt__:
testValuesList1 < testValuesList1 ..... False
testValuesList1 < testValuesInst1 ..... False
testValuesList1 < testValuesList2 ..... True
testValuesList1 < testValuesInst2 ..... True
testValuesList2 < testValuesList1 ..... False
testValuesList2 < testValuesInst1 ..... False
testValuesList2 < testValuesList2 ..... False
testValuesList2 < testValuesInst2 ..... False
print 'Testing __ne__:'
print 'testValuesList1 != testValuesList1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 != testValuesList1 )
print 'testValuesList1 != testValuesInst1 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 != testValuesInst1 )
print 'testValuesList1 != testValuesList2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 != testValuesList2 )
print 'testValuesList1 != testValuesInst2 '.ljust( 39, '.' ) + ' %s' % str(
    testValuesList1 != testValuesInst2 )
Testing __ne__:
testValuesList1 != testValuesList1 .... False
testValuesList1 != testValuesInst1 .... False
testValuesList1 != testValuesList2 .... True
testValuesList1 != testValuesInst2 .... True

All of the comparison-operator-related magic methods checked out just fine, so there's no need for me to override them.

The irony of this manual, brute-force testing in a series of posts about unit-testing does not escape me. At some point, I'll try to work out a good way to actually unit-test these, perhaps, but that would distract from the goal of today's post, so I'll leave that for later...

Checking the Mutation-operator-related Members

The mutation operators actually make changes to the object they are being applied to: __add__ and __iadd__ append members to a list, so they should also add members, in the exact same fashion, to a TestValues instance.

print 'Testing __add__:'
List1 = [ 1, 3 ]
Inst1 = TestValues( emptySource, List1 )
List2 = [ 2, 4 ]
Inst2 = TestValues( emptySource, List2 )
print 'type( List1 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( List1 ).__name__ )
print 'type( Inst1 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( Inst1 ).__name__ )
print 'List1 + List2 '.ljust( 39, '.' ) + ' %s' % str(
    List1 + List2 )
print 'type( List1 + List2 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( List1 + List2 ).__name__ )
print 'Inst1 + Inst2 '.ljust( 39, '.' ) + ' %s' % str(
    Inst1 + Inst2 )
print 'type( Inst1 + Inst2 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( Inst1 + Inst2 ).__name__ )
print 'Inst1 + List2 '.ljust( 39, '.' ) + ' %s' % str(
    Inst1 + List2 )
print 'type( Inst1 + List2 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( Inst1 + List2 ).__name__ )
Testing __add__:
type( List1 ) ......................... list
type( Inst1 ) ......................... TestValues
List1 + List2 ......................... [1, 3, 2, 4]
type( List1 + List2 ) ................. list
Inst1 + Inst2 ......................... [1, 3, 2, 4]
type( Inst1 + Inst2 ) ................. list
Inst1 + List2 ......................... [1, 3, 2, 4]
type( Inst1 + List2 ) ................. list

So, the short story behind the __add__ method is that it apparently always returns a list-instance, whether any of the instances being added are not lists themselves or not. That means that I will need to write a TestValues.__add__ method, in order to return a TestValues instance.

print 'Testing __iadd__:'
List1 = [ 1, 3 ]
Inst1 = TestValues( emptySource, List1 )
List2 = [ 2, 4 ]
Inst2 = TestValues( emptySource, List2 )
print 'type( List1 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( List1 ).__name__ )
print 'type( Inst1 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( Inst1 ).__name__ )
List1 += List2
print 'List1 += List2 '.ljust( 39, '.' ) + ' %s' % str(
    List1 )
print 'type( List1 += List2 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( List1 ).__name__ )
Inst1 += Inst2
print 'Inst1 += Inst2 '.ljust( 39, '.' ) + ' %s' % str(
    Inst1 )
print 'type( Inst1 += Inst2 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( Inst1 ).__name__ )
List2 += Inst2
print 'List2 += Inst2 '.ljust( 39, '.' ) + ' %s' % str(
    List2 )
print 'type( List2 += Inst2 ) '.ljust( 39, '.' ) + ' %s' % str(
    type( List2 ).__name__ )
Testing __iadd__:
type( List1 ) ......................... list
type( Inst1 ) ......................... TestValues
List1 += List2 ........................ [1, 3, 2, 4]
type( List1 += List2 ) ................ list
Inst1 += Inst2 ........................ [1, 3, 2, 4]
type( Inst1 += Inst2 ) ................ TestValues
List2 += Inst2 ........................ [2, 4, 2, 4]
type( List2 += Inst2 ) ................ list

I was expecting __iadd__ to return a TestValues instance if the left item in the applicable code were itself an instance of TestValues. I was not expecting a TestValues instance if the left value was not a TestValues instance. In both cases, my expectations panned out.

That does raise a potential concern, though — In any case where a TestValues instance is part of a += operation, if the result needs to be a TestValues then the TestValues instance must be the left item in the assignment. I don't expect this will be much of a concern, but it does mean that I may want to give some thought to creating some sort of conversion-method for lists in order to cast them as TestValues. It's possible to do so by simply creating a new TestValues and passing the list, but that also requires some awareness of which _UnitTestValuePolicy is applicable, or creating a new one just for the new instance. In a case like that, it might be easier to create an instance-method since the isnstance is already aware of its _UnitTestValuePolicy.

By the time I'd finished checking the first two mutation-operator items, I'd given some thought to the remaining ones (__imul__, __mul__ and __rmul__). I couldn't think of a case where I'd actually want to duplicate the value-list behind a TestValues instance. Ever. So those methods will also be overridden — I planned on raising some Exception, but hadn't decided which one.

Since I had already planned to override remove, there was no checking needed.

The Final TestValues Implementation

With only four of the inherited magic-method overrides to be written, and one override of another non-magic method, the additions are pretty short:

@describe.AttachDocumentation()
@describe.argument( 'values', 
    'the values to add to the members', 
    object )
def __add__( self, values ):
    """
Override of the "+" operator callback for lists. Returns an instance of the 
class, populated with the members of the original instance, and with the 
provided value appended to it."""
    selfValues = list( self )
    result = self.__class__( self.ValueSource, 
        selfValues + values )
    return result

@describe.AttachDocumentation()
@describe.raises( RuntimeError, 
    'if the *= operator is executed against an instance of the class'
)
def __imul__( self, value ):
    """
Override of the "*=" operator callback for lists. Prevents the use of the 
operator on instances of the class."""
    raise RuntimeError( '%s does not support the "*=" operator' % ( 
        self.__class__.__name__ ) )

@describe.AttachDocumentation()
@describe.raises( RuntimeError, 
    'if the * operator is executed against an instance of the class'
)
def __mul__( self, value ):
    """
Override of the "*=" operator callback for lists. Prevents the use of the 
operator on instances of the class."""
    raise RuntimeError( '%s does not support the "*=" operator' % ( 
        self.__class__.__name__ ) )

@describe.AttachDocumentation()
@describe.raises( RuntimeError, 
    'if the * operator is executed against an instance of the class'
)
def __rmul__( self, value ):
    """
Override of the "*=" operator callback for lists. Prevents the use of the 
operator on instances of the class."""
    raise RuntimeError( '%s does not support the "*=" operator' % ( 
        self.__class__.__name__ ) )

@describe.AttachDocumentation()
@describe.argument( 'values', 
    'the value (a single value that is not a list or tuple) or values '
    '(a list or tuple of single values that are not lists or tuples) to '
    'remove from the members of the instance', 
    list, tuple, object )
def remove( self, values ):
    """
Removes all instances of the value(s) supplied from the members of the 
instance."""
    if isinstance( values, ( list, tuple ) ):
        for value in values:
            while self.count( value ) != 0:
                list.remove( self, value )
    else:
        while self.count( values ) != 0:
            list.remove( self, values )
The magic-method overrides are almost brutally simple:
  • The implementation of __add__ renders the instance being added to down to a list, then returns an instance of the class populated with the results of + applied to that list plus the values supplied. Simple. It also keeps to the implementation pattern of the list type, by retruning a new instance.
  • The __imul__, __mul__ and __rmul__ overrides all raise a RuntimeError (for lack of a better Exception type to use) if they are called, and the error-messaging explains that the instance doesn't support the operation attempted.
The override of remove does break, a bit, from the behavior of list.removelist.remove will raise errors if the item specified for removal isn't present in the list to begin with. After some consideration, and weighing my desire to be able to remove all instances of a given value from a TestValues instance, I discarded that behavior, so a TestValues.remove shouldn't ever fail because a value being removed doesn't exist. There may be other implications of this that I haven't thought of yet, but for now, I'm happy with this implementation: It allows the removal of all values specified, and accepts a list or tuple of values to be removed, as well as single values.

That, then, wraps up the unit_testing module as far as implementation is concerned. The next post, and the last one dealing with unit-testing for the time being, will focus on actually unit-testing the serialization module — finally.

No comments:

Post a Comment