python单元测试unittest

整体结构概览

unittest原名为PyUnit,是由java的JUnit衍生而来。对于单元测试,需要设置预先条件,对比预期结果和实际结果。

整体结构:
unittest库提供了test cases, test suites, test fixtures,test runner:

  1. test case :通过继承TestCase类,我们可以创建一个test,或者一组tests,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。
  2. test suites : 测试套件,多个测试用例集合在一起,TestSuite也可以嵌套TestSuite。
  3. test fixtures : setup + test case + teardown结构
  4. TestLoader:用来加载TestCase到TestSuite中,其中的方法从各个地方寻找TestCase,创建它们的实例,然后add到TestSuite中,返回一个TestSuite实例。
  5. test runner:执行测试用例,其中的run()会执行TestSuite/TestCase。
  6. TextTestResult:测试的结果会保存到TextTestResult实例中,包括运行用例数,成功数,失败数等。

写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,整个过程集成在unittest.main模块中。

注:
所有的测试函数以test开头,test_XXX。

简单的示例:

import unittest
class TestStringMethods(unittest.TestCase):
    def test_upper(self):
        self.assertEqual('foo',upper(),'FOO')
    def test_isupper(self):
        self.assertEqualTure('FOO'.isupper())
        self.assertFalse('Foo'.isupper())
    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(),['hello','world'])
        with self.assertEqualRaise(TypeError):
            s.slipt(2)
if __name__ == '__main__': #unittest.main:为测试提供了入口。
    unittest.main()
    
#运行结果:
...
----------------------------------------------------------------------
Ran 3 tests in 0.064s
OK

其他与unittest类似的单元测试库:nose, pytest。

命令行

从命令行中可以运行单元测试的模块,甚至单独的测试方法

python -m unittest test_module1 test_module2  #同时测试多个module
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method

显示更详细的测试结果的说明使用-vflag:

python -m unittest -v test_module

查看所有的命令行选项使用命令python -m unittest -h

TestCase

Testcase类

class unittest.TestCase(methodName='runTest')

TestCase的实例是最小的可测试单元。 testcase 是由unittest的TestCase类的实例表示的。要编写自己的测试用例必须继承TestCase类,或者使用FunctionTestCase。且Testcase类提供了各种assert的方法来检测预期结果和实际结果。

看下面的例子(创建一个测试类DefaultWidgetSizeTestCase):

import unittest
class DefaultWidgetSizeTestCase(unittest.TestCase): #unittest.TestCase表示某个测试函数
    def runTest(self):
        widget = Widget('The widget')
        self.assertEqual(widget.size(), (50, 50), 'incorrect default size')

创建实例
建立这样一个测试用例的一个实例,使用该类的构造函数,且不带参数(这样会执行所有的测试方法):

testCase = DefaultWidgetSizeTestCase()

我们也可以创建多个实例,且每个实例之间都是独立的。
当我们需要对不同的实例执行不同的测试方法时,我们可以将要执行的测试用例方法在创建实例时通过类参数传入。

#建了两个WidgetTestCase的实例,每个实例只运行WidgetTestCase类中的一个测试方法(通过参数传入)
defaultSizeTestCase = WidgetTestCase('test_default_size')
resizeTestCase = WidgetTestCase('test_resize')

常用断言方法

unittest库提供了很多实用方法来检测程序运行的结果和预期。

包括三种类型的方法,每一种都覆盖了典型的类型,比如:

  1. 检查相等值
  2. 逻辑比较
  3. 异常

如果给定的assertion通过了,那么测试会执行下一行代码。
如果给定的assertion没有通过,测试会暂停并且生成错误信息。
unittest库提供所有标准的xUnit assert方法。下面列出较重要方法的一部分:
检测元素是否相等:
assertEqual(a,b [,msg]): 检测a==b,这个方法检查a是否等于b,常用语检测元素属性等。如:

assertEqual(element.text, "10")
assertNotEqual(a,b [,smg]):检测a!==b.

检测表达式是否为Ture,或者 False:

assertTrue(x [,msg])  #检测bool(x) is True.
#如:检测某个元素是否在页面上
#assertTrue(element.is_dispalyed())
assertFalse(x [,msg])  #检测bool(y) is Flase.
assertIsNot(a, b [,msg])  #检测 a is not b.

检测异常

assertRaises(exc, fun, *args, **kwds)
assertRaiseRegexp(exc, r, fun, *args, **kwds)

最有可能使用这些方法的是NoSuchElementFoundexception

检测数字,先四舍五入到指定的小数位数后再进行比较

assertAlmostEqual(a, b)   #检测round(a-b,7)==0
assertNotAlmostEqual(a, b)   #检测round(a-b,7)!=0

逻辑运算

