python单元测试unittest
Whisper Lv4

由示例开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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原名为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。

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

常用命令行

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

1
2
3
python -m unittest test_module1 test_module2  #同时运行多个测试模块
python -m unittest test_module.TestClass #运行指定测试类
python -m unittest test_module.TestClass.test_method #运行指定测试类下的测试方法

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

1
python -m unittest -v test_module

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

开始写测试用例

Testcase类

1
class unittest.TestCase(methodName='runTest')

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

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

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

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

1
testCase = DefaultWidgetSizeTestCase()

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

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

测试固定装置fixtures

方法固定装置

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

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

    注意:如果setup运行抛出错误,则测试用例代码则不会执行。但是,如果setpu执行成功,不管测试用例是否执行成功都会执行teardown。

Class固定装置:

必须为类实现

  • setUpClass():一个类方法在单个类测试之前运行。setUpClass作为唯一的参数被调用时,必须使用classmethod()作为装饰器。
  • tearDownClass():一个类方法在单个类测试之后运行。setUpClass作为唯一的参数被调用时,必须使用classmethod()作为装饰器。
1
2
3
4
5
6
7
8
9
import unittest
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls): #这里的cls是当前类的对象
cls._connection = createExpensiveConnectionObject()

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

Module固定装置:

必须为方法实现

1
2
3
4
5
def setUpModule():
createConnection()

def tearDownModule():
closeConnection()

setUpClass中的值在testcase中使用

