前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解unittest测试框架(二)框架核心——case

理解unittest测试框架(二)框架核心——case

原创
作者头像
点点寒彬
修改2020-03-18 15:13:30
8300
修改2020-03-18 15:13:30
举报
文章被收录于专栏:用Python做测试

背景

上文说到unittest框架的入口,知道了多种方式执行unittest的时候,框架是如何处理的。

本文会详细说明测试框架的核心,case:测试用例是如何构成的。

从使用来感知

我们在编写测试用例的有这么几步

  1. 需要定义一个类,这个类名以Test开头,并且需要继承unttest.TestCase
  2. 定义好这个类之后,在这个类中定义以test开头的函数。
  3. 在这个函数中编写用例

unittest测试框架最终会把函数当成一条测试用例去执行。

在执行的过程中,unittest测试框架会根据参数来打印不同详细程度的执行日志。

执行完毕后,打印出执行结果是成功或者是失败。

同时,在执行用例主体前,会执行setUp()初始化函数,执行用例完毕后,会执行tearDown()清理函数。

如果有多条用例,那么就会一直重复以上步骤。

case的构成

以上的核心就在TestCase这个基类。

从代码的结构来看,TestCase除了主体的功能,有很大一部分都是断言类的方法。断言类的方法不多展开描述,主要看主体的功能。

  • init

在测试的初始化中,主要的功能就是定义一些全局的信息,比较关键的有两个点。

第一,入参中传入methodName,默认值是runTest

实际上,在测试套件创建测试用例的时候,会把测试用例的名称(test开头的函数名))传进来,这里后面会用到。

第二,定义了一个清理的容器。

这个在后续,会吧清理方法丢进来,按顺序的去执行清理的工作。

其他功能都是一些断言的方法,或者声明了空的钩子方法。无需特别关注。

  • run

这个是需要重点关注的,整个测试执行的编排就是通过这个钩子函数来执行的。我们一段一段来看。

代码语言:txt
复制
orig_result = result
if result is None:
    result = self.defaultTestResult()
    startTestRun = getattr(result, 'startTestRun', None)
    if startTestRun is not None:
        startTestRun()

self._resultForDoCleanups = result
result.startTest(self)

testMethod = getattr(self, self._testMethodName)

最上面的部分是声明了测试结果的对象。这里会调用结果对象的钩子函数,告诉结果对象,测试用例开始执行了,结果对象会通过标准输出的方式把用例启动的信息打出来,关于结果对象,我们后续再看。

第二部分是把init方法中的testMethodName这个对象动态的加载进来,读者可以用debug模式在这里打一个断点。上文也提到了。这个参数其实就是函数名。通过getattr方法加载进来这个对象。

接着往下看。

代码语言:txt
复制
if (getattr(self.__class__, "__unittest_skip__", False) or
    getattr(testMethod, "__unittest_skip__", False)):
    # If the class or method was skipped.
    try:
        skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                    or getattr(testMethod, '__unittest_skip_why__', ''))
        self._addSkip(result, skip_why)
    finally:
        result.stopTest(self)
    return

这一部分是检查测试用例是否被跳过了,如果发现用例需要跳过,那么就把跳过的原因放到结果对象去,结果对象会把这些数据通过标准输出的方式打到控制台。

unittest中,跳过测试用例的方式是通过@unittest.skip()的装饰器来实现跳过执行的,这个skip方法也是在TestCase所属的case.py文件中。

代码语言:txt
复制
def skip(reason):
    """
    Unconditionally skip a test.
    """
    def decorator(test_item):
        if not isinstance(test_item, (type, types.ClassType)):
            @functools.wraps(test_item)
            def skip_wrapper(*args, **kwargs):
                raise SkipTest(reason)
            test_item = skip_wrapper

        test_item.__unittest_skip__ = True
        test_item.__unittest_skip_why__ = reason
        return test_item
    return decorator

这个skip的方法,实际上就是给这个测试对象添加__unittest_skip____unittest_skip_why__这两个属性。而测试用例是否跳过执行,就是从测试对象中拿这两个参数来对比。

这些检查做完之后,则是测试用例开始执行的代码了。代码有点长,我们一段一段来看。

代码语言:txt
复制
try:
    self.setUp()
except SkipTest as e:
    self._addSkip(result, str(e))
except KeyboardInterrupt:
    raise
except:
    result.addError(self, sys.exc_info())

这一部分的代码,处理的是当前对象的setUp()方法。在TestCase中,这个setUp()是一个空方法,我们写的时候如果写了setUp()方法,那么就相当于在子类中重写了setUp(),当然,不管怎样,这个方法都会优先于测试用例本身的代码执行。