assertGreater(a, b)  # 检测a > b.
assertGreaterEqual(a ,b)  # 检测a >= b.
assertLess(a, b)     #检测a < b.
assertLessEqual(a, b)   #检测a <= b.

正则表达式,检测正则是否匹配给定的text

assertRegexpMatches(s, r)       #检测r.search(s).
assertNotRegexpMatches(s, r)  #检测not r.search(s).

检测字符串
assertMultiLineEqual(a, b) #检测string
检测lists之间是否相等
assertListEqual(a, b) #检测lists
fail()无条件失败,用户自定义
fail()

Test fixtures

方法固定装置:

如果要对一个模块中的每一个测试函数都做同样的初始化操作和结尾清除等操作,那么创建n个测试用例就得写n遍一样的代码,为了减少重复的代码,可以使用下面两个函数:

  • setUp(): 每次执行测试用例之前调用。无参数,无返回值。该方法抛出的异常都视为error,而不是测试不通过。没有默认的实现。
  • tearDown(): 每次执行测试用例之后调用。无参数,无返回值。测试方法抛出异常,该方法也正常调用,该方法抛出的异常都视为error,而不是测试不通过。只用setUp()调用成功,该方法才会被调用。没有默认的实现。
    通过setup 和 tesrDown组装一个module成为一个固定的测试装置。
    注意:如果setup运行抛出错误,则测试用例代码则不会执行。但是,如果setpu执行成功,不管测试用例是否执行成功都会执行teardown。

Class固定装置:

必须为类实现
setUpClass():一个类方法在单个类测试之前运行。setUpClass作为唯一的参数被调用时,必须使用classmethod()作为装饰器。
tearDownClass():一个类方法在单个类测试之后运行。setUpClass作为唯一的参数被调用时,必须使用classmethod()作为装饰器。

import unittest
class Test(unittest.TestCase):
    @classmethod
    def setUpClass(cls): #这里的cls是当前类的对象
        cls._connection = createExpensiveConnectionObject()

    @classmethod
    def tearDownClass(cls):
        cls._connection.destroy()

Module固定装置:

必须为方法实现

def setUpModule():
    createConnection()

def tearDownModule():
    closeConnection()

使用Text Suite组织测试代码

unittest.TestSuite(tests=())

该类聚合测试用例和测试套件,运行一个TestSuite实例遍历套件,和单独运行每个testcase是相同的。TestSuite对象的行为就像TestCase对象,除了他们不实现一个测试。
一些方法可以将testcase添加到TestSuite实例:
addTest(test):Add a TestCase or TestSuite to the suite.
addTests(tests):添加所有的tests从可迭代的TestCase和TestSuite实例测试套件。这相当于迭代调用addTest()来添加每个元素。

根据不同的业务可能需要在不同的module中选择某一个或者几个测试用例,此时可以根据每个测试实例的特征对测试方法打包:

widgetTestSuite = unittest.TestSuite() #创建一个测试套件实例
widgetTestSuite.addTest(WidgetTestCase('test_default_size')) #添加测试用例到套件,抽取WidgetTestCase类中的test_default_size测试用例添加到testsuite
widgetTestSuite.addTest(WidgetTestCase('test_resize'))  #添加测试用例到套件,抽取WidgetTestCase类中的test_resize测试用例添加到testsuite

可以返回该测试套件的get入口:

def suite():
    suite = unittest.TestSuite()
    suite.addTest(WidgetTestCase('test_default_size'))
    suite.addTest(WidgetTestCase('test_resize'))
    return suite

或者更简洁的写法:

def suite():
    tests = ['test_default_size', 'test_resize']
    return unittest.TestSuite(map(WidgetTestCase, tests))

测试套件中也可以包含测试套件:

suite1 = module1.TheTestSuite()
suite2 = module2.TheTestSuite()
alltests = unittest.TestSuite([suite1, suite2])

使用TestLoader

class unittest.TestLoader

TestLoader 用来从clases和modules创建test suites,通常也需要创建一个该类的实例,unittest模块提供了一个实例,可以作为unittest.defaultTestLoader共享。使用一个子类或实例,允许定制可配置属性。
该类有以下方法:
loadTestsFromTestCase(testCaseClass):
loadTestsFromModule(module):返回一个给定的模块中所有测试用例,打包成一个套件返回。
.....

该类创建一个testsuites然后加载一个module并执行其中所有的测试用例,执行的顺序是根据测试用例的名称来的。

suite = unittest.TestLoader().loadTestsFromTestCase(WidgetTestCase) #执行WidgetTestCase中所有的测试用例

你可以将测试用例和测试套件放在一个module中,最好是分开放置,方便重构管理,如果测试策略改变了,也方便维护。

