16.7. 整合所有内容

你现在已经学习了足够多的知识,可以解析本章代码示例的前七行:读取目录并导入其中的选定模块。

示例 16.16. regressionTest 函数


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))          

让我们逐行、交互式地查看它。假设当前目录是 c:\diveintopython\py,其中包含本书附带的示例,包括本章的脚本。正如你在 16.2 节 “查找路径” 中看到的,脚本目录将位于 path 变量中,所以让我们从硬编码开始,然后继续。

示例 16.17. 步骤 1:获取所有文件

>>> 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 是脚本目录中所有文件和目录的列表。(如果你已经运行了一些示例,你可能还会在其中看到一些 .pyc 文件。)

示例 16.18. 步骤 2:筛选以找到你关心的文件

>>> 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 此正则表达式将匹配任何以 test.py 结尾的字符串。请注意,你需要转义句点,因为正则表达式中的句点通常表示“匹配任何单个字符”,但你实际上想要匹配一个文字句点。
2 编译后的正则表达式就像一个函数,因此你可以使用它来过滤大型文件和目录列表,以找到与正则表达式匹配的那些。
3 你只剩下单元测试脚本列表,因为它们是唯一命名为 SOMETHINGtest.py 的脚本。

示例 16.19. 步骤 3:将文件名映射到模块名

>>> 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 正如你在 4.7 节 “使用 lambda 函数” 中看到的,lambda 是一种快速创建内联、单行函数的方法。这个函数接受一个带扩展名的文件名,并使用你在 示例 6.17 “拆分路径名” 中看到的标准库函数 os.path.splitext 返回文件名部分。
2 filenameToModuleName 是一个函数。与使用 def 语句定义的常规函数相比,lambda 函数没有什么神奇之处。你可以像调用任何其他函数一样调用 filenameToModuleName 函数,它会完全按照你的意愿执行操作:从其参数中删除文件扩展名。
3 现在,你可以使用 map 将此函数应用于单元测试文件列表中的每个文件。
4 结果正是你想要的:一个模块列表,以字符串形式表示。

示例 16.20. 步骤 4:将模块名映射到模块

>>> 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 正如你在 16.6 节 “动态导入模块” 中看到的,你可以使用 map__import__ 的组合将模块名列表(作为字符串)映射到实际模块(你可以像任何其他模块一样调用或访问它们)。
2 modules 现在是一个模块列表,可以像任何其他模块一样完全访问。
3 列表中的最后一个模块 romantest 模块,就像你说了 import romantest 一样。

示例 16.21. 步骤 5:将模块加载到测试套件中

>>> 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 这些是真正的模块对象。你不仅可以像访问任何其他模块一样访问它们、实例化类和调用函数,还可以内省模块以找出它最初有哪些类和函数。这就是 loadTestsFromModule 方法的作用:它内省每个模块并为每个模块返回一个 unittest.TestSuite 对象。每个 TestSuite 对象实际上都包含一个 TestSuite 对象列表,每个对象对应于模块中的一个 TestCase 类,并且每个 TestSuite 对象都包含一个测试列表,每个测试对应于模块中的一个测试方法。
2 最后,你将 TestSuite 对象列表包装到一个大的测试套件中。unittest 模块可以毫无问题地遍历这个嵌套测试套件树;最终,它会找到一个单独的测试方法并执行它,验证它是否通过或失败,然后继续下一个。

这个内省过程是 unittest 模块通常为我们做的事情。还记得我们各个测试模块用来启动整个过程的神奇的 unittest.main() 函数吗?unittest.main() 实际上创建了一个 unittest.TestProgram 的实例,该实例又创建了一个 unittest.defaultTestLoader 的实例,并将其加载到调用它的模块中。(如果 你没有给它一个引用,它如何获得对调用它的模块的引用?通过使用同样神奇的 __import__('__main__') 命令,它动态地导入当前运行的模块。我可以写一本关于 unittest 模块中使用的所有技巧和技术的书,但我永远也写不完。)

示例 16.22. 步骤 6:告诉 unittest 使用你的测试套件


if __name__ == "__main__":                   
    unittest.main(defaultTest="regressionTest") 1
1 你没有让 unittest 模块为我们完成所有神奇的操作,而是自己完成了大部分操作。你创建了一个函数 (regressionTest),它自己导入模块,自己调用 unittest.defaultTestLoader,并将所有内容包装在一个测试套件中。现在你需要做的就是告诉 unittest,不要以通常的方式查找测试和构建测试套件,而应该调用 regressionTest 函数,该函数返回一个随时可用的 TestSuite