Python必会的单元测试框架 —— unittest

用Python搭建自动化测试框架,我们需要组织用例以及测试执行,这里博主推荐Python的标准库——unittest。

unittest是xUnit系列框架中的一员,如果你了解xUnit的其他成员,那你用unittest来应该是很轻松的,它们的工作方式都差不多。

unittest核心工作原理

unittest中最核心的四个概念是:test case, test suite, test runner, test fixture

下面我们分别来解释这四个概念的意思,先来看一张unittest的静态类图(下面的类图以及解释均来源于网络,原文链接):

unittest类图

一个TestCase的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。

而多个测试用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。

TestLoader是用来加载TestCase到TestSuite中的,其中有几个loadTestsFrom__()方法,就是从各个地方寻找TestCase,创建它们的实例,然后add到TestSuite中,再返回一个TestSuite实例。

TextTestRunner是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法。

测试的结果会保存到TextTestResult实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息。

而对一个测试用例环境的搭建和销毁,是一个fixture。

一个class继承了unittest.TestCase,便是一个测试用例,但如果其中有多个以test开头的方法,那么每有一个这样的方法,在load的时候便会生成一个TestCase实例,如:一个class中有四个test_xxx方法,最后在load到suite中时也有四个测试用例。

到这里整个流程就清楚了:

写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,我们通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者我们可以直接通过TextTestRunner来执行用例。这里加个说明,在Runner执行时,默认将执行结果输出到控制台,我们可以设置其输出到文件,在文件中查看结果(你可能听说过HTMLTestRunner,是的,通过它可以将结果输出到HTML中,生成漂亮的报告,它跟TextTestRunner是一样的,从名字就能看出来,这个我们后面再说)。

unittest实例

下面我们通过一些实例来更好地认识一下unittest。

我们先来准备一些待测方法:

mathfunc.py

defadd(a, b):returna+bdefminus(a, b):returna-bdefmulti(a, b):returna*bdefdivide(a, b):returna/b

简单示例

接下来我们为这些方法写一个测试:

test_mathfunc.py

# -*- coding: utf-8 -*-importunittestfrommathfuncimport*classTestMathFunc(unittest.TestCase):"""Test mathfuc.py"""deftest_add(self):"""Test method add(a, b)"""self.assertEqual(3, add(1,2))        self.assertNotEqual(3, add(2,2))deftest_minus(self):"""Test method minus(a, b)"""self.assertEqual(1, minus(3,2))deftest_multi(self):"""Test method multi(a, b)"""self.assertEqual(6, multi(2,3))deftest_divide(self):"""Test method divide(a, b)"""self.assertEqual(2, divide(6,3))        self.assertEqual(2.5, divide(5,2))if__name__ =='__main__':    unittest.main()

执行结果:

.F..======================================================================FAIL: test_divide (__main__.TestMathFunc)Test method divide(a, b)----------------------------------------------------------------------Traceback (most recent call last):  File"D:/py/test_mathfunc.py", line26,intest_divideself.assertEqual(2.5, divide(5,2))AssertionError:2.5!=2----------------------------------------------------------------------Ran4testsin0.000sFAILED (failures=1)

能够看到一共运行了4个测试,失败了1个,并且给出了失败原因,2.5 != 2也就是说我们的divide方法是有问题的。

这就是一个简单的测试,有几点需要说明的:

在第一行给出了每一个用例执行的结果的标识,成功是.,失败是F,出错是E,跳过是S。从上面也可以看出,测试的执行跟方法的顺序没有关系,test_divide写在了第4个,但是却是第2个执行的。

每个测试方法均以test开头,否则是不被unittest识别的。

在unittest.main()中加verbosity参数可以控制输出的错误报告的详细程度,默认是1,如果设为0,则不输出每一用例的执行结果,即没有上面的结果中的第1行;如果设为2,则输出详细的执行结果,如下:

test_add (__main__.TestMathFunc)Test method add(a, b) ... oktest_divide (__main__.TestMathFunc)Test method divide(a, b) ... FAILtest_minus (__main__.TestMathFunc)Test method minus(a, b) ... oktest_multi (__main__.TestMathFunc)Test method multi(a, b) ... ok======================================================================FAIL: test_divide (__main__.TestMathFunc)Test method divide(a, b)----------------------------------------------------------------------Traceback (most recent call last):  File"D:/py/test_mathfunc.py", line26,intest_divideself.assertEqual(2.5, divide(5,2))AssertionError:2.5!=2----------------------------------------------------------------------Ran4testsin0.002sFAILED (failures=1)

可以看到,每一个用例的详细执行情况以及用例名,用例描述均被输出了出来(在测试方法下加代码示例中的"""Doc String""",在用例执行时,会将该字符串作为此用例的描述,加合适的注释能够使输出的测试报告更加便于阅读

组织TestSuite

上面的代码示例了如何编写一个简单的测试,但有两个问题,我们怎么控制用例执行的顺序呢?(这里的示例中的几个测试方法并没有一定关系,但之后你写的用例可能会有先后关系,需要先执行方法A,再执行方法B),我们就要用到TestSuite了。我们添加到TestSuite中的case是会按照添加的顺序执行的

问题二是我们现在只有一个测试文件,我们直接执行该文件即可,但如果有多个测试文件,怎么进行组织,总不能一个个文件执行吧,答案也在TestSuite中。

下面来个例子:

在文件夹中我们再新建一个文件,test_suite.py

# -*- coding: utf-8 -*-importunittestfromtest_mathfuncimportTestMathFuncif__name__ =='__main__':    suite = unittest.TestSuite()    tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")]    suite.addTests(tests)    runner = unittest.TextTestRunner(verbosity=2)    runner.run(suite)

执行结果:

test_add (test_mathfunc.TestMathFunc)Test method add(a, b) ... oktest_minus (test_mathfunc.TestMathFunc)Test method minus(a, b) ... oktest_divide (test_mathfunc.TestMathFunc)Test method divide(a, b) ... FAIL======================================================================FAIL: test_divide (test_mathfunc.TestMathFunc)Test method divide(a, b)----------------------------------------------------------------------Traceback (most recent call last):  File"D:\py\test_mathfunc.py", line26,intest_divideself.assertEqual(2.5, divide(5,2))AssertionError:2.5!=2----------------------------------------------------------------------Ran3testsin0.001sFAILED (failures=1)

可以看到,执行情况跟我们预料的一样:执行了三个case,并且顺序是按照我们添加进suite的顺序执行的。

上面用了TestSuite的addTests()方法,并直接传入了TestCase列表,我们还可以:

# 直接用addTest方法添加单个TestCasesuite.addTest(TestMathFunc("test_multi"))# 用addTests + TestLoader# loadTestsFromName(),传入'模块名.TestCase名'suite.addTests(unittest.TestLoader().loadTestsFromName('test_mathfunc.TestMathFunc'))suite.addTests(unittest.TestLoader().loadTestsFromNames(['test_mathfunc.TestMathFunc']))# loadTestsFromNames(),类似,传入列表# loadTestsFromTestCase(),传入TestCasesuite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))

注意,用TestLoader的方法是无法对case进行排序的,同时,suite中也可以套suite。

将结果输出到文件中

用例组织好了,但结果只能输出到控制台,这样没有办法查看之前的执行记录,我们想将结果输出到文件。很简单,看示例:

修改test_suite.py

# -*- coding: utf-8 -*-importunittestfromtest_mathfuncimportTestMathFuncif__name__ =='__main__':    suite = unittest.TestSuite()    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))withopen('UnittestTextReport.txt','a')asf:        runner = unittest.TextTestRunner(stream=f, verbosity=2)        runner.run(suite)

执行此文件,可以看到,在同目录下生成了UnittestTextReport.txt,所有的执行报告均输出到了此文件中,这下我们便有了txt格式的测试报告了。

test fixture之setUp() tearDown()

上面整个测试基本跑了下来,但可能会遇到点特殊的情况:如果我的测试需要在每次执行之前准备环境,或者在每次执行完之后需要进行一些清理怎么办?比如执行前需要连接数据库,执行完成之后需要还原数据、断开连接。总不能每个测试方法中都添加准备环境、清理环境的代码吧。

这就要涉及到我们之前说过的test fixture了,修改test_mathfunc.py

# -*- coding: utf-8 -*-importunittestfrommathfuncimport*classTestMathFunc(unittest.TestCase):"""Test mathfuc.py"""defsetUp(self):print"do something before test.Prepare environment."deftearDown(self):print"do something after test.Clean up."deftest_add(self):"""Test method add(a, b)"""print"add"self.assertEqual(3, add(1,2))        self.assertNotEqual(3, add(2,2))deftest_minus(self):"""Test method minus(a, b)"""print"minus"self.assertEqual(1, minus(3,2))deftest_multi(self):"""Test method multi(a, b)"""print"multi"self.assertEqual(6, multi(2,3))deftest_divide(self):"""Test method divide(a, b)"""print"divide"self.assertEqual(2, divide(6,3))        self.assertEqual(2.5, divide(5,2))

我们添加了setUp()和tearDown()两个方法(其实是重写了TestCase的这两个方法),这两个方法在每个测试方法执行前以及执行后执行一次,setUp用来为测试准备环境,tearDown用来清理环境,已备之后的测试。

我们再执行一次:

test_add (test_mathfunc.TestMathFunc)Test method add(a, b) ... oktest_divide (test_mathfunc.TestMathFunc)Test method divide(a, b) ... FAILtest_minus (test_mathfunc.TestMathFunc)Test method minus(a, b) ... oktest_multi (test_mathfunc.TestMathFunc)Test method multi(a, b) ... ok======================================================================FAIL: test_divide (test_mathfunc.TestMathFunc)Test method divide(a, b)----------------------------------------------------------------------Traceback (most recent call last):  File"D:\py\test_mathfunc.py", line36,intest_divideself.assertEqual(2.5, divide(5,2))AssertionError:2.5!=2----------------------------------------------------------------------Ran4testsin0.000sFAILED (failures=1)dosomething before test.Prepare environment.adddosomething after test.Clean up.dosomething before test.Prepare environment.dividedosomething after test.Clean up.dosomething before test.Prepare environment.minusdosomething after test.Clean up.dosomething before test.Prepare environment.multidosomething after test.Clean up.

可以看到setUp和tearDown在每次执行case前后都执行了一次。

如果想要在所有case执行之前准备一次环境,并在所有case执行结束之后再清理环境,我们可以用setUpClass()与tearDownClass():

...classTestMathFunc(unittest.TestCase):"""Test mathfuc.py"""    @classmethoddefsetUpClass(cls):print"This setUpClass() method only called once."    @classmethoddeftearDownClass(cls):print"This tearDownClass() method only called once too."...

执行结果如下:

...This setUpClass() method only called once.dosomething before test.Prepare environment.adddosomething after test.Clean up....dosomething before test.Prepare environment.multidosomething after test.Clean up.This tearDownClass() method only called once too.

可以看到setUpClass以及tearDownClass均只执行了一次。

跳过某个case

如果我们临时想要跳过某个case不执行怎么办?unittest也提供了几种方法:

skip装饰器

...classTestMathFunc(unittest.TestCase):"""Test mathfuc.py"""...    @unittest.skip("I don't want to run this case.")deftest_divide(self):"""Test method divide(a, b)"""print"divide"self.assertEqual(2, divide(6,3))        self.assertEqual(2.5, divide(5,2))

执行:

...test_add (test_mathfunc.TestMathFunc)Test method add(a, b) ... oktest_divide (test_mathfunc.TestMathFunc)Test method divide(a, b) ... skipped"I don't want to run this case."test_minus (test_mathfunc.TestMathFunc)Test method minus(a, b) ... oktest_multi (test_mathfunc.TestMathFunc)Test method multi(a, b) ... ok----------------------------------------------------------------------Ran 4 testsin0.000sOK (skipped=1)

可以看到总的test数量还是4个,但divide()方法被skip了。

skip装饰器一共有三个unittest.skip(reason)、unittest.skipIf(condition, reason)、unittest.skipUnless(condition, reason),skip无条件跳过,skipIf当condition为True时跳过,skipUnless当condition为False时跳过。

TestCase.skipTest()方法

...classTestMathFunc(unittest.TestCase):"""Test mathfuc.py"""...deftest_divide(self):"""Test method divide(a, b)"""self.skipTest('Do not run this.')print"divide"self.assertEqual(2, divide(6,3))        self.assertEqual(2.5, divide(5,2))

输出:

...test_add (test_mathfunc.TestMathFunc)Test method add(a, b) ... oktest_divide (test_mathfunc.TestMathFunc)Test method divide(a, b) ... skipped'Do not run this.'test_minus (test_mathfunc.TestMathFunc)Test method minus(a, b) ... oktest_multi (test_mathfunc.TestMathFunc)Test method multi(a, b) ... ok----------------------------------------------------------------------Ran 4 testsin0.001sOK (skipped=1)

效果跟上面的装饰器一样,跳过了divide方法。

进阶——用HTMLTestRunner输出漂亮的HTML报告

我们能够输出txt格式的文本执行报告了,但是文本报告太过简陋,是不是想要更加高大上的HTML报告?但unittest自己可没有带HTML报告,我们只能求助于外部的库了。

HTMLTestRunner是一个第三方的unittest HTML报告库,首先我们下载HTMLTestRunner.py,并放到当前目录下,或者你的'C:\Python27\Lib'下,就可以导入运行了。

下载地址:

官方原版:http://tungwaiyip.info/software/HTMLTestRunner.html

灰蓝修改版:HTMLTestRunner.py(已调整格式,中文显示)

修改我们的test_suite.py:

# -*- coding: utf-8 -*-importunittestfromtest_mathfuncimportTestMathFuncfromHTMLTestRunnerimportHTMLTestRunnerif__name__ =='__main__':    suite = unittest.TestSuite()    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))withopen('HTMLReport.html','w')asf:        runner = HTMLTestRunner(stream=f,                                title='MathFunc Test Report',                                description='generated by HTMLTestRunner.',                                verbosity=2)        runner.run(suite)

这样,在执行时,在控制台我们能够看到执行情况,如下:

oktest_add(test_mathfunc.TestMathFunc)Ftest_divide(test_mathfunc.TestMathFunc)oktest_minus(test_mathfunc.TestMathFunc)oktest_multi(test_mathfunc.TestMathFunc)TimeElapsed: 0:00:00.001000

并且输出了HTML测试报告,HTMLReport.html,如图:

html report

这下漂亮的HTML报告也有了。其实你能发现,HTMLTestRunner的执行方法跟TextTestRunner很相似,你可以跟我上面的示例对比一下,就是把类图中的runner换成了HTMLTestRunner,并将TestResult用HTML的形式展现出来,如果你研究够深,可以写自己的runner,生成更复杂更漂亮的报告。

总结一下

unittest是Python自带的单元测试框架,我们可以用其来作为我们自动化测试框架的用例组织执行框架。

unittest的流程:写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,我们通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者我们可以直接通过TextTestRunner来执行用例。

一个class继承unittest.TestCase即是一个TestCase,其中以test开头的方法在load时被加载为一个真正的TestCase。

verbosity参数可以控制执行结果的输出,0是简单报告、1是一般报告、2是详细报告。

可以通过addTest和addTests向suite中添加case或suite,可以用TestLoader的loadTestsFrom__()方法。

用setUp()、tearDown()、setUpClass()以及tearDownClass()可以在用例执行前布置环境,以及在用例执行后清理环境

我们可以通过skip,skipIf,skipUnless装饰器跳过某个case,或者用TestCase.skipTest方法。

参数中加stream,可以将报告输出到文件:可以用TextTestRunner输出txt报告,以及可以用HTMLTestRunner输出html报告。

我们这里没有讨论命令行的使用以及模块级别的fixture,感兴趣的同学可以自行搜索资料学习。

作者:灰蓝蓝蓝蓝蓝蓝

链接:https://www.jianshu.com/p/38948d0d73f5

来源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容