一:ddt 数据驱动/参数化
1.什么是ddt
痛点:测试代码和测试数据耦合了,修改测试或者增加测试都需要修改代码。
DDT:DATA DRIVER TEST (数据驱动测试/参数化),是一个设计思想,不同的人有不同的理解。 它解决的问题:测试数据与测试用例代码分离,通过外部数据生成单元测试函数。
2.ddt 模块
pip install ddt
ddt
模块的原理:本质上就是一个装饰器,总共高就百来行代码。在创建类的时候,根据用例数据,动态的创建测试函数。
使用
-
从
ddt
模块中导入from ddt import ddt, data
两个装饰器 -
定义测试用例类时需要用
@ddt class SomeTestCase(unittest.TestCase): pass
-
定义单元测试函数时,需要
@data(case1, case2, case3,...) def test_some(self, case): pass
-
ddt模块会自动的通过case1,case2,...在类里面创建 test_some_1,test_some_2,..单元测试函数
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/8 21:08
# @Author : shisuiyi
# @File : test_login.py
# @Software: win10 Tensorflow1.13.1 python3.9
import unittest
from testcode import login
from ddt import ddt, data
# from unittestreport import ddt, list_data
cases = [
{"title": "登陆成功", "request": {"username": "python45", "password": "lemonban"},
"expect": {"code": 0, "msg": "登录成功"}},
{"title": "账号或密码不正确", "request": {"username": "python45", "password": "lemonban123"},
"expect": {"code": 1, "msg": "账号或密码不正确"}},
{"title": "账号或密码不正确", "request": {"username": "python45123", "password": "lemonban"},
"expect": {"code": 1, "msg": "账号或密码不正确"}},
{"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemo"},
"expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
{"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemonbanlemonbanlemonbanlemonbanlemonban"},
"expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
]
@ddt
class TestLogin(unittest.TestCase):
"""
测试登录函数
"""
@classmethod
def setUpClass(cls) -> None:
"""
类级前置
:return:
"""
print('我是类级前置,在整个类的测试前执行')
@classmethod
def tearDownClass(cls) -> None:
"""
类级后置
:return:
"""
print('我是类级后置,在整个测试类测试完成后执行')
def setUp(self) -> None:
"""
方法级前置
:return:
"""
print('======我会在每个测试执行前执行====')
def tearDown(self) -> None:
"""
方法级后置
:return:
"""
print('======我会在每个测试执行后执行======')
@data(*cases)
# @list_data(cases)
def test_login(self, case):
# 1. 准备测试数据
print(case)
print('{}开始测试'.format(case['title']))
# 2. 测试步骤
res = login(**case['request'])
# 3. 断言
self.assertEqual(res, case['expect'])
if __name__ == '__main__':
unittest.main()
输出
Launching unittests with arguments python -m unittest D:/Lemon/py45/day17/test_ddt_login.py in D:\Lemon\py45\day17
我是类级前置,在整个类的测试前执行
======我会在每个测试执行前执行====
{'title': '登陆成功', 'request': {'username': 'python45', 'password': 'lemonban'}, 'expect': {'code': 0, 'msg': '登录成功'}}
登陆成功开始测试
======我会在每个测试执行后执行======
======我会在每个测试执行前执行====
{'title': '账号或密码不正确', 'request': {'username': 'python45', 'password': 'lemonban123'}, 'expect': {'code': 1, 'msg': '账号或密码不正确'}}
账号或密码不正确开始测试
======我会在每个测试执行后执行======
======我会在每个测试执行前执行====
{'title': '账号或密码不正确', 'request': {'username': 'python45123', 'password': 'lemonban'}, 'expect': {'code': 1, 'msg': '账号或密码不正确'}}
账号或密码不正确开始测试
======我会在每个测试执行后执行======
======我会在每个测试执行前执行====
{'title': '密码长度在6-18位之间', 'request': {'username': 'python45', 'password': 'lemo'}, 'expect': {'code': 1, 'msg': '密码长度在6-18位之间'}}
密码长度在6-18位之间开始测试
======我会在每个测试执行后执行======
======我会在每个测试执行前执行====
{'title': '密码长度在6-18位之间', 'request': {'username': 'python45', 'password': 'lemonbanlemonbanlemonbanlemonbanlemonban'}, 'expect': {'code': 1, 'msg': '密码长度在6-18位之间'}}
密码长度在6-18位之间开始测试
======我会在每个测试执行后执行======
我是类级后置,在整个测试类测试完成后执行
Ran 5 tests in 0.010s
OK
Process finished with exit code 0
ddt-data 数据
一组数据之间用逗号隔开
data的参数可以为如下几种
- 1.一组数据中,每个数据为单个值
- 2.一组数据中,每个数据为一个列表或者一个字典
- 3.文件对象:json,yaml
一组数据中的数据为列表或者字典:
@data([a,b],[c,d])
如何从以上数据中获取到字典中每一项值
@unpack
若变量A=[{a:b,a1:b1},{c:d,c1:d1}]
如何将变量A中的每一组元素作为测试数据
@ddt.data(*A)
包含知识点
@unpack :当传递的是复杂的数据结构时使用。比如使用元组或者列表,添加 @unpack 之后, ddt 会自动把元组或者列表对应到多个参数上。字典也可以这样处理
- 当没有加unpack时,test_case方法的参数只能填一个;
- 当你加了unpack时,传递的数据量需要一致;如列表例子中,每个列表我都固定传了2个数据,当你多传或少传时会报错,而test_case方法的参数也要写2个,需要匹配上
- 当传的数据是字典类型时,要注意每个字典的key都要一致,test_case的参数的命名也要一致;如字典的例子,两个字典的key都是value1和value2,而方法的参数也是
- 当传的数据是通过变量的方式,变量前需要加上*
列表
import ddt
import unittest
@ddt.ddt
class test_PersonInfo(unittest.TestCase):
@classmethod
def setUp(self):
print("==========开始测试==========")
@classmethod
def tearDown(self):
print("==========结束测试==========")
@ddt.data([1,2],[3,4])
def test_print(self,a):
print(a)
if __name__ == '__main__':
unittest.main()
=====================输出结果====================
[1, 2]
[3, 4]
print(a[0])
1
3
列表拆包
@ddt.data([1,2],[3,4])
@ddt.unpack
def test_print(self,a,b):
print(a,b)
==========开始测试==========
1 2
==========结束测试==========
==========开始测试==========
3 4
==========结束测试==========
字典
@data({'value1': 1, 'value2': 2}, {'value1': 1, 'value2': 2})
@unpack
def test_dict(self, value1, value2):
print("test_dict", value1, value2)
===============输出==============
test_dict 1 2
test_dict 1 2
元祖
tuples = ((1, 2, 3), (1, 2, 3))
@data(*tuples)
def test_tuples(self, n):
print("test_tuples", n)
=============输出================
test_tuples (1, 2, 3)
test_tuples (1, 2, 3)
包含字典的列表
datas = [{'class': 'python2', 'name': 'xiaoshitou', 'id': '0001', 'sex': 'male'},
{'class': 'python2', 'name': 'nuonuo', 'id': '0002', 'sex': 'female'},
{'class': 'python2', 'name': 'fly', 'id': '0003', 'sex': 'male'},
{'class': 'python2', 'name': 'haiyang', 'id': '0004', 'sex': 'male'},
{'class': 'python2', 'name': 'shuangshuang', 'id': '0005', 'sex': 'female'}
]
@ddt.data(*datas)
def test_print(self,datas):
print(datas)
if __name__ == '__main__':
unittest.main()
==========开始测试==========
{'class': 'python2', 'name': 'xiaoshitou', 'id': '0001', 'sex': 'male'}
==========结束测试==========
==========开始测试==========
{'class': 'python2', 'name': 'nuonuo', 'id': '0002', 'sex': 'female'}
==========结束测试==========
==========开始测试==========
{'class': 'python2', 'name': 'fly', 'id': '0003', 'sex': 'male'}
==========结束测试==========
==========开始测试==========
{'class': 'python2', 'name': 'haiyang', 'id': '0004', 'sex': 'male'}
==========结束测试==========
==========开始测试==========
{'class': 'python2', 'name': 'shuangshuang', 'id': '0005', 'sex': 'female'}
==========结束测试==========
传递json文件
dat-data数据
数据为文件对象:Json yaml
@ddt.file_data(jason文件路径) json格式里面必须为双引号“”,且必须为键值对,不存在纯列表格式,值作为测试数据
{"name":"nick","gender":"male","age":29}
JSON文件
{
"first": [
{
"isRememberMe": "True",
"password": "111111",
"username": "root"
},
"200"
],
"second": [
"{'isRememberMe': True, 'password': '1111111', 'username': 'root'}",
"406"
],
"third": [
1,
2
],
"four": "123123"
}
单元测试类
from ddt import *
# 在测试类前必须首先声明使用 ddt
@ddt
class imoocTest(unittest.TestCase):
@file_data('F:/test/config/testddt.json')
def test_json(self, data):
print(data)
==========输出结果==========
[{'isRememberMe': 'True', 'password': '111111', 'username': 'root'}, '200']
["{'isRememberMe': True, 'password': '1111111', 'username': 'root'}", '406']
[1, 2]
123123
传递YAML文件
YAML文件
unsorted_list:
- 10
- 15
- 12
sorted_list: [ 15, 12, 50 ]
单元测试类
from ddt import *
# 在测试类前必须首先声明使用 ddt
@ddt
class imoocTest(unittest.TestCase):
@file_data('F:/test/config/testddt.yaml')
def test4(self, data):
print("yaml", data)
===============输出结果==================
yaml [10, 15, 12]
yaml [15, 12, 50]
3. unittestreport
这个也有ddt功能,使用方法同ddt
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/8 21:08
# @Author : shisuiyi
# @File : test_login.py
# @Software: win10 Tensorflow1.13.1 python3.9
import unittest
from testcode import login
# from ddt import ddt, data
from unittestreport import ddt, list_data
cases = [
{"title": "登陆成功", "request": {"username": "python45", "password": "lemonban"},
"expect": {"code": 0, "msg": "登录成功"}},
{"title": "账号或密码不正确", "request": {"username": "python45", "password": "lemonban123"},
"expect": {"code": 1, "msg": "账号或密码不正确"}},
{"title": "账号或密码不正确", "request": {"username": "python45123", "password": "lemonban"},
"expect": {"code": 1, "msg": "账号或密码不正确"}},
{"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemo"},
"expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
{"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemonbanlemonbanlemonbanlemonbanlemonban"},
"expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
]
@ddt
class TestLogin(unittest.TestCase):
"""
测试登录函数
"""
@classmethod
def setUpClass(cls) -> None:
"""
类级前置
:return:
"""
print('我是类级前置,在整个类的测试前执行')
@classmethod
def tearDownClass(cls) -> None:
"""
类级后置
:return:
"""
print('我是类级后置,在整个测试类测试完成后执行')
def setUp(self) -> None:
"""
方法级前置
:return:
"""
print('======我会在每个测试执行前执行====')
def tearDown(self) -> None:
"""
方法级后置
:return:
"""
print('======我会在每个测试执行后执行======')
# @data(*cases)
@list_data(cases)
def test_login(self, case):
# 1. 准备测试数据
print(case)
print('{}开始测试'.format(case['title']))
# 2. 测试步骤
res = login(**case['request'])
# 3. 断言
self.assertEqual(res, case['expect'])
if __name__ == '__main__':
unittest.main()
二:excel用例文件处理
1. openpyxl 模块
读写excel文件的第三方库 支持的格式: excel 2010 xlsx/xlsm/xltx/xltm
pip install openpyxl
基本操作
示例图表:
# 1. 打开工作簿
import openpyxl
wb = openpyxl.load_workbook('testdata.xlsx') # 读取excel文件返回一个workbook对象
print(wb, type(wb))
# 输出
<openpyxl.workbook.workbook.Workbook object at 0x000002476C754BB0> <class 'openpyxl.workbook.workbook.Workbook'>
# 2. 读取表
# 一个excel文件是要给workbook,拥有多个表,通过sheetname获取对应的表
ws = wb['login'] # 获取login表
print(ws, type(ws))
# 通过表名可以以字典取值的方式获取对应的表。封装为一个worksheet对象
# 输出
<Worksheet "login"> <class 'openpyxl.worksheet.worksheet.Worksheet'>
wb.active # workbook对象的active属性返回第一个表
输出
<Worksheet "login">
# 3. 读取单元格
# 通过表的行和列指定单元格,注意行号,列号以1开始cell = ws.cell(row=1,column=1)
# 单元格封装为cell对象print(cell, type(cell))
# 输出<Cell 'login'.A1> <class 'openpyxl.cell.cell.Cell'>
# 通过cell对象的value属性可以取出单元格中的值cell.value
输出
'id'
# 4. 读取一行
# worksheet对象的rows属性返回表的行数据的迭代对象
ws.rows
输出
<generator object Worksheet._cells_by_row at 0x000002476E52D5F0>
for row in ws.rows:
for item in row:
print(item.value, end=' ')
print()
# 输出
id title request expect
1 登录成功 {"username": "python45", "password": "lemonban"} {"code": 0, "msg": "登录成功"}
None 密码错误 {"username": "python45", "password": "lemonban1"} {"code": 1, "msg": "账号或密码不正确"}
None 账号错误 {"username": "python415", "password": "lemonban"} {"code": 1, "msg": "账号或密码不正确"}
None 密码过短 {"username": "python45", "password": "lemon"} {"code": 1, "msg": "密码长度在6-18位之间"}
None 密码过长 {"username": "python45", "password": "lemonban123456789012345678"} {"code": 1, "msg": "密码长度在6-18位之间"}
2. 封装读取excel用例数据的功能
是封装成一个函数,还是封装成一个类。
在大多数情况下,简单功能(代码函数较少,逻辑较简单)可以直接封装成函数。
复杂功能,数据结构复杂,一般进行面向对象封装。
1. 功能分析
- 根据exel文件名读取对应的文件数据
- 根据工作表名读取对应表数据
- 返回字典列表格式便于处理
2.封装成函数
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/11 20:51
# @Author : shisuiyi
# @File : read_excel_tool.py
# @Software: win10 Tensorflow1.13.1 python3.9
# 测试数据处理模块
from openpyxl import load_workbook
def get_data_from_excel(file, sheet_name=None):
"""
获取excel文件中指定表里的测试数据
:param file:
:param sheet_name:
"""
# 1. 读取excel文件
wb = load_workbook(file) # 读取excel文件返回一个workbook对象
# 2. 读取对应的表
if sheet_name is None:
# 如果不传sheet_name,默认使用第一张表
ws = wb.active
else:
ws = wb[sheet_name] # 通过表名可以以字典取值的方式获取对应的表。封装为一个worksheet对象
# 3. 创建一个列表容器存放数据
data = []
# 4. 组织数据
# 4.1 获取表头,作为字典的key
row_list = list(ws.rows) # worksheet对象的rows属性返回表的行数据的迭代对象
# tilte = []
# # 拿到第一行,然后迭代
# for key in row_list[0]:
# tilte.append(key.value) # 通过cell对象的value属性可以取出单元格中的值
title = [item.value for item in row_list[0]] # 列表生成式
# 4.2 获取其他数据
for row in row_list[1:]:
# 获取每一行的数据
temp = [i.value for i in row]
# 将表头与这一行数据打包,换成字典
data.append(dict(zip(title, temp)))
return data
if __name__ == '__main__':
res = get_data_from_excel('testdata.xlsx')
print(res)
使用封装的函数读取excel中的元素然后进行测试
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/8 21:08
# @Author : shisuiyi
# @File : test_login.py
# @Software: win10 Tensorflow1.13.1 python3.9
import unittest
from testcode import login
# from ddt import ddt, data
from unittestreport import ddt, list_data
from read_excel_tool import get_data_from_excel
# cases = [
# {"title": "登陆成功", "request": {"username": "python45", "password": "lemonban"},
# "expect": {"code": 0, "msg": "登录成功"}},
# {"title": "账号或密码不正确", "request": {"username": "python45", "password": "lemonban123"},
# "expect": {"code": 1, "msg": "账号或密码不正确"}},
# {"title": "账号或密码不正确", "request": {"username": "python45123", "password": "lemonban"},
# "expect": {"code": 1, "msg": "账号或密码不正确"}},
# {"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemo"},
# "expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
# {"title": "密码长度在6-18位之间", "request": {"username": "python45",
# "password": "lemonbanlemonbanlemonbanlemonbanlemonban"},
# "expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
# ]
cases = get_data_from_excel('testdata.xlsx', 'login')
@ddt
class TestLogin(unittest.TestCase):
"""
测试登录函数
"""
@classmethod
def setUpClass(cls) -> None:
"""
类级前置
:return:
"""
print('我是类级前置,在整个类的测试前执行')
@classmethod
def tearDownClass(cls) -> None:
"""
类级后置
:return:
"""
print('我是类级后置,在整个测试类测试完成后执行')
def setUp(self) -> None:
"""
方法级前置
:return:
"""
print('======我会在每个测试执行前执行====')
def tearDown(self) -> None:
"""
方法级后置
:return:
"""
print('======我会在每个测试执行后执行======')
# @data(*cases)
@list_data(cases)
def test_login(self, case):
# 1. 准备测试数据
print(case)
print('{}开始测试'.format(case['title']))
# 2. 测试步骤
res = login(**eval(case['request']))
# 3. 断言
self.assertEqual(res, eval(case['expect']))
if __name__ == '__main__':
unittest.main()
评论