这里监听了几个异常,如果有抛错跳过,那么就停止执行,如果监听到了KeyboardInterrupt异常,这个异常实际上就是我们在执行的时候按下Ctrl+C的信号引发的异常。如果有其他异常,同样的会停止执行。

代码语言:txt
复制
try:
    testMethod()
except KeyboardInterrupt:
    raise
except self.failureException:
    result.addFailure(self, sys.exc_info())
except _ExpectedFailure as e:
    addExpectedFailure = getattr(result, 'addExpectedFailure', None)
    if addExpectedFailure is not None:
        addExpectedFailure(self, e.exc_info)
    else:
        warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
                        RuntimeWarning)
        result.addSuccess(self)
except _UnexpectedSuccess:
    addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
    if addUnexpectedSuccess is not None:
        addUnexpectedSuccess(self)
    else:
        warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures",
                        RuntimeWarning)
        result.addFailure(self, sys.exc_info())
except SkipTest as e:
    self._addSkip(result, str(e))
except:
    result.addError(self, sys.exc_info())
else:
    success = True

这里逻辑其实也不复杂,testMethod这个对象在run方法开始的时候,就被赋值了测试函数作为对象,这里则是执行这个对象。这里有一大堆的异常捕获,有兴趣的读者可以慢慢的去跟踪这些异常。执行完毕如果没有发现异常,则把success置为True

用例执行完毕之后需要执行清理函数。

代码语言:txt
复制
try:
    self.tearDown()
except KeyboardInterrupt:
    raise
except:
    result.addError(self, sys.exc_info())
    success = False

这里的逻辑基本上与setUp()类似。值得注意的是,如果清理函数执行失败了,用例也会被当成失败的。

如果根据平时写用例来看,到这里似乎流程就跟踪完了。实际上我们看代码之后发现,还没有结束。

代码语言:txt
复制
cleanUpSuccess = self.doCleanups()
success = success and cleanUpSuccess
if success:
    result.addSuccess(self)

这里还调用了一次doCleanups()方法。

代码语言:txt
复制
def doCleanups(self):
    """Execute all cleanup functions. Normally called for you after
    tearDown."""
    result = self._resultForDoCleanups
    ok = True
    while self._cleanups:
        function, args, kwargs = self._cleanups.pop(-1)
        try:
            function(*args, **kwargs)
        except KeyboardInterrupt:
            raise
        except:
            ok = False
            result.addError(self, sys.exc_info())
    return ok

这里会循环的把self._cleanups中的对象弹出来执行,弹出的数据是这个数组的最后一个对象给弹出来执行。所以自定义清理函数的执行原则是后进先出

上面的这些执行逻辑统一包在一个try里面,执行了这些之后,不论结果如果,都会执行最后的工作。

代码语言:txt
复制
finally:
    result.stopTest(self)
    if orig_result is None:
        stopTestRun = getattr(result, 'stopTestRun', None)
        if stopTestRun is not None:
            stopTestRun()

最后的工作实际上就是结果对象的标准输出了。

FunctionTestCase

case.py文件中,还有一个这个我们基本上很少见到的方法。它同样是TestCase的子类。

这个方法的作用其实是一个装饰器,通过这个装饰器,可以吧一个已有的函数变成测试框架兼容的函数,通过源代码我们可以看到这个类中有这样一个方法.

代码语言:txt
复制
def runTest(self):
    self._testFunc()

而在上文中有提到,TestCase方法的methodName这个参数的默认值是runTest。而如果是一个正常的测试用例,这个参数会被传入函数名。而使用这个修饰器的函数,传入的就是默认值。所以在run中执行的testMethodName就是这个runTest方法。

总结

本文介绍了unittest测试框架中的测试用例是如何运行的。

再次回顾一下,测试用例首先呢通过初始化的时候传入用例名(测试函数名)。通过编排,顺序执行setUp()、测试用例主体逻辑、tearDown()以及个性化的doCleanups。完成一条用例的执行。

补充

  • 关于getattr方法

这个方法其实非常常见,可以简单的理解反射hook或者代理。通过这种方式可以拿到对象的某个方法的实例。再通过调用的方式来执行对象中的方法。

例如如下代码:

代码语言:txt
复制
class A(object):
    def test_attr():
        print("hello")

这样的一个类,下面两种方式,执行的结果是样的:

代码语言:txt
复制
a = A()
a.test_attr()


a = getattr(A, 'test_attr')
a()

在设计框架的时候,经常会使用到这种机制。相比于第一种方式来看,这种方式更加灵活。即使不知道传入的类是什么。只要这个类有对应的方法就可以执行。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 从使用来感知
  • case的构成
  • FunctionTestCase
  • 总结
  • 补充
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档