运行添加的测试套件:

if __name__ == "__main__":
    login_test_suite = unittest.TestSuite()
    login_test_suite.addTest(Login('test_xxx'))
    login_test_suite.addTest(Login('test_xxx2'))
    runner = unittest.TextTestRunner()
    runner.run(login_test_suite)

跳过测试和预期的失败

Unittest支持跳过单个的测试方法甚至整个类的测试。使用 skip() decorator来设置特定跳过的条件,如指定操作系统不执行该测试。

    @unittest.skipIf(mylib.__version__ < (1, 3), "not supported in this library version")
    def test_format(self):
        # Tests that work for only a certain version of the library.
        pass

执行的时候如果满足跳过条件,控制台会将后面的说明打印出来,并跳过该测试用例。跳过类也是相似的写法。
也可以自定义skipping装饰器。

定义预期的失败使用unittest.expectedFailure(),运行时 ,如果测试失败,测试不算作失败。

执行测试

用例执行顺序

unittest的执行的顺序是根据测试用例的名称来的。名称是按照英文字母排序。a-z。
每个测试用例之间的数据最好不要相互依赖,如果一定要相互依赖则调整用力的执行顺序来保证运行的正确性。
要改变运行顺序,可以使用addTest通过测试套件添加测试用例的顺序,运行测试。

使用HTMLTestRunner生成报告

unittest本身并不具备这个功能,需要使用HTMLTestRunner库
使用步骤:

  1. 首先需要下载.py文件:HTMLTestRunner
  2. 下载后放入python安装目录的lib文件夹下面。
  3. 打开终端进入python交互模式导入HTMLTestRunner ,如果无导入错误显示,则说明添加成功
    import HTMLTestRunner

注意:python3的stringIO有变化,使用该HTMLTestRunner会报错ImportError: No module named 'StringIO'。这里使用python2.7试验。

1 help()查看类帮助信息

为了说明在报告中显示用例的注释,这里先了解下help()帮助函数。
help帮助信息息都是代码中通过 '''xxx ''' 或者 """xxx""" 注释符号来标注的。
如我们自定义一个模块helpin.py,并将其放入python目录的lib文件夹下面:

#coding=utf-8
u'''
这是help能够显示的帮助信息
'''
def add(a):
    u"""这个函数用来输入a的值"""
    print 'a'

在cmd命令行中,进入python27的交互模式,导入后通过help查看帮助信息:

>>> import helpin
>>> help(helpin)
Help on module helpin:
NAME
    helpin - 这是help能够显示的帮助信息
FILE
    e:\python27\lib\helpin.py
FUNCTIONS
    add(a)
        这个函数用来输入a的值

2 报告中显示用例的注释

给报告中的每个测试用例添加注释,来说明该测试用例是用来干什么的,非常有必要。
通过help()函数来查看HTMLTestRunner类的详细说明:

>>> import HTMLTestRunner
>>> help(HTMLTestRunner) 

可以看到该类的注释说明,一个好的开源的框架应该有这样的规范。

这里在每个测试函数的下方添加上注释:

    def test_equal(self):
        u'''这里是测试a和b的两个值是否相等'''
        a = 1
        b = 2
        self.assertEqual(a,b)

运行后,打开生成的html文件可以看到,每个测试用例函数的后面有该用例的注释。

3 动态生成需要多次执行的测试报告文件名称

如果一个测试套件需要多次执行,如果每次执行不去修改测试报告.html的文件名,则每次执行后会将之前的报告覆盖。如果想要保存每次执行的结果而不手动修改报告名称。这里可以使用动态生成文件名的方法。具体的生成可以根据需要来写动态生成的代码。

动态时间

python 时间函数:

  1. time.time() 获取当前时间戳。
  2. time.ctime() 当前时间的字符串形式。
  3. time.localtime() 当前时间的 struct_time 形式。
  4. time.strftime() 用来获得当前时间,可以将时间格式化为字符串。

修改之前的代码,将文件名字中添加上当前获取到的时间:

#获取当前时间
now = time.strftime("%Y-%m-%d %H_%M_%S")


#定义报告文件存放位置,这里将结果放在桌面
outfile = open('C:\\Users\Administrator\\Desktop\\'+ now + '_result.html', "w")

生成的文件名称以2016-03-09_17_37_26_result.html格式出现。

4 集成多个测试结果

目前测试报告只集成到了单个测试文件中,我们的最终目的是将其集成到一个文件中。
之前是通过testunit.addTests()函数逐个添加。
现在通过unittest.defaultTestLoader.discover()方法循环遍历添加测试用例。
最后通过runner.run()方法运行整个测试用例集。

猜你喜欢

转载自www.cnblogs.com/for-you/p/9258699.html