1. 数据驱动
1.1 测试数据参数化
参数化测试 是一种 “ 数据驱动测试 “ (Data-DrivenTest),在同一个方法上测试不同的参数,以覆盖所有可能的预期分支的结果。它的测试数据可以与测试行为分离,被放入到文件、数据库或者外部介质中,再由测试程序读取。
Python 标准库中的 unittest 自身不支持参数化测试,为了解决这个问题,有人专门开发了两个库:一个是 ddt,一个是 parameterized。
数据驱动思想:数 据和用例进行分离,通过外部数据去生成测则试用例。
1.1.1 Ddt 库
DDT 正好是 “Data-Driven Tests” (数据驱动测试) 的缩写。安装方式:
data
然后我们直接来看个案例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import unittest from ddt import ddt, data, unpack
@ddt class MyTest(unittest.TestCase): @data((3, 1), (1, 0), (1.2, 1.0)) @unpack def test_values(self, first, second): print(first, second) self.assertTrue(first > second)
if __name__ == '__main__': unittest.main(verbosity=2)
|
运行后可以发现上面代码通过参数化测试分离成了 3 条测试用例,但我们不用写重复的三段代码。

需要注意的是,@data 中传入参数的变量数量,要能与下面测试用例函数的传入参数数量保持一致。
我们再来看一个案例,下面这个案例 @data 中测试的参数就有 4 种,因此在下面函数的传入参数中也要定义四个变量来接受参数。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import unittest from ddt import ddt, data, unpack
@ddt class MyTest(unittest.TestCase): @data([1, 2, 3, 6], [2, 3, 4, 9], [3, 4, 5, 12]) @unpack def test_add(self, testdata1, testdata2, testdate3, exceptdata): sum = testdata1 + testdata2 + testdate3 self.assertEqual(sum, exceptdata)
if __name__ == '__main__': unittest.main(verbosity=2)
|
@data 中传入的参数可以是以元组的形式,也可以是以列表的形式,总之要以序列的形式传入。

file_data
除了通过上面通过 data 的方式进行参数化外,我们还可以使用 file_data 装饰器来解析一个方式参数的文件。例如,我现在创建一个 json 文件,并往里面写入参数数据。
1 2 3 4 5 6
| [ "1|2|3", "3|2|5", "3|5|8", "5|5|10" ]
|
接着,我就可以通过 file_data 来读取这个 json 文件中的数据,然后拆分成多条用例使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import unittest from ddt import ddt, file_data, unpack
@ddt class MyTest(unittest.TestCase): @file_data('data.json') @unpack def test_sum(self, value): a, b, sum = value.split('|') print(a, b, sum) result = int(a) + int(b) self.assertEqual(result, int(sum))
if __name__ == '__main__': unittest.main(verbosity=2)
|

1.1.2 Parameterized 库
参数化驱动还有另一个库,还是需要通过 pip 先进行下载:
1
| pip install parameterized
|
而这个库使用起来则更加便捷一些,但不能像 ddt 的 file_data 装饰器一样去解析 json 等文件。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import unittest from parameterized import parameterized
class MyTest(unittest.TestCase): @parameterized.expand([(3, 1), (1, 0), (1.5, 1.0)]) def test_values(self, first, second): self.assertTrue(first > second)
if __name__ == '__main__': unittest.main(verbosity=2)
|

1.2 Excel 存储测试数据
Python 可以使用 xIrd 库来读取 Excel 文件(对 xls,xlsx 格式文件都可以读取),使用 xlwt 库来生成 Excel 文件(只支持到 excel 2003,即 xls 文件),另外 openpyxl 库同时支持读、写 excel 文件,且主要针对 Excel2007 之后的版本(.xlsx)。
我们这里重点介绍一下最常用的 openpyxl 库的使用,首先还是需要先通过 pip 进行下载:
我们可以将解析 excel 文件的能力都存放到一个 parse_excel.py 文件中,方便我们调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from openpyxl import load_workbook
class ParseExcel(object): def __init__(self, excelPath, sheetName): print(excelPath, sheetName) try: self.wb = load_workbook(excelPath) print("File loaded successfully.") except Exception as e: print(f"Error loading file: {e}") self.sheet = self.wb[sheetName] self.maxRowNum = self.sheet.max_row
def getDatasFromSheet(self): dataList = [] for line in self.sheet.rows: tmpList = [] tmpList.append(line[0].value) tmpList.append(line[1].value) dataList.append(tmpList) return dataList[1:]
|
上面代码获取的数据,假设只有两列值。

