Identifying missing test case classes

The balance of the testing goals noted earlier all require the ability to examine the code being tested in order to identify module members, and members of those members, that need to be tested. This might sound daunting, but Python provides a module dedicated to that purpose: inspect. It provides a very robust collection of functions that can be used to examine Python code at runtime, which can be leveraged to generate collections of member names that can, in turn, be used to determine whether the high-level test coverage meets the standard we're establishing.

For the purposes of illustration, the preceding classes that we need to test will be saved in a module called me.py, which makes them importable, and each step demonstrating the process for finding the needed information about the me module will be collected in inspect_me.pyas this shown here. The corresponding test cases will live in test_me.py, which will start as a near-empty file—no test case classes will be defined there at first.

The first step is identifying the target members of me that we're going to require test case classes for. As things stand right now, all we need is a list of classes in the target module, which can be retrieved as follows:

#!/usr/bin/env python

import inspect

import me as target_module

target_classes = set([
    member[0] for member in 
    inspect.getmembers(target_module, inspect.isclass)
])
# target_classes = {
#   'Child', 'ChildOverride', 'Parent', 'Showable'
# } at this point

Step by step, what's happening is this:

  1. The inspect module is being imported.

  2. The me module is being imported, using target_module as an override to its default module-name—we'll want to be able to keep imported module names predictable and relatively constant to make things easier to reuse down the line, and that starts here.

  3. The getmembers function of inspect is called against the target_module, using isclass as a filtering predicate. This returns a list of tuples that look like ('ClassName', <class object>). Those results are run through a list comprehension to extract only the class names, and that list is handed off to a Python set to yield a formal set of class names that were discovered.

Python's set type is a very useful basic data type it provides an iterable collection of values that are distinct (never repeated in the set), and that can be merged with other sets (with union), have its members removed from other sets (with difference), and a host of other operations that would be expected from standard set theory.

With those names available, creating a set of expected test case class names is simple:

expected_cases = set([
    'test%s' % class_name 
    for class_name in target_classes
    ]
)
# expected_cases = {
#   'testChild', 'testShowable', 'testChildOverride', 
#   'testParent'
# } at this point

This is just another list comprehension that builds a set of class names that start with test from the target class name set. A similar approach to the one that gathered the class names in the target module can be used to find the test case classes that exist in the test_me.py module:

import unittest

import test_me as test_module

test_cases = set([
    member[0] for member in 
    inspect.getmembers(test_module, inspect.isclass)
    if issubclass(member[1], unittest.TestCase)
])
# test_cases, before any TestCase classes have been defined, 
# is an empty set

Apart from the issubclass check of each member found, which will limit the members of the set to names of classes that are derived from unittest.TestCase, this is identical to the process that built the initial target_classes set. Now that we have sets that collect what's expected and what's actually defined, determining what test case classes need to be created is a simple matter of removing the defined test case names from the set of expected ones:

missing_tests = expected_cases.difference(test_cases)
# missing_tests = {
#   'testShowable', 'testChild', 'testParent', 
#   'testChildOverride'
# }

If missing_tests is not empty, then its collection of names represents the test case class names that need to be created in order to meet the first part of the "all members will be tested" policy. A simple print of the results at this point will suffice for now:

if missing_tests:
    print(
        'Test-policies require test-case classes to be '
        'created for each class in the code-base. The '
        'following have not been created:
 * %s' % 
        '
 * '.join(missing_tests)
    )

Having identified the missing test case class items that need to be created, they can be added to test_me.py:

#!/usr/bin/env python

import unittest

class testChild(unittest.TestCase):
    pass

class testChildOverride(unittest.TestCase):
    pass

class testParent(unittest.TestCase):
    pass

class testShowable(unittest.TestCase):
    pass

if __name__ == '__main__':
    unittest.main()

Once they have been added (and once subclasses are derived from unittest.TestCase, because of the check performed earlier in identifying actual test case classes), there are no missing test cases that need to be addressed.

A similar approach could be taken for identifying module-level functions that should arguably also be tested—they are also public members of a module, after all, and that's what the policy is concerned with, public members of modules. The actual implementation of tests against functions, or any other callable element, would follow the structures and processes that will be established later for class methods.

Really, the only public members that may not be easily identified with this sort of process are unmanaged attributes—module constants or variables that are created at the module level. While those could still be tested, and arguably should be, the fact that they are unmanaged, and can be changed at runtime without any checks to assure that they aren't going to break things somewhere down the line, might well make any formal testing policy around them little more than a waste of time. That said, there's no harm in testing them, if only to assure that changes to them, intentional or accidental, don't pass unnoticed and raise issues and bugs later on.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.160.43