断言
每个测试用例都要进行断言,如果没有断言或没有进行正确断言,那这条测试用例是无效的。(没有断言相当于功能测试中测试用例没有写预期结果;没有写正确的断言,相当于功能测试中测试用例预期结果写错了。)
unittest
的TestCase
类提供了一些断言方法,如下:
方法 | 检查对象 | 功能描述 |
---|---|---|
assertEqual(a,b) | a == b | 测试a和b是否相等。 如果两个值的比较结果是不相等,则测试将失败。 |
assertNotEqual(a,b) | a != b | 测试a和b是否不等。 如果两个值的比较结果是相等,则测试将失败。 |
assertTrue(x) | bool(x) is True | 测试x是否为真 |
assertFalse(x) | bool(x) is False | 测试x是否为假 |
assertIs(a,b) | a is b | 测试a和b是同一个对象 |
assertIsNot(a,b) | a is not b | 测试a和b不是同一个对象 |
assertIsNone(x) | x is None | 测试x是None |
assertIsNotNone(x) | x is not None | 测试x不是None |
assertIn(a,b) | a in b | 测试a是b的成员 |
assertNotIn(a,b) | a not in b | 测试a不是b的成员 |
assertIsInstance(a,b) | isinstance(a,b) | 测试a(对象)是b(类)的实例 |
assertNotIsInstance(a,b) | not isinstance(a,b) | 测试a(对象)不是b(类)的实例 |
上面12个断言中,最常用的是下面3个:
assertEqual(a,b)
import unittest class PlsRegister(unittest.TestCase): # 测试用例01-测试注册账号-老百 @unittest.skip("老百是系统初始化账号无需注册") def test_01_laobai(self): # self.skipTest("跳过此用例。。。") print("测试注册账号-老百") # 测试用例02-测试注册账号-nichengwe def test_02_nichengwe(self): print("测试注册账号-nichengwe") self.assertEqual(1, 1) # 测试用例03-测试注册账号-baby2016 def test_03_baby2016(self): print("测试注册账号-baby2016") self.assertEqual(1, 11)
执行结果:
$ python -m unittest test_pls_register -v test_01_laobai (test_pls_register.PlsRegister) ... skipped '老百是系统初始化账号无需注册' test_02_nichengwe (test_pls_register.PlsRegister) ... 测试注册账号-nichengwe ok test_03_baby2016 (test_pls_register.PlsRegister) ... 测试注册账号-baby2016 FAIL ====================================================================== FAIL: test_03_baby2016 (test_pls_register.PlsRegister) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/laobai/workspaces/testdemo01/unittestdemo/test_pls_register.py", line 29, in test_03_baby2016 self.assertEqual(1, 11) AssertionError: 1 != 11 ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=1, skipped=1)
assertTrue(x)
import unittest class PlsRegister(unittest.TestCase): # age = 17 # 测试用例01-测试注册账号-老百 @unittest.skip("老百是系统初始化账号无需注册") def test_01_laobai(self): # self.skipTest("跳过此用例。。。") print("测试注册账号-老百") # 测试用例02-测试注册账号-nichengwe def test_02_nichengwe(self): print("测试注册账号-nichengwe") self.assertTrue(1) # 测试用例03-测试注册账号-baby2016 def test_03_baby2016(self): print("测试注册账号-baby2016") self.assertTrue(0)
执行结果:
$ python -m unittest test_pls_register -v test_01_laobai (test_pls_register.PlsRegister) ... skipped '老百是系统初始化账号无需注册' test_02_nichengwe (test_pls_register.PlsRegister) ... 测试注册账号-nichengwe ok test_03_baby2016 (test_pls_register.PlsRegister) ... 测试注册账号-baby2016 FAIL ====================================================================== FAIL: test_03_baby2016 (test_pls_register.PlsRegister) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/laobai/workspaces/testdemo01/unittestdemo/test_pls_register.py", line 29, in test_03_baby2016 self.assertTrue(0) AssertionError: 0 is not true ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=1, skipped=1)
assertIn(a,b)
import unittest class PlsRegister(unittest.TestCase): # age = 17 # 测试用例01-测试注册账号-老百 @unittest.skip("老百是系统初始化账号无需注册") def test_01_laobai(self): # self.skipTest("跳过此用例。。。") print("测试注册账号-老百") # 测试用例02-测试注册账号-nichengwe def test_02_nichengwe(self): print("测试注册账号-nichengwe") self.assertIn("h", "hello") # 测试用例03-测试注册账号-baby2016 def test_03_baby2016(self): print("测试注册账号-baby2016") self.assertIn("a", "hello")
执行结果:
$ python -m unittest test_pls_register -v test_01_laobai (test_pls_register.PlsRegister) ... skipped '老百是系统初始化账号无需注册' test_02_nichengwe (test_pls_register.PlsRegister) ... 测试注册账号-nichengwe ok test_03_baby2016 (test_pls_register.PlsRegister) ... 测试注册账号-baby2016 FAIL ====================================================================== FAIL: test_03_baby2016 (test_pls_register.PlsRegister) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/laobai/workspaces/testdemo01/unittestdemo/test_pls_register.py", line 29, in test_03_baby2016 self.assertIn("a", "hello") AssertionError: 'a' not found in 'hello' ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=1, skipped=1)
测试报告
上面描述的一系列测试过程结束后,只是在控制台输出了简单的几行结果描述,而测试报告作为测试活动的一个重要的结果物,需要在测试完成后,出具一份有详细说明的报告。
HTMLTestRunner
HTMLTestRunner
是最古老的一款unittest的HTML格式的报告,这个报告仅支持python2(python2已停止维护多年),地址:http://tungwaiyip.info/software/HTMLTestRunner.html
下载HTMLTestRunner.py
后,需要稍作改动,以支持python3。
# 94行,import StringI0 改为: import io
# 539行,self.outputBuffer = StringI0.StringIO() 改为: self.outputBuffer = io.BytesIO()
# 631行,print >>sys .stderr, '\nTime ELapsed: %s' % (self.stopTime-seLf.startTime) 改为: print('\nTime Elapsed: %s' % (self.stopTime-self.startTime))
# 642行,if not rmap.has_key(cls): 改为: if not cls in rmap:
# 766行,uo = o.decode('latin-1') 改为: uo = e
# 772行,ue = e.decode('latin-1') 改为: ue = e
将修改后的HTMLTestRunner.py
拷贝到项目的/venv/lib/python3.9/site-packages/
目录下
HTMLTestRunner
方法有四个参数:HTMLTestRunner(stream='', verbosity='', title='', description=')
stream
是报告输出的路径+文件名.html;verbosity
是输出信息的详细程序,由于是输出到html的报告,这个参数可以省略;title
是测试报告的名称;description
是测试报告的具体描述;
import unittest
from HTMLTestRunner import HTMLTestRunner
class PlsRegister(unittest.TestCase):
# 测试用例01-测试注册账号-老百
@unittest.skip("老百是系统初始化账号无需注册")
def test_01_laobai(self):
# self.skipTest("跳过此用例。。。")
print("测试注册账号-老百")
# 测试用例02-测试注册账号-nichengwe
def test_02_nichengwe(self):
print("测试注册账号-nichengwe")
self.assertIn("h", "hello")
# 测试用例03-测试注册账号-baby2016
def test_03_baby2016(self):
print("测试注册账号-baby2016")
self.assertIn("a", "hello")
if __name__ == '__main__':
suite = unittest.TestSuite()
testcases = [PlsRegister('test_02_nichengwe'), PlsRegister('test_01_laobai'), PlsRegister('test_03_baby2016')]
suite.addTests(testcases)
fp = open('./result.html', 'wb')
runner = HTMLTestRunner(stream=fp, title='PLS自动化测试报告', description='用例执行情况:')
runner.run(suite)
fp.close()
输出的报告如下图所示:

注:由于HTMLTestRunner
测试报告过于简陋,所以基本不建议使用。
unittestreport
unittestreport
是由国内培训机构的成员设计的一款开源基于unittest
的测试报告模板。
github地址:https://github.com/musen123/UnitTestReport
a. 安装
pip install unittestreport
b. 使用
- 加载测试套件;
- 使用
unittestteport
中的TestRunner
创建一个运行对象; - 执行测试;
import unittest
import unittestreport
# 1、加载测试用例到套件中
suite = unittest.defaultTestLoader.discover('/Users/laobai/workspaces/testdemo01/unittestdemo')
# 2、创建一个用例运行程序
runner = unittestreport.TestRunner(suite,
tester='老百',
filename="report.html",
report_dir=".",
title='PLS自动化测试报告',
desc='用例执行情况',
templates=1
)
# 3、运行测试用例
runner.run()
其中templates
目前有三个,用1,2,3表示,这个示例中用的是1。生成的报告如下:

其中,用例描述,是指每个测试用例(方法)中的文档注释"""测试注册账号-老百"""
...
# 测试用例01-测试注册账号-老百
@unittest.skip("老百是系统初始化账号无需注册")
def test_01_laobai(self):
'''测试注册账号-老百'''
print("测试注册账号-老百")
...
https://unittestreport.readthedocs.io/en/latest/ ,这是unittestreport
的在线帮助文档,介绍了比较多的一些使用细节、技巧。
(类似的测试报告模板很不少,这个仅是其中的一款。由于在之后的项目中基本使用pytest
来替代unittest
,所以够用就好)
参数化
Python标准库中的unittest
自身不支持参数化测试,为了解决这个问题,可以使用第三方库来实现,主要有两个:DDT
和parameterized
DDT
DDT
,Data-Driven Test
(数据驱动测试),在同一个方法上测试不同的参数,以覆盖所有可能的预期分支的结果。它的测试数据可以与测试行为分离,被放入到文件、数据库或者外部介质中,再由测试程序读取。
DDT
是通过装饰器的形式来调用的,主要使用下面4个装饰器:
@ddt
(类装饰器,申明当前类使用ddt
框架)
@data
(函数装饰器,用于给测试用例传递数据)
@unpack
(函数装饰器,将传输的数据包解包(一般作用于元组tuple
和列表list
))
@file_data
(函数装饰器,可直接读取yaml
/json
文件)
a. 安装ddt
库
$ pip install ddt
b. 示例1
参数化前的代码
import unittest
class PlsLogin(unittest.TestCase):
# 测试用例01-测试老百登录
def test_01_laobai(self):
print("测试老百登录")
# 测试用例02-测试nichengwe登录
def test_02_nichengwe(self):
print("测试nichengwe登录")
# 测试用例02-测试baby2016登录
def test_03_baby2016(self):
print("测试baby2016登录")
if __name__ == '__main__':
unittest.main()
执行结果:
$ python pls_login_01.py
测试老百登录
.测试nichengwe登录
.测试baby2016登录
.
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
通过ddt
来进行参数化
import unittest
from ddt import ddt, data
# 使用@ddt声明这个类为ddt修饰的
@ddt
class PlsLogin(unittest.TestCase):
# ddt所使用的数据
@data('laobai','nichengwe','baby2016')
def test_login(self,login_name):
print("测试%s登录"%login_name)
if __name__ == '__main__':
unittest.main()
执行结果:
$ python pls_login_01.py
测试laobai登录
.测试nichengwe登录
.测试baby2016登录
.
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
@data
传入多个值的时候,传几个值,用例就执行几次。
c. @data
的参数可以是数字、字符串、列表、元组、字典、集合
@data(10,20)
@data(['laobai','nichengwe','baby2016'])
@data({'name':'laobai'},{'password':'123456'})
其中:
- 如果是数字或字符串,不需要使用
@unpack
解包; - 如果是元组和列表的话元组和列表,可以通过
@unpack
进行解包,参数的个数必须和解完包的值的个数一致; - 如果是字典,可以通过
@unpack
进行解包,参数的名字和个数必须和字典的键保持一致; - 如果是集合,无法进行解包;
d. 示例2
示例1中,传给data
的值为多组单个值,若需要传递多组多个值,可以使用@unpack
装饰器
import unittest
from ddt import ddt, data, unpack
@ddt
class PlsLogin(unittest.TestCase):
@data(('laobai', '123456'),('nichengwe','654321'))
@unpack
def test_login(self, login_name, password):
print("测试%s登录,使用密码为%s" % (login_name,password))
if __name__ == '__main__':
unittest.main()
执行结果:
$ python pls_login_01.py
测试laobai登录,使用密码为123456
.测试nichengwe登录,使用密码为654321
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
测试数据为多组字典形式
import unittest
from ddt import ddt, data, unpack
@ddt
class PlsLogin(unittest.TestCase):
@data({'login_name': 'laobai', 'password': '123456'}, {'login_name': 'nichengwe', 'password': '654321'})
@unpack
def test_login(self, login_name, password):
print("测试%s登录,使用密码为%s" % (login_name, password))
if __name__ == '__main__':
unittest.main()
执行j结果:
$ python pls_login_01.py
测试laobai登录,使用密码为123456
.测试nichengwe登录,使用密码为654321
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
e. 示例3
import unittest
from ddt import ddt, data, unpack
@ddt
class TestDemo01(unittest.TestCase):
@data([1, 2, 3], [2, 3, 5], [3, 4, 7])
@unpack
def test_add(self, data1, data2, exceptdata):
sum = data1 + data2
self.assertEqual(sum, exceptdata)
if __name__ == '__main__':
unittest.main()
执行结果:
$ python test_demo01.py -v
test_add_1__1__2__3_ (__main__.TestDemo01) ... ok
test_add_2__2__3__5_ (__main__.TestDemo01) ... ok
test_add_3__3__4__7_ (__main__.TestDemo01) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
上面的多个例子中的数据,都是写在程序的py文件中,相当于写死了测试数据。这里可以使用ddt
的file_data
读取外部文件的方式来获取测试数据
import unittest
from ddt import ddt, data, unpack, file_data
@ddt
class TestDemo01(unittest.TestCase):
@file_data('data.json')
@unpack
def test_add(self, value):
data1, data2, exceptdata = value.split('|')
sum = int(data1) + int(data2)
self.assertEqual(sum, int(exceptdata))
if __name__ == '__main__':
unittest.main()
执行结果:
$ python test_demo01.py -v
test_add_1_1_2_3 (__main__.TestDemo01)
test_add_1_1_2_3 ... ok
test_add_2_2_3_5 (__main__.TestDemo01)
test_add_2_2_3_5 ... ok
test_add_3_3_4_7 (__main__.TestDemo01)
test_add_3_3_4_7 ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Parameterized
Parameterized
是Python的一个参数化库,使用时先导入Parameterized
库下面的parameterized
类。再通过@parameterized.expand()
来装饰测试用例的方法即可。
a. 安装parameterized
$ pip install parameterized
b. 示例
import unittest
from parameterized import parameterized
class TestDemo02(unittest.TestCase):
@parameterized.expand([(3, 1), (10, 5), (1.1, 1.0)])
def test_values(self, first, second):
self.assertTrue(first > second)
if __name__ == '__main__':
unittest.main()
执行结果:
$ python test_demo02.py -v
test_values_0 (__main__.TestDemo02) ... ok
test_values_1 (__main__.TestDemo02) ... ok
test_values_2 (__main__.TestDemo02) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
DDT
的参数化能力要强于Parameterized
,所以更建议使用DDT