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, namedtest_{module being tested}.py
, in a project directory namedtest_{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
andAddPropertyTesting
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;
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 beidic.serialization
); and - Replacing every instance of
ModuleName
with the module name of the module being tested (serialization
in this case).
#!/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 ClassNamewith 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
andtestUnsanitizedJSONWarning
, because thetestHasSerializationDict
test-case class exists now. - The
testMethodCoverage
that was attached by theAddMethodTesting
decorator is working, and has identified that two test-methods need to be generated:testFromDict
andtestGetSerializationDict
. 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 ofHasSerializationDict
.
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 theidic
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 theLocalSuite
test-suite.
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.
No comments:
Post a Comment