将获取的值赋值给类属性,然后再testcase中直接使用self调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Login(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.s = requests.Session()
url = "http://xxxxxx"
data = {
"account": apis.LOGIN_ACCOUNT,
"password": apis.LOGIN_PASSWORD
}
res = cls.s.post(url, json=data)
assert res.status_code == 200, res.content

def test_get_list(self):
url = "xxxx"
res = self.s.get(url)
assert res.status_code == 200, res.content

测试套件组织测试代码

根据不同的业务需求,可能需要在不同的module中选择某一个或者多个测试用例来执行,此时可以根据不同的测试场景来对测试用例进行组织,
有多种方式添加用例到测试套件中,如下:

使用TestSuite添加

将testcase添加到TestSuite实例的常用方法:

方法1 TestSuite实例.addTest(test): 添加一个测试用例或者测试套件到测试套件中。

1
2
3
ts = unittest.TestSuite() #创建一个测试套件实例
ts.addTest(SomeTestClass('test_default_size')) #添加测试类SomeTestClass的test_default_size到测试套件中
ts.addTest(SomeTestClass('test_resize')) #添加测试类SomeTestClass中的test_resize到测试套件中

方法2 TestSuite实例.addTests(tests): 批量添加测试用例或者测试套件到测试套件中。相当于多次调用addTest()来添加。

方法3 或者在初始化实例的时候直接写入:

1
ts = unittest.TestSuite(tests=())

使用WidgetTestCase,将该测试套件直接返回

1
2
3
4
5
def suite():
suite = unittest.TestSuite()
suite.addTest(WidgetTestCase('test_default_size'))
suite.addTest(WidgetTestCase('test_resize'))
return suite

使用更简洁的写法:

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

使用TestLoader导入整个Class和Module中的用例

TestLoader用来从clases和modules中快速load所有测试用例,
并创建test suites实例,将测试用例添加到该实例,返回该实例

1
class unittest.TestLoader

该类有以下方法:

  • loadTestsFromTestCase(testCaseClass):
  • loadTestsFromModule(module)
1
suite = unittest.TestLoader().loadTestsFromTestCase(WidgetTestCase)

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

在测试套件中添加测试套件

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

注意:删改添加到了list中的测试套件还需要在经过TestSuite的加工才能运行。

执行测试

运行测试套件

使用unittest.TextTestRunner类的实例直接运行(run)测试套件。
上面制作测试套件的方法已经熟悉了,接下来通过TextTestRunner的实例调用run(测试套件)可运行测试套件中的所有测试用例。

1
2
3
4
if __name__ == "__main__":
some_suite = unittest.TestLoader().loadTestsFromTestCase(SomeTestClass)
runner = unittest.TextTestRunner()
runner.run(some_suite)

用例执行顺序

unittest的执行的顺序是根据测试用例的名称来的。0-9 A-Z a-z来排序。
每个测试用例之间的数据最好不要相互依赖,如果一定要相互依赖则调整用力的执行顺序来保证运行的正确性。

要改变运行顺序,可以使用addTest通过测试套件添加测试用例的顺序,运行测试。
示例:test_order.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import unittest

class Order(unittest.TestCase):
def test_c(self):
print("ccccccc")
self.assertEqual(3, 3)

def test_a(self):
print("aaaaaaa")
self.assertEqual(1, 1)

def test_b(self):
print("bbbbbbb")
self.assertEqual(2, 2)

运行输出

1
2
3
aaaaaaa
bbbbbbb
ccccccc

可以看到上面代码按照名称顺序执行。

通过添加suite添加执行后,顺序仍然是按照名称顺序执行的

1
2
3
4
5
if __name__ == "__main__":
ts = unittest.TestSuite()
ts.addTest(Order('test_c'))
ts.addTest(Order('test_a'))
ts.addTest(Order('test_b'))

所以,要固定测试用例的顺序可以在名称上加上字母或者数字test_a_login, test_b_logout等,虽然这样看起来显得比较随意,但是这正是unittest力求简单的地方。

常用断言方法

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

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

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

如果给定的assertion通过了,那么测试会执行下一行代码。
如果给定的assertion没有通过,测试会暂停并且生成错误信息。
unittest库提供所有标准的xUnit assert方法。下面列出较重要方法的一部分:

元素相等

assertEqual(a,b [,msg]): 检测a==b,这个方法检查a是否等于b,常用语检测元素属性等。如:

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

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

1
2
3
4
5
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.

检测异常

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

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

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

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

逻辑运算

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

正则检测匹配text

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

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

跳过测试和预期的失败

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

1
2
3
4
@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(),运行时 ,如果测试失败,测试不算作失败。

使用HTMLTestRunner生成报告

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

  1. 首先需要下载.py文件:HTMLTestRunner(这里试用的时候为0.8.2版本)
  2. 下载后放入python安装目录的lib文件夹下面,如果使用的为虚拟环境则要放在虚拟环境下面
  3. 打开终端进入python交互模式导入HTMLTestRunner ,如果无导入错误显示,则说明添加成功
    import HTMLTestRunner

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

help()查看类帮助信息

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

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

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

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

报告中显示用例的注释

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

1
2
>>> import HTMLTestRunner
>>> help(HTMLTestRunner)

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

python 时间函数:

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

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

1
2
3
4
5
6
7
8
9
10
11
t_suites = unittest.TestSuite(some suite)

now = time.strftime("%Y-%m-%d %H_%M_%S")
report_path = 'report/' + now + '_result.html'
with open(report_path, 'wb') as fp:
runner = HTMLTestRunner.HTMLTestRunner(
stream=fp,
title='测试报告',
verbosity=2
)
runner.run(t_suites)

参数verbosity默认为1,如果设置大于1则会在测试过程中输出测试相关信息。
生成的文件名称以2016-03-09_17_37_26_result.html格式出现。

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

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

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

python3使用需要修改的地方

如果当前环境使用的是python3,则需要修改HTMLTestRunner.py的源文件,该文件基于py2.7编写,有的方法在python3中已经修改或弃用

  1. 第94行的import StringIO修改import io
  2. 第118行的self.fp.write(s)修改self.fp.write(bytes(s,'UTF-8'))
  3. 第539行的self.outputBuffer = StringIO.StringIO()修改self.outputBuffer = io.StringIO()
  4. 第642行的if not rmap.has_key(cls)修改if not cls in rmap
  5. 第767行的uo = o.decode('latin-1')修改uo = o
  6. 第769行的uo = o修改uo = o.decode('utf-8')
  7. 第773行的ue = e.decode('latin-1')修改ue = e
  8. 第775行的ue = e修改ue = e.decode('utf-8')
  9. 第631行的print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)``修改print(sys.stderr, ‘\nTime Elapsed: %s’ % (self.stopTime-self.startTime))``

修改后的py文件可直接用py.