unittest模块提供了单元测试的组件,方便开发人员进行自测。
一、unittest中的重要概念:
测试用例:测试用例对象是最小的测试单位,针对指定的输入来测试期待的输出。由类TestCase的派生类或FunctionTestCase类来创建的。
测试固件:代表了测试相关的准备和清除工作,比如在一个测试进行之前需要创建数据库连接,测试结束之后需要关闭数据库连接。测试固件是在TestCase子类中进行重载的setUp和tearDown函数实现的。每个测试用例执行前后都会自动执行setUp和tearDown方法。另外如果setUp执行抛出异常,则忽略未执行的测试用例,测试结束
测试套件:包含一组测试用例,一起执行。同时,也可以包含其他测试套件。可以通过TestSuite类创建对象来添加测试用例;也可以使用unittest提供的TestLoader来自动将指定的测试用例收集到一个自动创建的TestSuit对象中。
测试驱动:主要负责执行测试,并反馈测试结果。TestRunner对象存在一个run()方法,它接收一个TestCase对象或TestSuit对象作为参数,返回测试的结果对象(TestResult)
二、编写最简单的测试代码
下面是一个数学操作的类,包含加法和除法操作。并提供了对应的单元测试代码,从这个例子上,我们学习一些unittest基本的功能:
#exam.py文件提供了供测试的示例类 #coding: utf-8 class operator(object): def __init__(self, a, b): self.a = a self.b = b def add(self): return self.a + self.b def divide(self): return self.a / self.b #test.py文件提供了通过unittest构建的测试代码 #coding:utf-8 from exam import operator import unittest class TestOperator(unittest.TestCase): def setUp(self): #test fixture self.oper = operator(10,0) def test_add(self): #test case self.assertEqual(self.oper.add(), 10, u"加法基础功能不满足要求") def test_divide(self): self.assertRaises(ZeroDivisionError, self.oper.divide()) #def tearDown(self): #pass if __name__ == "__main__": unittest.main(verbosity=2)
运行test.py文件,即可见到下面的输出:
test_add (__main__.TestOperator) ... ok test_divide (__main__.TestOperator) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
- 测试类需要继承自TestCase
- 测试方法默认是通过前缀test来标示的,所以在测试类中添加非test前缀的辅助方法并不会影响测试用例的搜集。
- 测试方法一般通过TestCase提供的assert*方法来判断结果是否符合预期。
- 每个测试实例都仅包含一个test*方法,即上面的代码会创建两个测试实例,每个测试实例包含一个test*的方法
- unittest.main提供了命令行的接口,启动测试,并反馈测试结果。其中的参数verbosity指详细显示测试结果。
想象:main中的逻辑应该是挺复杂的,需要构建test实例对象?需要找到那些是用于测试的方法?需要统计测试结果?等等一些我们还没认识到的东西?
解决这些困惑的方法很直接,让我们调试main函数吧,,come on!
我们可以看到main代表一个命令行接口类:我们可以通过命令行的方式执行测试,这和通过代码中的main启动测试时一样的过程。
main = TestProgram # ... class TestProgram(object): #命令行接口类 """A command-line program that runs a set of tests; this is primarily for making test modules conveniently executable. """
运行main(),即无传参调用__init__.py来构建一个对象。
def __init__(self, module=‘__main__‘, defaultTest=None, argv=None, testRunner=None, testLoader=loader.defaultTestLoader, exit=True, verbosity=1, failfast=None, catchbreak=None, buffer=None): 。。。。 self.exit = exit self.failfast = failfast self.catchbreak = catchbreak self.verbosity = verbosity self.buffer = buffer self.defaultTest = defaultTest self.testRunner = testRunner self.testLoader = testLoader self.progName = os.path.basename(argv[0]) #以上是初始化工作 self.parseArgs(argv) #解析参数argv,并加载test self.runTests() #运行test,并反馈结果
在执行__init__.py的过程中,首先进行一些初始化工作,即传入main的参数或是通过命令行添加的参数影响了unittest内部的某些特性,比如例子中的verbosity代表了测试结果输出的详细度,如果被设置为1,或者不设置,结果中将不会显示具体的testcase名称,大家可以自己验证一下;
接下来,进入self.parseArgs(argv),让我们看下它做了什么:
def parseArgs(self, argv): if len(argv) > 1 and argv[1].lower() == ‘discover‘: self._do_discovery(argv[2:]) return 。。。。 try: options, args = getopt.getopt(argv[1:], ‘hHvqfcb‘, long_opts) for opt, value in options: if opt in (‘-h‘,‘-H‘,‘--help‘): self.usageExit() if opt in (‘-q‘,‘--quiet‘): self.verbosity = 0 if opt in (‘-v‘,‘--verbose‘): #命令行参数-v即代表了main参数verbosity self.verbosity = 2 if opt in (‘-f‘,‘--failfast‘): if self.failfast is None: self.failfast = True 。。。。 #以上是从argv中读取参数,并适当对初始化值进行修改 self.createTests() #创建测试实例,返回他们的集合-suit对象(测试套件) 。。。。
首先,参数如果是‘discover’则进入另一个分支,是关于自动发现的功能,后面会讲到。
然后开始解析argv,这里的argv首选传入main的argv参数,如果为None,则取命令行执行该脚本时传递的sys.argv。可以看到命令行传递的sys.argv参数和传递到main的其他参数是相互替代的,这就达到了通过命令行传参启动和通过main代码传参启动,效果是一样的。
接下来调用createTests来创建测试实例,我们继续看下:
def createTests(self): if self.testNames is None: self.test = self.testLoader.loadTestsFromModule(self.module) else: self.test = self.testLoader.loadTestsFromNames(self.testNames, self.module)
仅从方法的名字就可以看出,创建Tests就是在模块或是具体的test方法上加载。加载的过程主要就是搜集测试方法,创建TestCase实例,并返回包含有这些case的TestSuit对象,后面会详细看下。
至此,创建测试实例完成,接着就回到__init__中执行self.runTest()来真正启动测试了:
def runTests(self): if self.catchbreak: #-c表示运行过程中捕捉CTRL+C异常 installHandler() if self.testRunner is None: self.testRunner = runner.TextTestRunner #runner默认是TextTestRunner if isinstance(self.testRunner, (type, types.ClassType)): try: testRunner = self.testRunner(verbosity=self.verbosity, failfast=self.failfast, buffer=self.buffer) except TypeError: # didn‘t accept the verbosity, buffer or failfast arguments testRunner = self.testRunner() else: # it is assumed to be a TestRunner instance testRunner = self.testRunner #以上部分是构建testRunner对象,即测试驱动 self.result = testRunner.run(self.test) #就像上面讲到的由runner的run方法启动测试 if self.exit: sys.exit(not self.result.wasSuccessful())
从代码中可以看出,测试由testRunner实例通过run函数来启动,默认的testRunner是unittest提供的TextTestRunner。这个run方法设计很亮眼,感兴趣的同志可以深入看下,里面涉及了__call__和__iter__的用法并且巧妙结合。
main函数简单的调用即代替我们完成了基本的测试功能,其内部可是复杂滴很哦。
三、命令行接口
上面我们看到了,main和命令行接口根本就是同一个类,只是这个类做了两种执行方式的兼容。
使用python -m unittest -h可以查看帮助命令,其中python -m unittest discover是命令行的另一分支,后面讨论,它也有自己的帮助命令,即也在后面加上-h
具体的命令可自行研究。
四、测试发现
测试发现指,提供起始目录,自动搜索该目录下的测试用例。与loadTestsFromModule等相同的是都由TestLoader提供,用来加载测试对象,返回一个TestSuit对象(包裹了搜索到的测试对象)。不同的是,测试发现可以针对一个给定的目录来搜索。
也可以通过上面提到的命令行来自动发现:python -m unittest discover **
可以指定下面的参数:-s 起始目录(.) -t 顶级目录(.) -p 测试文件的模式匹配
过程简要描述如下:目录:顶级目录/起始目录,该目录应该是一个可导入的包,即该目录下应该提供__init__.py文件。在该目录下。使用-p模式匹配test用例所在的文件,然后在从这些文件中默认通过‘test’前缀来搜集test方法构建test实例,最终返回一个test实例集合的suit对象。
五、一些好用的修饰器
unittest支持跳过某些测试方法甚至整个测试类,也可以标志某些方法是期待的不通过,这样如果不通过的话就不会列入failure的计数中。等等这些都是通过装饰器来实现的。让我们把本文开篇的基础的例子重用一下,将test.py改成下面这样:
#test.py文件提供了通过unittest构建的测试代码 #coding:utf-8 from exam import operator import unittest,sys class TestOperator(unittest.TestCase): def setUp(self): #test fixture self.oper = operator(10,0) @unittest.skip("I TRUST IT") # def test_add(self): #test case self.assertEqual(self.oper.add(), 10, u"加法基础功能不满足要求") @unittest.skipIf(sys.platform == ‘win32‘, "it just only run in Linux!") def test_divide(self): self.assertRaises(ZeroDivisionError, self.oper.divide()) #def tearDown(self): #pass if __name__ == "__main__": unittest.main(verbosity=2)
再次运行之后,结果如下:
test_add (__main__.TestOperator) ... skipped ‘I TRUST IT‘ test_divide (__main__.TestOperator) ... skipped ‘it just only run in Linux!‘ ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK (skipped=2)
unittest.skipUnless(condition, reason):如果condition为真则不会跳过该测试
unittest.expectedFailure():将该test标志为期待的失败。之后如果该测试不符合预期或引发异常,则不会计入失败数
一直很崇拜装饰器,不如就在此领略一下大神的风采,让我们看看到底装饰器是否必要,主要应用场景是什么。就先拿里面最简单的skip来看吧:
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__属性即可,这会在实例运行中判断。如果修饰测试方法时,会将修饰的方法替代为一个触发SkipTest异常的方法,并同样给修饰的方法添加__unittest_skip__属性。
添加的属性在测试实例运行时会用到,在TestCase类提供的run方法中作判断:
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_skip__属性为真,则会跳过该测试。通过上面我们看出,实例运行时只会检查__unittest_skip__属性值而并不会抓取SkipTest异常,那为什么skip装饰器中要对修饰的函数进行替换的操作呢?
想不通,注释掉if块,程序依然可以运行的好好的,留个疑点吧!