目录
unittest框架中的重要组件
unittest包的位置就是python源文件中,包中结构如下:
在分析源码之前我们需要知道unittest的基本构成,这样我们可以围绕这些组件来了解它的实现原理。
unittest框架中主要有以下这些组件:
- case : 测试用例,测试的基本单元
- suite : 测试套件,可以包含多个用例和套件
- result : 测试结果,用来存放测试的结果
- loader : 加载器,用于从各种环境中加载测试
- runner : 执行器,用于执行测式
unittest.main()方法
一、启动测试
main()是启动测试的函数,我们调用该方法来开始测试。使用unittest模块来启动测试有两种方式:
1. 在命令行中输入命令
我们使用命令行方式启动测试时,需要使用python -m unittest [被测模块名]的格式来运行。当使用python -m命令时,意味着将unittest包以脚本的形式运行,会执行unittest包中的__main__.py文件,它的代码如下:
import sys
# 当使用python -m来运行包时,sys.argv[0]是该包中的__main__.py文件的绝对路径
if sys.argv[0].endswith("__main__.py"):
import os.path
executable = os.path.basename(sys.executable)
sys.argv[0] = executable + " -m unittest" # 将argv[0]修改为python -m unittest
del os
__unittest = True
from .main import main, TestProgram
main(module=None)
代码中将系统参数的第一个元素sys.argv[0]修改为"python(具体名称由版本而定) -m unittest",默认为__main__.py的绝对路径。sys.argv[0]是将会打印在帮助信息中的内容,例如,我们在终端中输入python -m unittest --help,将会打印如下内容:
图中红框标示的内容就是sys.argv[0]的值。
2. 在代码中使用unittest.main()方法
有些时候我们会在测试脚本中直接使用如下代码:
if __name__ == '__main__':
a = unittest.main(argv=['test1.py', 'TestStringMethods.test_split'])
当代码中包含上述代码时,我们可以直接执行代码来启动测试,直接在解释器中运行或在命令行中输入python [模块名]。这里需要注意的是,如果代码中main的参数argv未给定,将会以sys.argv作为argv来执行,这意味我们使用终端时,可以添加各中-v、-f等参数来修改测试的设置。如果如上示代码中给定了参数,那么在终端中以模块方式来执行时的输入的参数将会被忽略。给定argv参数时,他的第一个元素应该是该程序的名称,其作用与上一节中sys.argv[0]的作用相同,用以完善帮助信息。
二、main/TestProgram源码解析
如下所示代码,从最后一行main = TestProgram可以看出,main即TestProgram类。下述代码展示了该类的结构,笔者根据自己的理解对TestProgram类中的各方法进行了描述。
class TestProgram(object):
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.
"""
# defaults for testing
module=None
verbosity = 1
failfast = catchbreak = buffer = progName = warnings = None
_discovery_parser = None
def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None, warnings=None, *, tb_locals=False):
"""
定义各参数,解析命令,创建测试test,并执行测试。
"""
...
self.parseArgs(argv) #解析命令,并创建测试test
self.runTests() #创建执行器runner,并使用它来执行测试
def usageExit(self, msg=None):
"""
打印帮助信息并退出。
"""
def _print_help(self, *args, **kwargs):
"""
打印帮助信息。
"""
...
def parseArgs(self, argv):
"""
调用self._initArgParsers初始化参数解析功能;
解析命令,规定了当discover后面没有其他参数时仍然可以运行;
最后调用self.createTests()来创建测试self.test。
"""
def createTests(self):
"""
使用loader加载器创建测试self.test。
"""
def _initArgParsers(self):
"""
初始化参数解析套件;
parent_parser : 调用self._getParentArgParser创建参数解析父类;
self._main_parser : 调用self._getMainArgParser继承父类,创建常规参数解析类;
self._discovery_parser : 调用self._getDiscoveryArgParser继承父类,创建discover模式的参数解析类。
"""
def _getParentArgParser(self):
"""
创建参数解析的父类,添加了-v,-q,--locals,-f,-c,-b参数;
返回argparse.ArgumentParser实例。
"""
...
def _getMainArgParser(self, parent):
"""
继承父类,创建常规参数解析类,添加了[tests]参数;
返回argparse.ArgumentParser实例。
"""
...
def _getDiscoveryArgParser(self, parent):
"""
继承父类,创建discover参数解析类;
规定了参数名后需要空格后加上discover;
添加了discover中使用的-s,-p,-t参数;
返回argparse.ArgumentParser实例。
"""
...
def _do_discovery(self, argv, Loader=None):
"""
调用self._discovery_parser解析命令行参数,并使用loader中的discover方法来创建测试self.test
"""
...
def runTests(self):
"""
该方法是执行测试的主要方法;
构建执行器self.testRunner,并调用它的run方法来执行测试,返回result对象并存储在self.result中。
"""
main = TestProgram
关于TestProgram类的__init__方法的各参数的含意及作用,读者可以查看文档或参考笔者手译的unittest模块官方文档手译。__init__方法中的最后两行执行了两个函数:self.parseArgs(argv)与self.runTest(),它们也是该类中最为重要的两个方法。前者解析argv并使用loader创建test,后者则创建runner执行器并调用其run方法来运行test,接下来我们会着重介绍它们两个。
1. 命令解析函数parseArgs
parseArgs方法是对给定的命令进行解析,这个命令可以是调用unittest.main(argv=[])时的argv参数,也可以是终端命令行中输入的命令。在解读parseArgs方法之前,我们先了解一下self._initArgParsers()方法,它也是parseArgs方法中执行的第一行代码,它的相关代码如下:
def _initArgParsers(self):
parent_parser = self._getParentArgParser() # 创建父parser
self._main_parser = self._getMainArgParser(parent_parser) # 创建main parser
self._discovery_parser = self._getDiscoveryArgParser(parent_parser) # 创建discover parser
def _getParentArgParser(self):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-v', '--verbose', dest='verbosity',
action='store_const', const=2,
help='Verbose output')
# 创建可选的常量参数-v,--verbose参数,解析到有该参数时,会执行verbosity=2
parser.add_argument('-q', '--quiet', dest='verbosity',
action='store_const', const=0,
help='Quiet output')
# 创建可选的常量参数-q,--quiet参数,解析到有该参数时,会执行verbosity=0
parser.add_argument('--locals', dest='tb_locals',
action='store_true',
help='Show local variables in tracebacks')
# 创建可选的bool参数--locals参数,解析到有该参数时,会执行tb_locals=True
if self.failfast is None:
# 创建可选的bool参数-f,--failfast参数,解析到有该参数时,会执行failfast=True
# 设置failfast=False
parser.add_argument('-f', '--failfast', dest='failfast',
action='store_true',
help='Stop on first fail or error')
self.failfast = False
if self.catchbreak is None:
# 创建可选的bool参数-c,--catch参数,解析到有该参数时,会执行catchbreak=True
# 设置catchbreak=False
parser.add_argument('-c', '--catch', dest='catchbreak',
action='store_true',
help='Catch Ctrl-C and display results so far')
self.catchbreak = False
if self.buffer is None:
# 创建可选的bool参数-b,--buffer参数,解析到有该参数时,会执行buffer=True。
# 设置buffer=False
parser.add_argument('-b', '--buffer', dest='buffer',
action='store_true',
help='Buffer stdout and stderr during tests')
self.buffer = False
return parser
def _getMainArgParser(self, parent):
parser = argparse.ArgumentParser(parents=[parent]) # 继承父parser
parser.prog = self.progName # 定义命令的第一个元素,即argv[0]
parser.print_help = self._print_help
parser.add_argument('tests', nargs='*',
help='a list of any number of test modules, '
'classes and test methods.')
# 创建位置参数tests,nargs='*'规定的tests可以给定多个
return parser
def _getDiscoveryArgParser(self, parent):
parser = argparse.ArgumentParser(parents=[parent])
parser.prog = '%s discover' % self.progName # 命令的第一元素结尾加入' discover'
parser.epilog = ('For test discovery all test modules must be '
'importable from the top level directory of the '
'project.')
parser.add_argument('-s', '--start-directory', dest='start',
help="Directory to start discovery ('.' default)")
# 创建可选的关键字参数-s,--start-directory,执行start=[给定值]
parser.add_argument('-p', '--pattern', dest='pattern',
help="Pattern to match tests ('test*.py' default)")
# 创建可选的关键字参数-p,--pattern,执行pattern=[给定值]
parser.add_argument('-t', '--top-level-directory', dest='top',
help='Top level directory of project (defaults to '
'start directory)')
# 创建可选的关键字参数-t,--top-level-directory,执行top=[给定值]
for arg in ('start', 'pattern', 'top'):
# 规定了start,pattern,top也可以作为位置参数给出,nargs='?'第个参数只能给定单个值
parser.add_argument(arg, nargs='?',
default=argparse.SUPPRESS, #禁止参数默认添加,未设置时,如果未解析到参数时会为为参数赋值None
help=argparse.SUPPRESS)
return parser
_initArgParsers本身只有三行代码。第一行,创建一个父parser对象;第二行,继承父parse对象创建main模式的parser对象;第三行,继承父parser对象,创建discover模式的parse对象。这里用到了标准库argparse,用它来解析命令,并将解析结果作为属性传递结指定对象。
代码中各行的作用,笔者按自己的理解注释在了代码中,如有疑问,欢迎留言讨论。
self._initArgParsers构造了一套解析命令的功能,它主要创建了三个argparse.ArgumentParser类的实例:parent_parser,self._main_parser与self._discovery_parser。
(1). parent_parser,该对象是为创建后两个对象作准备,它创建了-v,-q,--local,-f,-c,-b参数,它们的含义笔者在上述代码中进行了注释,而作用可参考笔者手译官方文档command-line。
(2). self._main_parser,该对象继承parent_parse对象,即拥有父对象的所有参数,我称之为main解析模式。另外该对象添加了一个tests的位置参数,该参数可以一次性给定多个,解析到的结果会以list类型赋值给self.tests。这里的self.tests就是需要进行测试的测试模块。
(3). self._discovery_parser,该对象继承parent_parser对象,即拥有父对象的所有参数,我称之为discover解析模式。另外该对象在将原有的argv[0]参数修改为argv[0]+" discover"。并添加了-s,-p,-t参数,这三个参数也可以作为位置参数直接输入,它们的具体涵意可以参考笔者手译官方文档Test Discover。
在self._initArgParsers为我们构建好解析对象之后,我们才可以使用它们来应对各种组合的命令。接下来我们看parseArgs方法中的剩余代码,笔者依然以注释的方式在代码中添加了自己的理解:
def parseArgs(self, argv):
self._initArgParsers() # 构建解析对象
"""解析命令结构,执行相应的解析模式:
1、在命令行运行python -m unittest时,执行unittest包中的__main__.py文件,它调用main(module=None)
2、以脚本方式运行时,可设置module参数,默认为'__main__'
"""
if self.module is None:
# 在命令行使用python -m unittest...时,执行该分支
if len(argv) > 1 and argv[1].lower() == 'discover':
# 如果命令之后接着的是discover,执行批量测试,并可使用discover参数
self._do_discovery(argv[2:])
return
self._main_parser.parse_args(argv[1:], self) # 解析参数
if not self.tests:
# 如果命令只是python -m unittest,执行批量测试,不可使用discover参数
self._do_discovery([])
return
else:
# 脚本中运行时执行该分支
self._main_parser.parse_args(argv[1:], self)
"""为执行createTests()设置条件"""
if self.tests:
self.testNames = _convert_names(self.tests)
if __name__ == '__main__':
# to support python -m unittest ...
self.module = None
elif self.defaultTest is None:
# 未解析到[tests],也未指定defaultTest参数,从脚本所在模块加载测试
self.testNames = None
elif isinstance(self.defaultTest, str):
self.testNames = (self.defaultTest,)
else:
self.testNames = list(self.defaultTest)
self.createTests()
def createTests(self):
# 根据解析结果来执行该方法,以创建self.test,即TestSuite实例的列表。
if self.testNames is None:
# 当没有解析到'tests'并且defaultTest=None时
self.test = self.testLoader.loadTestsFromModule(self.module)
else:
self.test = self.testLoader.loadTestsFromNames(self.testNames,
self.module)
这里注意,代码中有self.tests与self.test,前者是从用户输入的命令或argv参数中解析的"tests"参数(由_getMainArgParser函数定义),是str类型;后者由是由loader加载器所构建的TestSuite类的实例。TestCase与TestSuite我们会在下章介绍。
parseArg方法中的if module is None: ... else: self._main_parse ... 这一段的代码用于解析各种格式的命令;而if self.tests: ... self.createTests() ...这一段的代码则是根据解析的结果来为creatTests方法设置条件,最终目的是使用creatTests方法来创建self.test,即TestSuite类的实例。这里我们得到了所需要的TestSuite实例对象,它就是我们后面需要使用runner执行器去执行的对象。
2. 测试执行函数runTest
runTest是执行测试的函数,该函数具体代码如下:
def runTests(self):
if self.catchbreak:
# 加载control-c处理器
installHandler()
if self.testRunner is None:
self.testRunner = runner.TextTestRunner
if isinstance(self.testRunner, type):
# 根据python的版本不同,创建相应的runner执行器
try:
try:
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
warnings=self.warnings,
tb_locals=self.tb_locals)
except TypeError:
# didn't accept the tb_locals argument
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
warnings=self.warnings)
except TypeError:
# didn't accept the verbosity, buffer or failfast arguments
testRunner = self.testRunner()
else:
# 参数中给定了testRunner是一个实例时
testRunner = self.testRunner
self.result = testRunner.run(self.test) #执行测试,并保存结果,result默认为TextTestResult
if self.exit:
# 如果结果中没有失败或错误以及unexpectedSuccessfule的测试时,执行sys.exit(False),否则执行sys.exit(True)
sys.exit(not self.result.wasSuccessful())
由于python版本的更新中为testRunner陆续添加了参数,所以代码中使用了2个try来创建testRunner。如果在__init__方法的参数中直接给定了testRunner则会跳过这些步骤。接下来的self.result = testRunner.run(self.test)是用来执行测试用例,这行代码执行了测试,并返回结果result,result是TestTestResult类的实例对象,详情可以参考result.py模块中的该类。最后则是使用sys.exit来根据结果来执行程序的退出。
三、总结
TestProgram/main是unittest的最前端的执行程序,它利用了各种组件相互协作来完成测试。代码中我们可以大致了解到loader、runner、result、suite(即self.test)等这些组件的基本作用。下一期先从最底层的case以及suite来了解被测对象是如何创建的。