python3 unittest模块源码解析(三) --- 测试结果result

一、result简介

照惯例,引用官方文档的说明:

This class is used to compile information about which tests have succeeded and which have failed.

该类是用来编辑测试的成功与失败的信息。

上述说明是对unittest.TestResult类的说明,该类或该类子类的实例化对象是源码中被频繁使用的。


二、TestResult类

进入result.py模块,在TestResult类源码之前还有一个装饰器函数:

def failfast(method):
    @wraps(method)
    def inner(self, *args, **kw):
        if getattr(self, 'failfast', False):
            self.stop()
        return method(self, *args, **kw)
    return inner

该函数是当检查到failfast属性时,执行stop函数停止测试。在TestResult类中我们可以看到有3个方法使用了该装饰器,分别是:addError、addFailure、addUnexpectedSuccess,意味着遇到这3种情况时中止测试。

下面来解读TestResultI源码:

1. TestResult的属性

class TestResult(object):

    _previousTestClass = None
    _testRunEntered = False
    _moduleSetUpFailed = False

    def __init__(self, stream=None, descriptions=None, verbosity=None):
        self.failfast = False                    # 是否遇到第一个失败或错误时就停止测试
        self.failures = []                       # 存放发生AssertionError错误的测试
        self.errors = []                         # 存放发生非AssertionError错误的测试
        self.testsRun = 0                        # 计算测试的数量
        self.skipped = []                        # 存放跳过的测试
        self.expectedFailures = []               # 存放expectedFailure的测试
        self.unexpectedSuccesses = []            # 存放unexpectedSuccess的测试
        self.shouldStop = False                  # 一个标记,如果变为True则停止测试
        self.buffer = False                      # 是否启用缓存
        self.tb_locals = False
        self._stdout_buffer = None               # stdout的缓存对象
        self._stderr_buffer = None               # stderr的缓存对象
        self._original_stdout = sys.stdout       # 保存原始的stdout
        self._original_stderr = sys.stderr       # 保存原始的stderr
        self._mirrorOutput = False

    

2. stdout与stderr的缓存处理

    def _setupStdout(self):
        if self.buffer:
            if self._stderr_buffer is None:
                self._stderr_buffer = io.StringIO()        # 创建一个StringIO对象,用来存放缓存的stdout信息
                self._stdout_buffer = io.StringIO()        # 创建一个StringIO对象,用来存放缓存的stderr信息
            sys.stdout = self._stdout_buffer               # 将系统的stdout重定向到自定义StriongIO对象
            sys.stderr = self._stderr_buffer               # 将系统的stderr重定向到自定义的stringIO对象

    def _restoreStdout(self):
        if self.buffer:
            if self._mirrorOutput:
                output = sys.stdout.getvalue()             
                error = sys.stderr.getvalue()
                if output:
                    if not output.endswith('\n'):
                        output += '\n'
                    self._original_stdout.write(STDOUT_LINE % output)    # 向原始的stdout地址中写入缓存的stdout信息
                if error:
                    if not error.endswith('\n'):
                        error += '\n'
                    self._original_stderr.write(STDERR_LINE % error)     # 向原始的stdout地址中写入缓存的stderr信息

            sys.stdout = self._original_stdout        # 将系统的stdout定向到原始地址
            sys.stderr = self._original_stderr        # 将系统的stderr定向到原始地址
            self._stdout_buffer.seek(0)
            self._stdout_buffer.truncate()            # 清除stdout缓冲区信息
            self._stderr_buffer.seek(0)
            self._stderr_buffer.truncate()            # 清除stderr缓冲区信息

系统的sys.stdout/stderr默认指向标准数据流对象stream,调用stream.write方法会将标准的输出与错误信息打印在屏幕上的,代码中为了实现信息缓存,将它们重定向到了一个自定义的StringIO对象,在它们被还原到原始地址以前,输出/错误信息都不会显示在屏幕上。

在__ini__方法中,首先为sys.stdout/stderr指向的stream创建了一个新的地址标签,用于之后恢复以及在镜像模式时打印信息,分别是_original_stdout与_original_stderr。

在_setupStdout方法中在未自定义缓存对象的情况下,会创建2个StringIO类的实例对象,这2个对象将会被作为缓存区使用,分别为_stdout_buffer与_stderr_buffer。随后将sys.stdout/stderr重定向到这2个对象,程序执行时的输出就会存放到这2个StringIO对象中。