ddt 的 file_data 装饰器是无法解析 excel 文件的。
因此,我们可以在其它文件中,通过调用上面封装好的函数,以列表的形式获取到 excel 中的数据,然后传入 ddt 的 data 装饰器中,从而转化成参数执行用例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import unittest from ddt import ddt, data from parse_excel import ParseExcel
excelPath = r'测试数据.xlsx' sheetName = '参数表' excel = ParseExcel(excelPath, sheetName)
@ddt class TestDemo(unittest.TestCase): @data(*excel.getDatasFromSheet()) def test_print(self, data): username, password = tuple(data) print(username, password)
if __name__ == '__main__': unittest.main(verbosity=2)
|

@unpack 装饰器:
通过上面代码可以发现,这次并没有使用 unpack 装饰器,这是因为这里我们不需要通过 unpack 进行参数的拆分。而是将参数全部传入到第一个 data 参数中,自行通过代码处理这个数据,将其拆分成 username 和 password 变量。
也就是说,如果不加 unpack 装饰器,则 data 装饰器获取到的数据都会传入到第一个参数中,不管你下面函数的入参定义了几个,都只会传入第一个。反之,加了 unpack 装饰器,则会自动与定义的入参一一对应起来。
我们可以对上面的代码加上 unpack 装饰器,但最终执行的结果还是一样的,只是 unpack 会自动帮我们拆分参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import unittest from ddt import ddt, data, unpack from parse_excel import ParseExcel
excelPath = r'测试数据.xlsx' sheetName = '参数表' excel = ParseExcel(excelPath, sheetName)
@ddt class TestDemo(unittest.TestCase): @data(*excel.getDatasFromSheet()) @unpack def test_print(self, username, password): print(username, password)
if __name__ == '__main__': unittest.main(verbosity=2)
|
2. Mock 接入
2.1 Mock 背景
在一些特殊场景下,我们一些服务可能会比较不稳定或者还没有开发完,会出现一些不可控的因素,而上游急着需要调用下游的这些服务来观察自身服务是否符合预期。
那么我们可以通过 Mock 的方式,Mock 出下游(也就是通过模拟的方式,实现一个下游的替身出来),从而可以使用 Mock 的替身提前测试自身服务。


常见 Mock 适用场景:
- 前后端联调:当前端需要的接口后端还在开发,就可以使用 mock 模拟后端返回我们需要的数据。
- 单元测试:当我们所要测试的接口还未开发完,而我们还要完成测试用例代码的编写,我们可以先模拟这个接口进行测试。
- 屏蔽无关的真实服务:专注于要测试服务本身,而其它服务又不在本次测试范围内,则可以使用 mock 只模拟出我们关注的服务。
- 依赖第三方接口:当我们的接口需要调用第三方,而第三方接口我们还未得到权限或未购买时,可使用 mock 模拟进行测试。
注意 mock 只适合用于临时的测试,而不能代替真实场景的测试。
例如,我现在有个服务需要调用下游的支付服务,通过这个下游服务的返回值来决定之后该怎么执行:
- 当返回值状态码为 200,则表示支付成功,我当前服务就可以执行后续逻辑
- 当返回值状态码为 500,则表示支付失败,需要做一些异常的处理逻辑
如果此时下游的支付服务出现问题,或者没有开发完毕,无法提供这个返回值,而我此时有想验证一下我后续代码的处理逻辑,则可以通过 Mock 的方式模拟返回值的状态码,从而可以执行后续逻辑并进行观察。

