python3 unittest模块源码解析(一) --- 主程序unittest.main()

本文深入剖析unittest框架,介绍其核心组件,如case、suite、result、loader和runner,并详细讲解unittest.main()方法的工作流程,包括命令行解析和测试执行过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

unittest框架中的重要组件

unittest.main()方法

一、启动测试

1.  在命令行中输入命令

2.  在代码中使用unittest.main()方法

二、main/TestProgram源码解析

1. 命令解析函数parseArgs

2. 测试执行函数runTest

三、总结



unittest框架中的重要组件

unittest包的位置就是python源文件中,包中结构如下:

在分析源码之前我们需要知道unittest的基本构成,这样我们可以围绕这些组件来了解它的实现原理。
unittest框架中主要有以下这些组件:

  1.  case : 测试用例,测试的基本单元
  2.  suite : 测试套件,可以包含多个用例和套件
  3.  result : 测试结果,用来存放测试的结果
  4.  loader : 加载器,用于从各种环境中加载测试
  5.  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来了解被测对象是如何创建的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值