在_restoreStdout方法中,如果self._mirrorOutput=True,则从已经重定向到StringIO对象的stdout/err中读取信息,并将其打印到屏幕。最后6行代码,前2行将sys.stdout/stderr重定向到原始地址,后4行则是清除缓存区信息。

3. startTest*与stopTest*方法

    def startTest(self, test):
        """ 每一个测试用例开始前执行的方法 """
        self.testsRun += 1
        self._mirrorOutput = False
        self._setupStdout()

    def startTestRun(self):
        """ 第一个测试用例开始前执行的方法 """

    def stopTest(self, test):
        """ 每一个测试用例结束后执行的方法 """
        self._restoreStdout()
        self._mirrorOutput = False

    def stopTestRun(self):
        """ 最后一个测试用例结束后运行的测试 """

前4个方法都是在测试执行前后执行的,对测试本身并没有影响。startTest与stopTest是每个测试周期中测试前后执行的方法,而startTestRun与stopTestRun则是所有测试开始前及全部执行完后才执行的方法。它们与测试的关系如下图所示:

startTestRun与stopTestRun默认没有实现任何功能的,需要用户自定义。startTest与stopTest实现了每测试前后将_mirrorOutput属性还原为False,以及如果启用buffer功能则实现缓存区的创建与释放,另外startTest中还完成了测试数量的统计。

4. add*方法以及_failfast装饰器

    def failfast(method):
        @wraps(method)
        def inner(self, *args, **kw):
            if getattr(self, 'failfast', False):
                self.stop()
            return method(self, *args, **kw)
        return inner

    @failfast
    def addError(self, test, err):
        """Called when an error has occurred. 'err' is a tuple of values as
        returned by sys.exc_info().
        非AssertionError类错误时才会调用该方法
        """
        # 将错误信息添加到self.errors属性中,这里亦执行了self._exc_info_to_string方法将信息打印在屏幕上
        self.errors.append((test, self._exc_info_to_string(err, test)))     
        self._mirrorOutput = True        # 使buffer功能启用时仍打印错误信息在屏幕上

    @failfast
    def addFailure(self, test, err):
        """
        Called when an error has occurred. 'err' is a tuple of values as
        returned by sys.exc_info(). 
        是AssertionError类错误时才会调用该方法
        """
        self.failures.append((test, self._exc_info_to_string(err, test)))   
        self._mirrorOutput = True

    def addSubTest(self, test, subtest, err):
        """
        Called at the end of a subtest.
        'err' is None if the subtest ended successfully, otherwise it's a
        tuple of values as returned by sys.exc_info().
        """
        # By default, we don't do anything with successful subtests, but
        # more sophisticated test results might want to record them.
        if err is not None:
            if getattr(self, 'failfast', False):
                self.stop()
            if issubclass(err[0], test.failureException):        # 根据错误类型是否是AssertionError子类,来将错误添加到相应的属性中
                errors = self.failures
            else:
                errors = self.errors
            errors.append((subtest, self._exc_info_to_string(err, test)))    # 这里会执行self._exc_info_to_string()方法,从而将错误信息打印在屏幕上 
            self._mirrorOutput = True

    def addSuccess(self, test):
        "Called when a test has completed successfully"
        pass

    def addSkip(self, test, reason):
        """Called when a test is skipped."""
        self.skipped.append((test, reason))

    def addExpectedFailure(self, test, err):
        """Called when an expected failure/error occurred."""
        self.expectedFailures.append(
            (test, self._exc_info_to_string(err, test)))

    @failfast
    def addUnexpectedSuccess(self, test):
        """Called when a test was expected to fail, but succeed.
        当一个预期失败的测试执行成功时,执行该方法"""
        self.unexpectedSuccesses.append(test)

首先我们看failfast(method)方法,它的内部函数仅仅是在method前增加了一个if判断,if条件语句中的self.stop()方法的作用是将self.shouldStop属性设置为True,而后面我们会了解到,self.shoudstop属性为True时测试将会停止。因此这个装饰器的作用就是,任何使用该装饰器的方法在执行前都必须通过判断self.failfast的真假,来确定是否停止测试。

以add开头的方法中,addSubTest是比较特殊的,它是只有代码中使用了subtest并在subtest代码块中发生了错误时才会执行的方法。subtest内的测试内容会被封装为一个_SubTest类(一个TestCase类的了类),在将测试结果存储进result对象时会先行判断它是否是一个_SubTest类,注意subtest内的测试并不会算入测试的总数中,其错误亦是如此。