2.2 Mock 使用
如果你的团队技术基础很好,开发能力很强,那么我建议你用对应语言的 Mock 框架,例如 Java 语言的 Mockito (https://github.com/mockito/mockito)框架和 Python 语言的 mock 框架(https://pypi.org/project/mock/)。
如果你的团队技术基础相对比较薄弱,那么我推荐你看看 moco,这个框架在开发 Mock 服务的时候,提供了一种不需要任何编程语言的方式,你可以通过撰写它约束的 Json 建立报务,并通过命令启动对应的服务,这就可以快速开发和启动运行你需要的 Mock 服务(项目地址:https://github.com/dreamhead/moco/)。
2.2.1 unittest.mock 类
官方文档:https://docs.python.org/dev/library/unittest.mock.html
Mock 类库是一个专门用于在 unittest 过程中制作(伪造)和修改(篡改)测试对象的类库,制作和修改的目的是避免这些对象在单元测试过程中依赖外部资源(网络资源,数据库连接,其其它服务以及耗时过长等)。
使用前须知
- Python2.x 的版本,mock 是一个独立的模块,需要用 pip 安装
- 从 Python3.3 以后的版本,mock 已经合并到 unittest 模块中了,是 unittest 单元测试的一部分,直接导入过来就行
1
| from unittest import mock
|
案例一
直接举一个简单的例子,下面这段代码的 send_url 函数中的地址不连通,因此我们可以使用 mock 的方式模拟 send_url 的返回值。
1 2 3 4 5 6 7 8 9 10
| from unittest import mock import requests
def send_url(): url = 'http://127.0.0.1:5000/helloworld' return requests.get(url)
send_url = mock.Mock(return_value='hello world') response = send_url() print(response)
|

如果我们想在 mock 的基础上,调用回真实的函数,那么就可以在 Mock 方法中,添加一个 side_effect 属性,让其指向想调用的真实函数,这样其实就等同于直接调用这个真实的函数了。
1 2 3 4 5 6 7 8 9 10
| from unittest import mock import requests
def send_url(): url = 'http://127.0.0.1:5000/helloworld' return requests.get(url)
send_url = mock.Mock(return_value='hello world', side_effect=send_url()) response = send_url() print(response)
|
案例二
mock 里面另一种实现方式,即 patch 装饰器的使用。patch() 作为函数收装饰器,将会创建模拟,并将其传递到装饰函数。
而这种方法,一般适用于单元测试当中,例如现在 functions.py 文件中我有一段功能函数的代码。
1 2 3 4 5 6 7
| def add_and_multiply(x, y): addition = x + y multiple = multiply(x, y) return (addition, multiple)
def multiply(x, y): return x * y
|
而我在单元测试代码中,则可以针对上面的两个函数功能进行测试。
1 2 3 4 5 6 7 8 9 10
| import unittest import functions
class MyTestCase(unittest.TestCase): def test_add_and_multiply(self): x = 3 y = 5 addition, multiple = functions.add_and_multiply(x, y) self.assertEqual(8, addition) self.assertEqual(15, multiple)
|
但如果有一天这个 multiply 函数变得不稳定,可能返回的结果会有差错,那么我单元测试中的代码就会报错。
为了防止这种情况的发生,我们可以 mock multiply 函数的返回值,例如下面我将返回值始终 mock 成 15,那么我这个单元测试的代码就不会受到 multiply 函数不稳定的影响了,从而可以专注于当前代码逻辑的测试。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import unittest import functions from unittest.mock import patch
class MyTestCase(unittest.TestCase): @patch("functions.multiply") def test_mock_add_and_multiply(self, mock_multiply): x = 3 y = 5 mock_multiply.return_value = 15 addition, multiple = functions.add_and_multiply(x, y) self.assertEqual(8, addition) self.assertEqual(15, multiple)
|
案例三
现在假设有一个支付服务的场景,在 pay.py 文件中,存放着支付场景的处理代码,但是 pay 函数功能目前还未开发完成,不过目前明确知道该接口的返回数据是怎么定义的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| def pay(): ''' 假设这里是一个支付的功能,未开发完 支付成功返回:{"result":"success", "msg":"支付成功"] 支付失败返回:{"result":"fail", "msg":"余额不足"} ''' pass
def pay_status(): ''' 根据支付的结果 success or fail,判断跳转哪个页面 ''' result = pay() try: if result["result"] == "success": return "支付成功" elif result["result"] == "fail": return "支付失败" else: return "未知错误异常" except: return "Error,服务端返回异常!"
|
那么我们就可以模拟未开发完的接口的返回数据,从而可以检测相应返回数据的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import unittest from unittest import mock import pay
class TestPayStatus(unittest.TestCase): def test_01(self): '''测试支付成功场景''' pay.pay = mock.Mock(return_value={"result": "success", "msg": "支付成功"}) status = pay.pay_status() print(status) self.assertEqual(status, "支付成功")
def test_01(self): '''测试支付成功场景''' pay.pay = mock.Mock(return_value={"result": "fail", "msg": "余额不足"}) status = pay.pay_status() print(status) self.assertEqual(status, "支付失败") if __name__ == "__main__": unittest.main()
|
同样,我们也可以使用 patch 装饰器来 mock 数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import unittest from unittest import mock import pay
class TestPayStatus(unittest.TestCase): @mock.patch("pay.pay") def test_01(self, mock_pay): '''测试支付成功场景''' mock_pay.return_value = {"result": "success", "msg": "支付成功"} status = pay.pay_status() print(status) self.assertEqual(status, "支付成功")
@mock.patch("pay.pay") def test_02(self, mock_pay): '''测试支付成功场景''' mock_pay.return_value = {"result": "fail", "msg": "支付失败"} status = pay.pay_status() print(status) self.assertEqual(status, "支付失败") if __name__ == "__main__": unittest.main()
|
2.2.2 Mock 技术栈选型
- 简单是第一要素,需要将学习成本降到最低
- 处理速度比完美的 mock 服务更重要
- mock 服务要能轻量化启动,并且容易销毁
3. 测试报告
Unittest 测试默认结果输出是比较简单的,即使通过指定 -v 参数得到详细输出,也就只是在默认输出内容的基础上,额外显示了用例名称,还达不到一份测试报告的标准,而是一份日志信息。
3.1 HTMLTestRunner
最新版:https://github.com/oldani/HtmlTestRunner
汉化版:https://github.com/wangquansheng/HTMLTestRunner\_cn
如果想要生成 HTML 格式的报告,那么就需要额外借助第三方库(如 HTMLTestRunner)来操作。
HTMLTestRunner 是一个第三方的 unittest HTML 报告库,首先我们下载 HTMLTestRunner.py,并放到对应目录下或者放到 …/python\Lib 目录下。
Tips:
这里我们使用的是汉化版的报告库。

然后可以在写测试用例的文件中导入这个 HTMLTestRunner.py 文件。
- HTMLTestRunner 类说明:
- stream:指定报告的路径
- title:报告的标题
- description:报告的描述
- run() 方法说明:
- suit:运行的测试套件
- rerun:重跑次数
- save_last_run:是否保存最后一个结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import unittest from test_project.lib.HTMLTestRunner import HTMLTestRunner
class TestDemo(unittest.TestCase): """测试用例说明""" def test_success(self): """执行成功""" self.assertEqual(2 + 3, 5)
@unittest.skip("skip case") def test_skip(self): pass
def test_fail(self): self.assertEqual(5, 6)
def test_error(self): self.assertEqual("a", 6)
class TestDemo2(unittest.TestCase): def test_success(self): self.assertEqual(4 + 5, 9)
class TestDemo3(unittest.TestCase): def test_fail(self): self.assertEqual(3, 9)
if __name__ == '__main__': suit = unittest.TestSuite() suit.addTest(TestDemo("test_success")) suit.addTest(TestDemo("test_skip")) suit.addTest(TestDemo("test_fail")) suit.addTest(TestDemo("test_error")) suit.addTest(TestDemo2("test_success")) suit.addTest(TestDemo3("test_fail"))
with(open('./result.html', 'wb')) as fp: runner = HTMLTestRunner( stream=fp, title='<TestDemo>-unittest测试框架报告', description='第一个报告示例', ) runner.run(suit, rerun=3, save_last_run=True)
|
我们运行后,在当前目录下就会生成一个 result.html 文件,将其以浏览器的方式打开,就能看到一份视图化的测试报告了。

3.2 BeautifulReport
项目地址:https://github.com/TesterlifeRaymond/BeautifulReport
仓库地址:https://www.cnpython.com/pypi/beautifulreport
BeautifulReport 是一个基于 unittest.TestReport 模块实现的测试用例模板,可以把我们每次测试中的结果通过 BeautifulReport 整合成一个可视化的报表。
安装
1
| pip install BeautifulReport
|
案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import os import unittest from BeautifulReport import BeautifulReport
class TestDemo(unittest.TestCase): """测试用例说明""" def test_success(self): """执行成功""" self.assertEqual(2 + 3, 5)
@unittest.skip("skip case") def test_skip(self): pass
def test_fail(self): self.assertEqual(5, 6)
def test_error(self): self.assertEqual("a", 6)
class TestDemo2(unittest.TestCase): def test_success(self): self.assertEqual(4 + 5, 9)
class TestDemo3(unittest.TestCase): def test_fail(self): self.assertEqual(3, 9)
if __name__ == '__main__': suite = unittest.TestSuite() suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestDemo))
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'report') if not os.path.exists(path): os.mkdir(path) runner = BeautifulReport(suite) runner.report(filename='测试报告', description='生成BeautifulReport报告', log_path='report')
|