14.7. Putting it all together

We've learned enough now to deconstruct the first seven lines of this chapter's code sample: reading a directory and importing selected modules within it.

Example 14.16. The regressionTest function


def regressionTest():
    path = os.path.abspath(os.path.dirname(sys.argv[0]))   
    files = os.listdir(path)                               
    test = re.compile("test\.py$", re.IGNORECASE)          
    files = filter(test.search, files)                     
    filenameToModuleName = lambda f: os.path.splitext(f)[0]
    moduleNames = map(filenameToModuleName, files)         
    modules = map(__import__, moduleNames)                 
load = unittest.defaultTestLoader.loadTestsFromModule  
return unittest.TestSuite(map(load, modules))          

Let's look at it line by line, interactively. Assume that the current directory is c:\diveintopython\py, which contains the examples that come with this book, including this chapter's script. As we saw in Finding the path, the script directory will end up in the path variable, so let's start hard-code that and go from there.

Example 14.17. Step 1: Get all the files

>>> import sys, os, re, unittest
>>> path = r'c:\diveintopython\py'
>>> files = os.listdir(path)                               
>>> files 1
['BaseHTMLProcessor.py', 'LICENSE.txt', 'apihelper.py', 'apihelpertest.py',
'argecho.py', 'autosize.py', 'builddialectexamples.py', 'dialect.py',
'fileinfo.py', 'fullpath.py', 'kgptest.py', 'makerealworddoc.py',
'odbchelper.py', 'odbchelpertest.py', 'parsephone.py', 'piglatin.py',
'plural.py', 'pluraltest.py', 'pyfontify.py', 'regression.py', 'roman.py', 'romantest.py',
'uncurly.py', 'unicode2koi8r.py', 'urllister.py', 'kgp', 'plural', 'roman',
'colorize.py']
1 files is a list of all the files and directories in the script's directory. (If you've been running some of the examples already, you may also see some .pyc files in there as well.)

Example 14.18. Step 2: Filter to find the files we care about

>>> test = re.compile("test\.py$", re.IGNORECASE)           1
>>> files = filter(test.search, files)                      2
>>> files                                                   3
['apihelpertest.py', 'kgptest.py', 'odbchelpertest.py', 'pluraltest.py', 'romantest.py']
1 This regular expression will match any string that ends with test.py. Note that we need to escape the period, since a period in a regular expression usually means “match any single character”, but we actually want to match a literal period instead.
2 The compiled regular expression acts like a function, so we can use it to filter our large list of files and directories, to find the ones that match the regular expression.
3 And we're left with the list of unit testing scripts, because they were the only ones named SOMETHINGtest.py.

Example 14.19. Step 3: Map filenames to module names

>>> filenameToModuleName = lambda f: os.path.splitext(f)[0] 1
>>> filenameToModuleName('romantest.py')                    2
'romantest'
>>> filenameToModuleName('odchelpertest.py')
'odbchelpertest'
>>> moduleNames = map(filenameToModuleName, files)          3
>>> moduleNames                                             4
['apihelpertest', 'kgptest', 'odbchelpertest', 'pluraltest', 'romantest']
1 As we saw in Using lambda functions, lambda is a quick-and-dirty way of creating an inline, one-line function. This one takes a filename with an extension and returns just the filename part, using the standard library function os.path.splitext that we saw in Example 6.17, “Splitting pathnames”.
2 filenameToModuleName is a function. There's nothing magic about lambda functions as opposed to regular functions that we define with a def statement. We can call the filenameToModuleName function like any other, and it does just what we wanted it to do: strips the file extension off of its argument.
3 Now we can apply this function to each file in our list of unit test files, using map.
4 And the result is just what we wanted: a list of modules, as strings.

Example 14.20. Step 4: Mapping module names to modules

>>> modules = map(__import__, moduleNames)                  1
>>> modules                                                 2
[<module 'apihelpertest' from 'apihelpertest.py'>,
<module 'kgptest' from 'kgptest.py'>,
<module 'odbchelpertest' from 'odbchelpertest.py'>,
<module 'pluraltest' from 'pluraltest.py'>,
<module 'romantest' from 'romantest.py'>]
>>> modules[-1]                                             3
<module 'romantest' from 'romantest.py'>
1 As we saw in Dynamically importing modules, we can use a combination of map and __import__ to map a list of module names (as strings) into actual modules (which we can call or access like any other module).
2 modules is now a list of modules, fully accessible like any other module.
3 The last module in the list is the romantest module, just as if we had said import romantest.

Example 14.21. Step 5: Loading the modules into a test suite

>>> load = unittest.defaultTestLoader.loadTestsFromModule  
>>> map(load, modules)                     1
[<unittest.TestSuite tests=[
  <unittest.TestSuite tests=[<apihelpertest.BadInput testMethod=testNoObject>]>,
  <unittest.TestSuite tests=[<apihelpertest.KnownValues testMethod=testApiHelper>]>,
  <unittest.TestSuite tests=[
    <apihelpertest.ParamChecks testMethod=testCollapse>, 
    <apihelpertest.ParamChecks testMethod=testSpacing>]>, 
    ...
  ]
]
>>> unittest.TestSuite(map(load, modules)) 2
1 These are real module objects. Not only can we access them like any other module, instantiate classes and call functions, we can also introspect into the module to figure out which classes and functions it has in the first place. That's what the loadTestsFromModule method does: it introspects into each module and returns a unittest.TestSuite object for each module. Each TestSuite object actually contains a list of TestSuite objects, one for each TestCase class in our module, and each of those TestSuite objects contains a list of tests, one for each test method in our module.
2 Finally, we wrap the list of TestSuite objects into one big test suite. The unittest module has no problem traversing this tree of nested test suites within test suites; eventually it gets down to an individual test method and executes it, verifies that it passes or fails, and moves on to the next one.

This introspection process is what the unittest module usually does for us. Remember that magic-looking unittest.main() function that our individual test modules called to kick the whole thing off? unittest.main() actually creates an instance of unittest.TestProgram, which in turn creates an instance of a unittest.defaultTestLoader and loads it up with the module that called it. (How does it get a reference to the module that called it if we don't give it one? By using the equally-magic __import__('__main__') command, which dynamically imports the currently-running module. I could write a book on all the tricks and techniques used in the unittest module, but then I'd never finish this one.)

Example 14.22. Step 6: Telling unittest to use our test suite


if __name__ == "__main__":                   
    unittest.main(defaultTest="regressionTest") 1
1 Instead of letting the unittest module do all its magic for us, we've done most of it ourselves. We've created a function (regressionTest) that imports the modules ourselves, calls unittest.defaultTestLoader ourselves, and wraps it all up in a test suite. Now all we have to do is tell unittest that, instead of looking for tests and building a test suite in the usual way, it should just call our regressionTest function, which returns a ready-to-use TestSuite.