addError、addFailure、addUnexpectedSuccess这三个方法都装饰了@failfast装饰器,因此当这三个方法被调用时,failfast属性决定了是否中止测试。这三个方法的调用条件已在代码的描述处进行了说明,代码内部则只是将发生了该异常的测试对象及异常信息添加到相应的属性中。

addSuccess方法默认不执行任何操作,它是在测试成功时调用,用户可以根据需求自定义result对象时重写该方法。

addSkip方法在跳过测试时执行。addExpectedFailure则在一个预期失败的测试执行也失败时调用。

5. wasSuccessful与stop

    def wasSuccessful(self):
        """Tells whether or not this result was a success."""
        # The hasattr check is for test_result's OldResult test.  That
        # way this method works on objects that lack the attribute.
        # (where would such result intances come from? old stored pickles?)
        return ((len(self.failures) == len(self.errors) == 0) and
                (not hasattr(self, 'unexpectedSuccesses') or
                 len(self.unexpectedSuccesses) == 0))

    def stop(self):
        """Indicates that the tests should be aborted."""
        self.shouldStop = True

wasSuccessful是用来判断测试是否完全通过的,从代码中可以看出,只有self.failures与self.errors属性为空,则没有发生unexpectedSuccesses异常时,结果才为真。

stop方法则是简单地将self.shoultStop属性设置为True,测试中会通过检测该属性的值来判断是否停止测试。

6.其它方法

    def _exc_info_to_string(self, err, test):
        """Converts a sys.exc_info()-style tuple of values into a string."""
        exctype, value, tb = err        # err是由sys.exc_info()方法返回的元组

        # 如果异常是由unittest内部组件抛出的,则self._is_relevant_tb_level(tb)的值为True。
        # 因此while循环的作用是从当前位置开始,向异常发生的源头迭代(即对异常栈进行迭代),直
        # 到找到第1个由非unittest内部组件抛出的异常为止,并将tb赋值为由该文件抛出的异常对象
        while tb and self._is_relevant_tb_level(tb):   
            tb = tb.tb_next

        # 统计抛出了异常的非unittest内部组件的对象数,用以创建新的异常
        if exctype is test.failureException:
            length = self._count_relevant_tb_levels(tb)
        else:
            length = None
        tb_e = traceback.TracebackException(
            exctype, value, tb, limit=length, capture_locals=self.tb_locals)
        msgLines = list(tb_e.format())

        if self.buffer:
            output = sys.stdout.getvalue()
            error = sys.stderr.getvalue()
            if output:
                if not output.endswith('\n'):
                    output += '\n'
                msgLines.append(STDOUT_LINE % output)
            if error:
                if not error.endswith('\n'):
                    error += '\n'
                msgLines.append(STDERR_LINE % error)
        return ''.join(msgLines)


    def _is_relevant_tb_level(self, tb):
        """
        unittest包中每一个组件中都含有一个'__unittest'变量;
        这里用来判断异常是否是unittest组件内部发生的
        """
        return '__unittest' in tb.tb_frame.f_globals

    def _count_relevant_tb_levels(self, tb):
        """
        对异常栈进行迭代,统计由非unittest内部组件抛出的异常数
        """
        length = 0
        while tb and not self._is_relevant_tb_level(tb):
            length += 1
            tb = tb.tb_next
        return length

    def __repr__(self):
        """result对象的打印显示"""
        return ("<%s run=%i errors=%i failures=%i>" %
               (util.strclass(self.__class__), self.testsRun, len(self.errors),
                len(self.failures)))

_exc_info_to_string方法的作用是将sys.exc_info()方法所捕获的异常转换为字符串的形式。这之前我们需要已经了解了异常栈的简单知识:当异常抛出时,该异常的抛出过程往往经过了多层方法的调用,这些层则构成了栈,栈顶则是当前的抛出点。在该方法中,通过while循环迭代,在异常栈中找出那些非unittest内部组件部分,将这部分整合后反馈给用户,这样可以大大减少异常中不必要的信息量。因为由人为编写的代码引发的异常才是我们需要重点关注的。

_is_relevant_tb_level方法是用来判断当前帧的tracebacke对象是否是由unittest内部组件抛出。(unittest包中的每一个组件文件中都有一行__unittest = True

 

 

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值