目录
一、需注意事项
二、单元测试框架unittest
2.1 作用
2.2 测试用例(TestCase)
2.2.1 单元测试函数
2.2.2 测试函数的执行顺序
2.3 用例收集器(TestLoader)
2.4 测试套件(TestSuite)
2.5 测试运行器(TextTestRunner)
2.6 测试脚手架(FIXture)
2.7 断言
三、基础框架的搭建
3.1 编写测试用例
3.2 定义main.py
3.3 生成html报告
3.3.1 HTMLTestRunner报告
3.3.2 BeautifulReport报告
四、ddt的使用(项目参数化)
4.1 安装
4.2 使用
4.3 ddt生成报告定义标题信息
4.3.1 复制ddt.py到common文件夹下,并修改标记处代码
4.3.2 修改测试用例调用的模块信息
五、http请求的封装及使用
5.1 http请求的封装
5.2 注册接口测试用例编写
六、读取excel中的用例数据
6.1 安装
6.2 测试数据编写
6.3 excel文件的基本操作
6.4 读取excel文件功能封装
6.5 将封装的功能应用到项目中
七、生成日志
7.1 日志的基本操作
7.1.1 日志的级别
7.1.2 日志的基本操作
7.1.3 日志导出到文件
7.1.4 日志的高阶用法
7.2日志模块的封装
7.3 封装的日志模块应用到项目中
7.4 单例模式
7.4.1 问题描述
7.4.2 处理生成重复日志问题
八、项目配置
8.1 配置文件
8.1.1 ini、conf、cnf、cfg
8.1.2 yaml、yml
8.2 配置文件模块封装
8.2.1 函数封装
8.2.2 类封装
8.3 封装的配置文件应用到项目中
8.3.1 定义yaml配置文件
8.3.2 修改日志器调用时传参
8.3.3修改测试用例数据文件的路径
8.3.4 修改生成报告时传入参数
九、项目配置最终方案
9.1 创建settings.py的配置文件
9.2 修改日志调用时的传参
9.3 修改用例数据文件路径
9.4 修改生成报告时传参
十、项目路径处理
10.1 路径基础知识
10.1.1 全局变量__file__
10.1.2 os模块
10.2 项目路径设置
十一、MySQL数据库验证
11.1 MySQL数据库的基本操作
11.1.1 安装
11.1.2 python操作mysql数据库基本流程
11.1.3 python查询mysql数据库
11.1.4 Python修改mysql数据库
11.1.5 操作mysql数据库时需注意事项
11.2 封装数据库操作并应用到项目中
11.2.1 数据库操作的封装
11.2.2 配置文件中增加数据库信息
11.2.3 修改数据库操作为单例模式
11.2.4 修改测试数据表格中内容
11.2.5 调整注册接口断言信息
十二、定义生成的报告
12.1 时间处理模块datetime的基础知识
12.2 封装测试报告并应用到项目中
12.2.1 测试报告的封装
12.2.2 修改配置文件中报告相关信息
12.2.3 修改出入口函数
十三、动态生成手机号码
13.1 随机库random的基础知识
13.2 封装随机生成手机号码并应用到项目中
13.2.1 生成随机号码的封装
13.2.2 修改测试数据中信息
13.2.3 修改测试数据处理模块中代码
13.2.4 修改注册用例代码
十四、项目路径配置
14.1在配置文件中配置项目域名、接口信息
14.2 修改用例数据中url信息
14.3 修改测试用例代码
十五、类前置条件与充值接口
15.1 封装注册、登录用户函数
15.2 将注册、登录模块应用到项目中
15.2.1 新增recharge(充值)的测试数据
15.2.2 新增充值测试用例
十六、动态参数替换
16.1 正则表达式的基本使用
16.2 动态参数模块的使用
16.2.1 封装动态获取槽位并替换槽位内容
16.2.2 修改充值接口用例类
十七、方法级前置与项目审核接口测试
17.1 封装创建项目函数
17.2审核项目接口用例数据
17.3配置文件中添加接口信息
17.4 创建审核接口
十八、测试用例基类抽取
18.1 项目状态码验证
18.2 测试基类的定义
18.3 修改其他用例类
18.3.1 修改注册接口用例类
18.3.2 修改充值接口用例类
18.3.3修改审核接口用例类
十九、业务流测试
19.1 在测试用例类中根据业务流定义对应的单元测试方法
19.2 将业务流做成数据驱动
19.2.1 将业务流做成数据驱动:
19.2.2 用例数据设计:
19.2.3 jsonpath的基础知识:
19.2.4 读取响应数据后定义为类属性的封装
19.2.5 编写投资业务流代码
二十、session鉴权的处理
20.1 requests的会话对象
20.2 封装session鉴权的http请求
二一、V3版本鉴权与多条sql校验
21.1 rsa加密
22.2封装v3版本token签名函数及多条sql验证
二二、mock测试
22.1 mock的基础知识
22.2将mock应用到项目中
22.3mock服务
在pycharm中调试时,存在项目路径容易报错问题,故单独打开项目文件夹:
导入模块时,顺序是系统模块 > 第三方模块 > 自定义模块:
在函数中,设置三引号回车方式函数注释自动生成:
修改File > Settings > Tools > Python Integrated Tools > Docstring format > reStructuredText
在函数方法名下输入三个双(单)引号,回车之后自动生成
项目运行方式选择:
编码格式报错:
在.py文件中最上方定义编码格式:#coding = utf-8
修改pycharm工作编码规则,File>Settings>Editor>File Encodings,修改编码为utf-8
项目相关内容:
项目参考柠檬班教材,标题十五处开始可搭配pdf24查看。
接口无法使用,仅参考学习接口自动化思路
测试接口过多时处理方法:
当测试接口过多时,可在testcase文件夹下新建test_**__module.py文件,分模块编写测试用例,模块中每个用例为单独的一个类。
unittest是Python自带的一个单元测试框架
管理用例
批量执行用例
组织运行结果/报告
让代码更稳健
可拓展
unittest提供了一个基类TestCase用来创建测试用例;
创建测试用例,导入unittest模块后,需要创建一个类,来继承TestCase;
类中每个方法代表一个测试用例,方法名必须以test开头,unittest只会识别以test开头的方法为测试用例
在test_cases文件夹下新建test_abs.py文件,文件内容如下:
import unittest
class TestAbs(unittest.TestCase):
name='测试abs函数'
def test_negative_num(self):
'''
测试负数
:return:
'''
# 1.测试数据
data=-1
except_data=1
# 2.测试步骤
res=abs(data)
# 3.断言
print(1)
self.assertEqual(res,except_data,msg='断言运行结果')
def test_zero(self):
'''
测试0
:return:
'''
# 1.测试数据
data=0
except_data=0
# 2.测试步骤
res=abs(data)
# 3.断言
print(2)
self.assertEqual(res,except_data,msg='断言运行结果')
def test_postive_num(self):
'''
测试正数
:return:
'''
# 1.测试数据
data=1
except_data=1
# 2.测试步骤
res=abs(data)
# 3.断言
print(3)
self.assertEqual(res,except_data,msg='断言运行结果')
测试用例执行时不是按照定义的顺序执行,而是按照ascii码排序,可以在test_后加上数字,定义执行顺序
test_1.... 、 test_2.....
定义main.py文件内容,运行main.py文件,可执行测试用例
'''
项目入口
1.收集所有的用例
2.执行所有的用例
'''
import unittest
if __name__ == '__main__':
#test_cases为文件路径
re=unittest.TestLoader().discover('test_cases')
print(re)
收集规则可以通过patterns自定义,默认是以test开头的模块,一般不做修改。
会到指定目录下递归去寻找符合patterns规则的模块中继承了TestCase的类。
注意:如果有嵌套的文件夹,一定要在文件夹中加入__init__.py,其实就是把文件夹变成一个Python的包。
用例收集器,会返回能够收集到的所有的用例,封装成测试套件类型。
测试套件,其实就是一系列的测试用例类。
添加单个测试用例:
import unittest
import testcase01
suite = unittest.TestSuite()
suite.addTest(testcase01.MyTest("test_001"))
suite.addTest(testcase01.MyTest("test_002"))
# 只是把测试用例添加到了测试套件中,并不是执行测试用例
添加测试用例类:
import unittest
import testcase01
suite = unittest.TestSuite()
# 只是把测试用例添加到了测试套件中,并不是执行测试用例
suite.addTest(unittest.makeSuite(testcase01.MyTest))
TextTestRunner是用来执行和输出测试用例和测试套件的结果的。
import unittest
#输出内容在控制台
if __name__ == '__main__':
re=unittest.TestLoader().discover('testcase')
#实例化TextTestRunner对象
runner=unittest.TextTestRunner()
#调用对象的run方法,执行测试用例
runner.run(re)
#输出内容问txt报告
if __name__ == '__main__':
re=unittest.TestLoader().discover('testcase')
# 保存在报告文件中
with open('reports/测试报告.txt','w',encoding='utf-8') as f:
runner=unittest.TextTestRunner(f)
runner.run(re)
可以在测试用例执行之前自动调用指定的函数,在测试用例执行之后自动调用指定的函数。
对象、方法:
-setUp会在每个单元函数测试开始前执行。
import unittest
class MyTest(unittest.TestCase):
def setUp(self) -> None:
print('setup被自动调用了')
def test_01(self):
print('测试用例01')
def test_02(self):
print('测试用例02')
if __name__ == '__main__':
MyTest()
-tearDown会在每个单元测试函数执行结束之后执行。
import unittest
class MyTest(unittest.TestCase):
def tearDown(self) :
print('teardown被自动调用了')
def test_01(self):
print('测试用例01')
def test_02(self):
print('测试用例02')
if __name__ == '__main__':
MyTest()
类方法,记得使用@classmethod去修饰:
--setUpclass在整个测试用例类开始执行前执行。
import unittest
class MyTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
print('setupclass自动调用了')
def test_01(self):
print('测试用例01')
def test_02(self):
print('测试用例02')
if __name__ == '__main__':
MyTest()
-tearDownclass在整个测试用例类执行结束之后执行。
import unittest
class MyTest(unittest.TestCase):
@classmethod
def tearDownClass(cls):
print('teardownclass自动调用了')
def test_01(self):
print('测试用例01')
def test_02(self):
print('测试用例02')
if __name__ == '__main__':
MyTest()
模块方法:
-setUpModule在.py文件开始执行前时调用
import unittest
def setUpModule():
print('setUpModule自动调用了')
class MyTest(unittest.TestCase):
def test_01(self):
print('测试用例01')
class HerTest(unittest.TestCase):
def test_02(self):
print('测试用例02')
if __name__ == '__main__':
MyTest()
-tearDownModule在.py文件执行完成时调用
import unittest
def tearDownModule():
print('tearDownModule自动调用了')
class MyTest(unittest.TestCase):
def test_01(self):
print('测试用例01')
class HerTest(unittest.TestCase):
def test_02(self):
print('测试用例02')
if __name__ == '__main__':
MyTest()
断言方法 | 断言描述 |
---|---|
assertTrue(expr, msg=None) | 验证 expr 是 true,如果为 false,则 fail |
assertFalse(expr, msg=None) | 验证 expr 是 false,如果为 true,则 fail |
assertEqual(expected, actual, msg=None) | 验证 expected==actual , 不等则 fail |
assertNotEqual(first, second, msg=None) | 验证 first != second, 相等则 fail |
assertIsNone(obj, msg=None) | 验证 obj 是 None , 不是则 fail |
assertIsNotNone(obj, msg=None) | 验证 obj 不是 None , 是则 fail |
assertIn(member, container, msg=None) | 验证是否 member in container |
assertNotIn(member, container, msg=None) | 验证是否 member not in container |
在项目根目录下新建common文件夹,用来存储公用方法。
在项目根目录下新建reports文件夹,用来存储项目报告。
在项目根目录下新建logs文件夹,用来存储项目日志。
在项目根目录下新建test_data文件夹,用来存储用例数据。
在项目根目录下新建test_cases文件夹,用例存储测试用例模块。
在项目根目录下新建main.py文件,作为入口函数,方便项目调试。
在test_cases文件夹下新建test_abs.py文件,内容如下
import unittest
#定义一个类继承unittest.TestCase
class TestAbs(unittest.TestCase):
name='测试abs函数'
def test_01negative_num(self):
'''
测试负数
:return:
'''
# 1.测试数据
data=-1
except_data=1
# 2.测试步骤
res=abs(data)
# 3.断言
print(1)
self.assertEqual(res,except_data,msg='断言运行结果')
def test_02zero(self):
'''
测试0
:return:
'''
# 1.测试数据
data=0
except_data=0
# 2.测试步骤
res=abs(data)
# 3.断言
print(2)
self.assertEqual(res,except_data,msg='断言运行结果')
def test_03postive_num(self):
'''
测试正数
:return:
'''
# 1.测试数据
data=1
except_data=1
# 2.测试步骤
res=abs(data)
# 3.断言
print(3)
self.assertEqual(res,except_data,msg='断言运行结果')
'''
项目入口
1.收集所有的用例
2.执行所有的用例
'''
#创建main.py入口函数
import unittest
if __name__ == '__main__':
#收集用例并返回测试套件
re=unittest.TestLoader().discover('test_cases')
#运行器运行并生成报告
with open('reports/测试报告.txt','w',encoding='utf-8') as f:
runner=unittest.TextTestRunner(f)
runner.run(re)
将HTMLTestRunner.py文件放在common文件夹下,修改main.py文件,并运行main.py文件
#main.py文件
import unittest
from common.HTMLTestRunner import HTMLTestRunner
if __name__ == '__main__':
re=unittest.TestLoader().discover('test_cases')
with open('reports/测试报告.html','wb') as f:
runner=HTMLTestRunner(f)
runner.run(re)
可修改HTMLTestRunner.py中默认信息
安装:
pip install beautifulreport
使用:
#main.py文件
import unittest
from BeautifulReport import BeautifulReport
if __name__ == '__main__':
re=unittest.TestLoader().discover('test_cases')
br=BeautifulReport(re)
br.report(description='项目描述',filename='reports/测试报告.html')
实现数据和测试脚本的分离,将测试数据加载到脚本中,一组数据对应生成一个测试用例。
只有测试流程完全一致时,才可以使用ddt。
pip install ddt
修改common文件夹下test_abs.py文件为如下内容
import unittest
#导入ddt
from ddt import ddt,data
#测试数据
cases=[
{'title':'负数','data':-1,'expect_data':1},
{'title':'0','data':0,'expect_data':0},
{'title':'正数','data':1,'expect_data':1}
]
#使用ddt修饰类
@ddt
class TestAbs(unittest.TestCase):
#使用data修饰函数,并将cases进行解包
@data(*cases)
#将解包后的数据传入
def test_abs(self,case):
# 1.测试数据
# 2.测试步骤
res=abs(case['data'])
# 3.断言
self.assertEqual(res,case['expect_data'],'断言信息')
if __name__ == '__main__':
unittest.main()
在使用ddt模块时,通过main.py入口文件调用并生成beautifulreport时,beautifulreport中标题为null,可将ddt.py文件复制并重命名一份放在common文件夹下,修改图片标记处代码,导入修改之后的模块,用例数据传入的title字段为标题内容。
修改前信息:
import unittest
#导入ddt
from ddt import ddt,data
修改后信息:
import unittest
#导入ddt
from common.myddt import ddt,data
因为发送请求在多个模块中使用,为了对代码进行复用,故对该功能进行封装。
在common文件夹下新建make_request.py文件
import requests
#此处->requests.Response的作用是表示返回的类型,其他模块调用时,pycharm可以进行提示
def send_http_requests(url,method,**kwargs) ->requests.Response:
"""
发送http请求
:param url:请求路径
:param method:请求方式
:param kwargs:params、data、json、headers.....
:return:response
"""
#把方法名小写化,防止误传
method=method.lower()
#获取对应的方法
return getattr(requests,method)(url,**kwargs)
#方法的调用
res=send_http_requests(url='http://www.baidu.com',method='get')
print(res.json)
在test_cases文件夹下新建test_register.py文件,文件内容如下
#注册接口测试用例
import unittest
from common.make_request import send_http_requests
from common.myddt import ddt,data
cases=[
{
'id':1,
'title':'注册成功-不带昵称和类型',
'method':'post',
'url':'http://test.lemonban.com/futureloan/mvc/api/member/register',
'request':{
'data':{"mobilephone":"13000450100","pwd":"12345678"},
'headers':{"x-Lemonban-Media-Type":"lemonban.v2"}
},
'expect_data':{"code":"10001","msg":"注册成功"}
},
{
'id':2,
'title':'注册成功-带昵称不带类型',
'method':'post',
'url':'http://test.lemonban.com/futureloan/mvc/api/member/register',
'request':{
'data':{"mobilephone":"13000450100","pwd":"12345678","type":0,"reg_name":"ces"},
'headers':{"x-Lemonban-Media-Type":"lemonban.v2"},
},
'expect_data':{"code":"10001","msg":"注册成功"}
}
]
@ddt
class TestRegister(unittest.TestCase):
#类方法
@classmethod
def setUpClass(cls) -> None:
print('注册接口开始测试')
@classmethod
def tearDownClass(cls) -> None:
print('注册接口测试结束')
@data(*cases)
def test_register(self,case):
# 1.测试数据
# 2.测试步骤
re=send_http_requests(url=case['url'],method=case['method'],**case['request'])
# 3.断言
re_data=re.json()
# 3.1状态码断言
self.assertEqual(200,re.status_code)
# 3.2响应结果断言
self.assertEqual(case['expect_data'],{'code':re_data['code'],'msg':re_data['msg']})
if __name__ == '__main__':
unittest.main()
excel是二进制文件,有特殊的编码
需要使用第三方库,openpyxl 支持的格式,Excel 2010 xlsx/xlsm/xltx/xltm
pip install openpyxl
在test_data文件夹下新建test_data.xlsx文件
字段信息:
id :序号
title:测试用例标题
method:请求方式post/get/...
url:请求的url
request:请求数据,包括请求头、请求体
expect_data:期望数据
测试数据中引号使用" "英文双引号,方便读取后转换为json格式
from openpyxl import load_workbook
# 1.用只读的方式打开工作簿
wb=load_workbook('test_data/test_data.xlsx',read_only=True)
# 2.打开表
#wb.get_sheet_by_name
sh=wb['register']
print(sh)
# 3.读取数据
# 读取出来的数据类型:数值 字符串
# row为行,column为列
c=sh.cell(row=1,column=2).value
print(c)
# 4.最大行、最大列
print(sh.max_row)
print(sh.max_column)
在common文件夹下新建test_data_handler.py文件
import json
from openpyxl import load_workbook
def get_test_data_from_excel(file,sheet_name):
'''
获取excel表格中用例数据
:param file: 文件名
:param sheet_name: 表名
:return: [{dict},{dict}]
'''
# 1.打开工作簿
wb=load_workbook(filename=file)
# 2.获取sheet
sh=wb[sheet_name]
#获取最大行数
row=sh.max_row
#获取最大列数
column=sh.max_column
# 3.获取数据
data=[]
# 获取第一行,拿到所有的key
keys=[]
for i in range(1,column+1):
keys.append(sh.cell(1,i).value)
# 循环每一行,组成字典
for i in range(2,row+1):
#定义一个临时变量用来存放每一行的数据
temp={}
#循环每一行的列
for j in range(1,column+1):
# 每个单元格就是一个键值对
# 获取对应列的键,因lie是1开头,索引是0开头,故j-1
# key=keys[j-1]
# value=sh.cell(i,j).value
# temp[key]=value
temp[keys[j-1]]=sh.cell(i,j).value
# 把request,expect_data json数据转换成python对象
# 编写时容易存在json格式错误,故加入try
try:
temp['request']=json.loads(temp['request'])
temp['expect_data']=json.loads(temp['expect_data'])
except json.decoder.JSONDecodeError:
raise ValueError('用例数据json格式错误')
#把每一行的数据形成的字典添加到data列表中
data.append(temp)
return data
if __name__ == '__main__':
re=get_test_data_from_excel('../test_data/test_data.xlsx','register')
print(re)
修改test_cases文件夹下test_register.py文件
导入封装的读取excel模块
from common.test_data_handler import get_test_data_from_excel
修改测试用例数据cases
#从excel中提取用例数据,因为是通过main.py执行,故文件路径是相对main.py文件的
cases=get_test_data_from_excel('test_data/test_data.xlsx','register')
日志的作用:记录运行情况,排查错误
使用的模块:logging模块(Python自带)
logging.debug() #调试:诊断问题的时候用,最详细的日志
logging.info() #普通信息:确定程序按照预定的流程运行
logging.warning() #警告信息:可能会出问题,程序还可以继续运行
logging.error() #错误信息:某些功能可能不能正确的执行
logging.critical() #危险信息:一个严重的错误
#导入模块
import logging
'''5个快捷方法,产生对应等级的日志
日志是否会被处理,通过阈值进行过滤,默认过滤阈值值是warning 30,当小于该值时,日志不处理
debug=10,info=20,warning=30,error=40,critical=50
'''
#设置日志过滤阈值,日志格式
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d] - %(message)s')
logging.debug('这是一个调试信息')
logging.info('这是一个普通信息')
logging.warning('这是一个警告信息')
logging.error('这是一个错误信息')
logging.critical('这是一个危险信息')
import logging
#在原来基础上传入文件名
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d] - %(message)s',
filename='log.log' #传入filename参数直接写入文件,不在控制台显示
)
logging.debug('这是一个调试信息')
import logging
#1.创建日志器
logger=logging.getLogger('lenmon')
#日志器可以设置等级,创建日志的时候生效
logger.setLevel(logging.DEBUG)
#2.创建日志处理器
#创建一个写到文件中的日志处理器
#日志处理器也可以设置等级
file_handler=logging.FileHandler(filename='log.log',encoding='utf-8')
file_handler.setLevel(logging.WARNING)
#创建一个控制台处理器将日志输出到控制台
console_handler=logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
#3.创建格式化器
formater=logging.Formatter(fmt='%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d] - %(message)s')
#4.将格式化器添加到日志处理器上
file_handler.setFormatter(formater)
console_handler.setFormatter(formater)
#5.将日志处理器添加到日志器上
logger.addHandler(file_handler)
logger.addHandler(console_handler)
logger.debug('这是一个调试信息')
logger.info('这是一个普通信息')
logger.warning('这是一个警告信息')
logger.error('这是一个错误信息')
logger.critical('这是一个危险信息')
封装日志模块,使日志内容既能够输出在控制台,又能够写入文件,并指定日志的路径、等级。
在common文件夹下新建log_handler.py文件
#coding=utf-8
import logging
def get_logger(name,filename,mode='a',encoding='utf-8',fmt=None,debug=False):
'''
:param name: 日志器的名字
:param filename:日志文件名
:param mode:文件模式
:param encoding:字符编码
:param fmt:日志格式
:param debug:调试模式
:return:
'''
#创建一个日志器并设置日志等级
logger=logging.getLogger(name)
logger.setLevel(logging.DEBUG)
#确定文件和控制台输出的日志级别,文件处理器的等级一般情况比控制台要高
if debug:
file_level=logging.DEBUG
console_level=logging.DEBUG
else:
file_level=logging.WARNING
console_level=logging.INFO
#定义日志的输出格式
if fmt is None:
fmt='%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d] - %(message)s'
#创建日志处理器
#写入文件的日志处理器
file_handler=logging.FileHandler(filename=filename,mode=mode,encoding=encoding)
file_handler.setLevel(file_level)
#写入控制台的日志处理器
console_handler=logging.StreamHandler()
console_handler.setLevel(console_level)
#创建格式化器并添加到日志处理器
formatter=logging.Formatter(fmt=fmt)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 将日志处理器添加到日志器上
logger.addHandler(file_handler)
logger.addHandler(console_handler)
#返回日志
return logger
if __name__ == '__main__':
logger=get_logger(name='py',filename='../logs/py.log',debug=True)
logger.info('我是普通信息')
修改test_cases文件夹下test_register.py文件
导入封装的生成日志模块
from common.log_handler import get_logger
定义测试类中每个步骤输出的日志内容
@ddt
class TestRegister(unittest.TestCase):
#将日志相关内容定义为类属性
logger=get_logger('ces','logs/ces.log',debug=True)
#类方法
@classmethod
def setUpClass(cls) -> None:
cls.logger.info('注册接口开始测试')
@classmethod
def tearDownClass(cls) -> None:
cls.logger.info('注册接口测试结束')
# ddt中data修饰测试用例,并将case列表解包传入 ,解包后的每一个元素为一个用例
@data(*cases)
def test_register(self,case):
# 通过 对象.属性调用日志方法
self.logger.info('用例【{}】开始测试》》》》'.format(case['title']))
# 1.测试数据
# 2.测试步骤
# 调试时查看发送的请求内容
self.logger.debug('url:{},method:{},request:{}'.format(case['url'],case['method'],case['request']))
# 调用send_http_requests发送请求
re=send_http_requests(url=case['url'],method=case['method'],**case['request'])
# 3.断言
re_data=re.json()
# 3.1状态码断言
try:
self.assertEqual(200,re.status_code)
except AssertionError as e:
#将报错内容输出到控制台,并打印错误日志
# self.logger.warn('状态码断言失败',exc_info=e)
# 将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('状态码断言失败')
raise e
#定义无异常时打印日志内容
else:
self.logger.info('状态码断言成功')
# 3.2响应结果断言
res={'code':re_data['code'],'msg':re_data['msg']}
try:
self.assertEqual(case['expect_data'],res)
#当断言失败,抛出异常,输出日志
except AssertionError as e:
self.logger.exception('请求结果断言失败')
self.logger.debug('期望数据:{}'.format(case['expect_data']))
self.logger.debug('实际数据:{}'.format(res))
self.logger.debug('响应数据:{}'.format(re_data))
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('用例[{}]测试通过'.format(case['title']))
# 用例执行完成日志内容,不考虑断言结果
finally:
self.logger.info('用例[{}]测试结束<<<<<<<'.format(case['title']))
if __name__ == '__main__':
unittest.main()
单例模式:一个生命周期中,只有一个实例
python中的模块本身也是对象,是天然的单例模式
直接在测试用例中调用封装的日志方法,名字、文件名相同时,会导致重复添加多个日志处理器,输出的日志文件重复。
1.导入模块时,会执行模块所在文件__init__.py文件(初始化),故将日志创建放到__init__.py文件中,实现单例模式。
在common文件夹下新建__init__.py文件
from .log_handler import get_logger
logger=get_logger('ces','logs/ces.log',debug=True)
2.修改测试用例中导入模块、类属性定义方法
目的:
处理不同的测试环境(开发环境、测试环境)
不同的项目,不改写代码
封装彻底,解耦合
格式:
section为段,配置文件中不需要引号,且key大小写不敏感
[section]
key=value
key1=value1
[section2]
key2=value2
conf.ini文件
[project]
base_url = http://test.lemonban.com/futureloan
[log]
name = pylog
filename = logs/pylog.log
debug = false
解析ini文件:
from configparser import ConfigParser
#实例化
conf=ConfigParser()
#读取配置文件
conf.read('conf.ini',encoding='utf-8')
#拿到所有的段名
secs=conf.sections()
print(secs)
#拿到某一个段名下的option配置名
opts=conf.options('log')
print(opts)
#拿到某个段名下的配置的二元元祖
tp=conf.items('log')
print(tp)
#获取指定段名中配置的之
res=conf.get('log','name')
# 支持字典模式取值
print(conf['project']['base_url'])
# 默认情况下配置的值,全部会被转换成字符串
# 手动转换值的类型
print(conf.getboolean('log','debug'))
yaml格式验证网站:YAML、YML在线编辑器(格式化校验)-BeJSON.com
区分大小写
使用缩进表示层级,不能使用tab键缩进,只能用空格(和python一样)
缩进没有数量的,只要前面是对齐的就行
注释是#
Map对象:
section:
键:(空格)值
键1:(空格)值1
log:
name: pylog
filename: logs/pylog.log
debug: true
#等同于
#{
# 'log':
# {
# 'name':'py38',
# 'filename':'logs/pylog.log',
# 'debug':True
# }
#}
数组:
键:
-(空格)值
-(空格)值1
laguages:
- python
- java
- perl
#等同于
# {
# 'laguages':['python','java','perl']
# }
解析yaml文件:
安装:
pip install yamlpy
解析:
# coding=utf-8
import yaml
with open('conf.yaml','r',encoding='utf-8') as f:
config=yaml.load(f,Loader=yaml.FullLoader)
print(config)
简单功能直接封装成函数,复杂功能封装成类。
配置文件封装:能够处理多种类型配置文件,返回值数据结构一致
在common文件夹下新建config_handler.py
import yaml
from configparser import ConfigParser
#定义返回类型为字典
def get_config(filename,encoding='utf-8') ->dict:
'''
获取配置文件
:param filename: 文件名
:param encoding: 文件编码
:return: data
'''
# 1.获取文件名后缀
suffix=filename.split('.')[-1]
# 2.判断这个配置文件的类型
if suffix in ['ini','cfg','cnf']:
# ini配置
conf=ConfigParser()
conf.read(filename,encoding=encoding)
# 将ini配置信息解析成一个字典
data={}
for section in conf.sections():
data[section]=dict(conf.items(section))
elif suffix in ['yaml','yml']:
# yaml配置
with open(filename,'r',encoding=encoding) as f:
data=yaml.load(f,Loader=yaml.FullLoader)
else:
raise ValueError('不能识别的配置文件后缀名')
return data
if __name__ == '__main__':
res=get_config('../conf.yaml')
print(res)
import yaml
from configparser import ConfigParser
class Config:
def __init__(self,filename,encoding='utf-8'):
#初始化工作
self.filename=filename
self.encoding=encoding
self.suffix=filename.split('.')[-1]
if self.suffix not in ['ini','conf','cnf','cfg','yml','yaml']:
raise ValueError('不能识别的配置文件后缀名')
def __parse_ini(self):
'''
解析ini,conf,cnf,cfg
:return:
'''
conf=ConfigParser()
conf.read(self.filename,encoding=self.encoding)
# 将ini配置信息解析成一个大字典
data={}
for section in conf.sections():
data[section]=dict(conf.items(section))
return data
def __parse_yaml(self):
'''
解析yaml,yml文件
:return:
'''
with open(self.filename,'r',encoding=self.encoding) as f:
data=yaml.load(f,Loader=yaml.FullLoader)
return data
def parse(self):
'''
解析配置文件
:return:
'''
if self.suffix in ['yaml','yml']:
return self.__parse_yaml()
else:
return self.__parse_ini()
if __name__ == '__main__':
res=Config('../conf.yaml')
res=res.parse()
print(res)
res=Config('../conf.ini')
res = res.parse()
print(res)
一个框架封装的彻不彻底的标准是能否复用,即应用到其他项目时,不需要修改框架的源码。
在项目文件夹下新建conf.yaml配置文件,文件内容
log:
name: pylog
filename: logs/pylog.log
debug: true
testdata:
file: test_data/test_data.xlsx
report:
filename: reports/测试报告.html
description: 一个练手项目
将这些内容写到配置文件中,然后在框架代码中动态的获取配置文件的相对应设置,实现代码的解耦。
修改common文件夹下__init__.py文件
from .log_handler import get_logger
from .config_handler import get_config
# 获取解析出来的配置数据字典
config=get_config('conf.yaml')
# 将字典中log的数据解包传入
logger=get_logger(**config['log'])
修改test_cases文件夹下test_register.py文件中导入模块,和cases
# from common import logger
#》》》》修改部分,增加导入config
from common import logger,config
# cases=get_test_data_from_excel('test_data/test_data.xlsx','register')
# 》》》》修改部分,文件路径
cases=get_test_data_from_excel(config['testdata']['file'],'register')
修改根目录下main.py文件,并运行main.py
import unittest
from BeautifulReport import BeautifulReport
from common import config
if __name__ == '__main__':
re=unittest.TestLoader().discover('test_cases')
br=BeautifulReport(re)
# br.report(description='项目描述',filename='reports/测试报告2.html')
br.report(**config['report'])
可以单独使用一个python文件,在其中定义配置信息,框架中的其他核心模块,直接导入该配置模块中的配置变量进行使用,效率更高,更直观。
在根目录下新建settings.py文件
#日志配置
LOG_CONFIG={
'name':'pylog',
'mode':'a',
'encoding':'utf-8',
'debug':True
}
#测试数据配置
TEST_DATA_FILE='test_data/test_data.xlsx'
#测试报告
REPORT_CONFIG={
'filename':'reports/测试报告.html',
'description':'一个练手项目'
}
修改common文件夹下__init__.py文件为如下内容
import settings
from .log_handler import get_logger
from .config_handler import get_config
#将settings.LOG_CONFIG的值解包传入
logger=get_logger(**settings.LOG_CONFIG)
修改test_cases文件夹下test_register.py
#from common import logger,config
# 》》》》修改部分,删除config
from common import logger
#增加导入模块
import settings
#case=get_test_data_from_excel(config['testdata']['file'],'register')
# 》》》》修改部分,文件路径
case=get_test_data_from_excel(settings.TEST_DATA_FILE,'register')
修改根目录下main.py文件
import unittest
from BeautifulReport import BeautifulReport
# from common import config
import settings
if __name__ == '__main__':
re=unittest.TestLoader().discover('test_cases')
br=BeautifulReport(re)
# br.report(**config['report'])
br.report(**settings.REPORT_CONFIG)
在现有代码中,路径都是使用项目根目录的相对路径,在调试子模块时会出现路径错误的问题,为解决该问题,对项目路径进行处理。
__file__返回当前模块的绝对路径。
#demo.py
print(__file__)
运行后输出当前模块的绝对路径,在其他模块中调用该模块时,值依然为这个模块的绝对路径。
可通过该属性动态获取项目的根目录,拼接项目下其他文件路径获取其他需要配置路径的绝对路径。
路径处理涉及到不同系统的格式问题,可以使用内置模块os进行路径的处理。
import os
# 获取当前模块绝对路径
print(__file__)
# 获取当前模块绝对路径,并对格式进行处理
print(os.path.abspath(__file__))
# 获取当前文件父目录的绝对路径
print(os.path.dirname(__file__))
# 获取处理格式之后模块父目录的绝对路径
res=os.path.dirname(os.path.abspath(__file__))
print(res)
# 拼接文件路径
print(os.path.join(res,'logs'))
在settings.py新增路径内容BASE_DIR,并修改其他配置内容中的文件路径
import os
# 项目路径
BASE_DIR=os.path.dirname(os.path.abspath(__file__))
#日志配置
LOG_CONFIG={
'name':'pylog',
'filename':os.path.join(BASE_DIR,'logs/pylog.log'),
'mode':'a',
'encoding':'utf-8',
'debug':True
}
#测试数据配置
TEST_DATA_FILE=os.path.join(BASE_DIR,'test_data/test_data.xlsx'),
#测试报告
REPORT_CONFIG={
# 因为beautiful report是根据项目相对路径处理,故不做修改
'filename':'reports/测试报告.html',
'description':'一个练手项目'
}
在接口测试中,除验证响应数据外,如过接口对数据库进行操作,且数据敏感则需要对数据库进行验证。
pip install pymysql
'''
1.创建连接
2.创建游标
3.执行sql语句
4.获取结果
5.关闭游标
6.关闭连接
'''
import pymysql
# 1.创建连接
conn=pymysql.connect(
# 主机名
host='api.lemonban.com',
# 用户
user='future',
# 密码
password='123456',
# 数据库名
db='futureloan',
# 编码格式
charset='utf8',
# 端口
port=3306
)
# 2.创建游标
cursor=conn.cursor()
# 3.执行sql语句
sql='select count(id) as "总人数" from member'
cursor.execute(sql)
# 4.获取结果
res=cursor.fetchone()
print(res)
# 5.关闭游标
cursor.close()
# 6.关闭链接
conn.close()
import pymysql
db_config={
'host':'api.lemonban.com',
'user':'future',
'password':'123456',
'db':'futureloan',
'charset':'utf8',
# 创建连接时,定义返回的信息包含字段名
'cursorclass':pymysql.cursors.DictCursor,
'port':3306
}
conn=pymysql.connect(**db_config)
# 通过with方式打开,系统会自动断开连接
# 返回内容包含字段信息
# with conn.cursor(pymysql.cursors.DictCursor) as cursor:
# 仅返回值,不包含字段名称
with conn.cursor() as cursor:
sql='select * from member order by id limit 10'
cursor.execute(sql)
# 获取一条
res1=cursor.fetchone()
print(res1)
# 获取三条
res2=cursor.fetchmany(3)
print(res2)
# 获取剩下所有的数据
res3=cursor.fetchall()
print(res3)
import pymysql
db_config={
'host':'api.lemonban.com',
'user':'future',
'password':'123456',
'db':'futureloan',
'charset':'utf8',
# 创建连接时,定义返回的信息包含字段名
'cursorclass':pymysql.cursors.DictCursor,
'port':3306
}
# 1.连接数据库
conn=pymysql.connect(**db_config)
# pymysql默认开启事务
try:
with conn.cursor() as cursor:
# 构造sql语句
sql1='update account set amount=amount-100 where username="A"'
sql2 = 'update account set amount=amount+100 where username="B"'
#执行sql语句
cursor.execute(sql1)
cursor.execute(sql2)
#如果执行成功就提交事务
conn.commit()
except Exception as e:
# 异常则进行回滚
conn.rollback()
raise e
finally:
conn.close()
print(123)
sql语句不需要加分号
execute执行后返回的是查询后更新的数据条数,结果需要通过cursor获取
需要释放资源,关闭游标与连接
事务的回滚与提交
import pymysql
class DB:
# 规定传入的db_config为字典格式
def __init__(self,db_config:dict):
# 创建连接
self.conn=pymysql.connect(**db_config)
def get_one(self,sql):
'''
获取一条数据
:param sql: sql语句
:return:
'''
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchone()
def get_many(self,sql,size:int):
'''
获取多条数据
:param sql: sql语句
:param size: 获取的数据条数
:return:
'''
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)
def get_all(self,sql):
'''
获取所有数据
:param sql:
:return:
'''
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()
def exist(self,sql):
'''
判断是否存在数据
:param sql:
:return:
'''
with self.conn.cursor() as cursor:
cursor.execute(sql)
if cursor.fetchone():
return True
else:
return False
def __del__(self):
'''
忘记关闭连接的时候,程序自动调用关闭连接
:return:
'''
if __name__ == '__main__':
import settings
db=DB(settings.DB_CONFIG)
sql='select id,reg_name from member limit 10'
res=db.get_one(sql)
print(res)
在settings.py文件中新增如下内容
#数据库配置
DB_CONFIG={
'host':'api.lemonban.com',
'user':'future',
'password':'123456',
'db':'futureloan',
'charset':'utf8',
'port':3306
}
一个DB对象可实现对数据库的所有操作,通过一个DB对象创建一个连接,完成所有操作,达到节省资源的效果。
common文件夹下__init__.py文件新增如下内容
from .db_handler import DB
db=DB(settings.DB_CONFIG)
当要验证数据库时,在test_data文件夹下test_data.xlsx中第一行新增sql字段,注册成功的用了后写入对应的sql语句
#注册接口测试用例
import unittest
import settings
from common.make_request import send_http_requests
from common.myddt import ddt,data
from common.test_data_handler import get_test_data_from_excel
from common import logger,db
#从excel中提取用例数据,因为是通过main.py执行,故文件路径是相对main.py文件的
cases=get_test_data_from_excel(settings.TEST_DATA_FILE,'register')
@ddt
class TestRegister(unittest.TestCase):
# 将日志相关内容定义为类属性
logger=logger
# 将数据库相关内容定义为类属性
db=db
#类方法
@classmethod
def setUpClass(cls) -> None:
cls.logger.info('注册接口开始测试')
@classmethod
def tearDownClass(cls) -> None:
cls.logger.info('注册接口测试结束')
# ddt中data修饰测试用例,并将case列表解包传入 ,解包后的每一个元素为一个用例
@data(*cases)
def test_register(self,case):
# 通过 对象.属性调用日志方法
self.logger.info('用例【{}】开始测试》》》》'.format(case['title']))
# 1.测试数据
# 2.测试步骤
# 调试时查看发送的请求内容
self.logger.debug('url:{},method:{},request:{}'.format(case['url'],case['method'],case['request']))
# 调用send_http_requests发送请求
re=send_http_requests(url=case['url'],method=case['method'],**case['request'])
# 3.断言
re_data=re.json()
# 3.1状态码断言
try:
self.assertEqual(200,re.status_code)
except AssertionError as e:
#将报错内容输出到控制台,并打印错误日志
# self.logger.warn('状态码断言失败',exc_info=e)
# 将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('状态码断言失败')
raise e
#定义无异常时打印日志内容
else:
self.logger.info('状态码断言成功')
# 3.2响应结果断言
res={'code':re_data['code'],'msg':re_data['msg']}
try:
self.assertEqual(case['expect_data'],res)
#当断言失败,抛出异常,输出日志
except AssertionError as e:
self.logger.exception('请求结果断言失败')
self.logger.debug('期望数据:{}'.format(case['expect_data']))
self.logger.debug('实际数据:{}'.format(res))
self.logger.debug('响应数据:{}'.format(re_data))
raise e
# 定义无异常时打印日志内容
# else:
# self.logger.info('用例[{}]测试通过'.format(case['title']))
# # 用例执行完成日志内容,不考虑断言结果
# finally:
# self.logger.info('用例[{}]测试结束<<<<<<<'.format(case['title']))
# 定义无异常时打印日志内容
else:
self.logger.info('请求结果断言成功')
# 3.3若sql不为空,校验数据库
if case['sql']:
# 查询数据
# 根据项目情况,编写断言代码
try:
db_res=self.db.exist(case['sql'])
self.assertTrue(db_res)
except Exception as e:
self.logger.exception('数据库断言失败')
self.logger.debug('执行的sql是{}'.format(case['sql']))
raise e
else:
self.logger.info('数据库断言成功')
self.logger.info('用例【{}】测试结束《《《《'.format(case['title']))
if __name__ == '__main__':
unittest.main()
python语言中有四个类来处理时间
time 时间
date 日期
datetime 时间日期
timedelta 时间间隔
from datetime import datetime
# 1.创建一个datetime对象
d=datetime(year=2023,month=1,day=28,hour=20,minute=10,second=42)
print(d)
# 2.获取当前时间
now=datetime.now()
print(now,type(now))
# 3.生成时间格式前缀 格式 yyyymdHMS
# 将datetime对象转换为指定格式的字符串时间表示
res=now.strftime('%Y%m%d%H%M%S')
print(res)
print(now.strftime('%Y年%m月%d日 %H时%M分%S秒'))
在common文件夹下新建report_handler.py文件,内容如下:
import os
from datetime import datetime
from BeautifulReport import BeautifulReport
from .HTMLTestRunner import HTMLTestRunner
def report(ts,filename,report_dir,theme='theme_default',title=None,
description=None,tester=None,_type='br'):
'''
执行用例并生成报告
:param ts: 测试套件
:param filename: 报告文件名
:param report_dir: 报告文件夹,仅支持BeautifulReport
:param theme: 主题,仅支持BeautifulReport
:param title: 报告标题,仅支持HTMLTestRunner
:param description: 报告描述
:param tester: 测试人员,仅支持HTMLTestRunner
:param _type: 默认值为br,表示生成BeautifulReport风格的报告
:return:
'''
# 1.生成时间前缀
time_prefix=datetime.now().strftime('%Y%m%d%H%M%S')
# 2.拼接时间前缀到报告文件名
filename='{}_{}'.format(time_prefix,filename)
if _type=='br':
# 生成BeautifulReport报告
br=BeautifulReport(ts)
br.report(description=description,filename=filename,report_dir=report_dir,theme=theme)
else:
# 生成HTMLTestRunner报告
with open(os.path.join(report_dir,filename),'wb') as f:
runner=HTMLTestRunner(f,title=title,description=description,tester=tester)
runner.run(ts)
修改根目录下settings.py文件中测试报告信息
#测试报告
REPORT_CONFIG={
'filename':'测试报告.html',
'description':'一个练手项目',
'report_dir':os.path.join(BASE_DIR,'reports')
}
修改根目录下main.py文件为如下内容:
import unittest
# from BeautifulReport import BeautifulReport
from common.repor_handler import report
import settings
if __name__ == '__main__':
# re=unittest.TestLoader().discover('test_cases')
# br=BeautifulReport(re)
# br.report(**settings.REPORT_CONFIG)
ts = unittest.TestLoader().discover('test_cases')
report(ts,**settings.REPORT_CONFIG)
import random
# 1.生成[0,1)之间的浮点数
print(random.random())
# 2.生成[a,b]之间的随机整数
print(random.randint(1,10))
# 3.生成[a,b)之间的随机整数
print(random.randrange(1,10))
# 4.在一个序列中随机的获取一个元素
ls=list('abcdefg')
print(random.choice(ls))
common文件夹下test_data_handler.py文件中新增如下内容
import random
from common import db
def generate_phone():
'''
随机生成一个手机号码
:return:
'''
#1.1开头
#2.11位
#3.第二位数字3-9
#以实际项目为准
phone=['158']
#剩下8位
for i in range(8):
phone.append(str(random.randint(0,9)))
return ''.join(phone)
def generate_no_use_phone(sql='select id from member where mobile_phone={}'):
'''
随机生成没有使用过的手机号码
:param sql: 校验sql模板
:return:
'''
while True:
phone=generate_phone()
sql=sql.format(phone)
if not db.exist(sql):
return phone
if __name__ == '__main__':
res=generate_no_use_phone()
print(res)
在测试用例数据中生成手机号码的槽位,在代码中检测测试数据,如果出现操作则动态生成并替换。
因为使用槽位后,表格中数据不能正确转换为json格式,故修改test_data_handler.py文件
修改common文件夹下test_data_handler.py中get_test_data_from_excel函数,注释try处。
def get_test_data_from_excel(file,sheet_name):
'''
获取excel文件中的用例数据
:param file:文件名
:param sheet_name:表名
:return:[{dict},{dict}]
'''
#1.打开工作簿
wb=load_workbook(filename=file)
#2.获取sheet
sh=wb[sheet_name]
row = sh.max_row
column = sh.max_column
#3.获取数据
data=[]
#获取第一行拿到所有的key
keys=[]
for i in range(1,column+1):
keys.append(sh.cell(1,i).value)
#循环每一行,组成字典
for i in range(2,row+1):
#定义一个临时变量用来存放每一行的数据
temp={}
#循环每一行的列
for j in range(1,column+1):
#每个单元格就是一个键值对
#获取对应列的键,因lie是1开头,索引是0开头,故j-1
# key=keys[j-1]
# value=sh.cell(i,j).value
# temp[key]=value
temp[keys[j-1]]=sh.cell(i,j).value
#把request,expect_data json数据转换成python对象
#编写时容易存在json格式错误,故加入try
# try:
# temp['request']=json.loads(temp['request'])
# temp['expect_data']=json.loads(temp['expect_data'])
# except json.decoder.JSONDecodeError:
# raise ValueError('用例数据json格式错误')
#把每一行数据形成的字典添加到data列表中
data.append(temp)
return data
# if __name__ == '__main__':
# re=get_test_data_from_excel('../test_data/test_data.xlsx', 'register')
# print(re)
# import openpyxl
# def redy_excl(load,sheel=None):
# workbook=openpyxl.load_workbook(load)
# sheel=workbook[sheel]
# # 获取所有数据
# rows=list(sheel.values)
# # 定义一个列表
# lists=[dict(zip(rows[0],i)) for i in rows[1:]]
# try:
# for i in range(0,len(lists)):
# lists[i]['request']=json.loads(lists[i]['request'])
# lists[i]['except_data']=json.loads(lists[i]['except_data'])
# except json.decoder.JSONDecodeError:
# raise ValueError('用例数据json格式错误')
# return lists
#
# if __name__ == '__main__':
# re=redy_excl('../test_data/test_data.xlsx', 'register')
# print(re)
增加数据处理步骤,将槽位替换为随机生成的手机号码,修改test_cases 文件夹下test_register.py文件
import json
import unittest
import settings
from common.make_request import send_http_requests
from common.myddt import ddt,data
# from common.test_data_handler import get_test_data_from_excel
from common.test_data_handler import get_test_data_from_excel,generate_no_use_phone
from common import logger,db
#从excel中提取用例数据,因为是通过main.py执行,故文件路径是相对main.py文件的
cases=get_test_data_from_excel(settings.TEST_DATA_FILE,'register')
@ddt
class TestRegister(unittest.TestCase):
logger=logger
db=db
@classmethod
def setUpClass(cls) -> None:
#类方法调用
cls.logger.info('注册接口开始测试')
@classmethod
def tearDownClass(cls) -> None:
# 类方法调用
cls.logger.info('注册接口测试结束')
#ddt中data修饰测试用例,并将case列表解包传入 ,解包后的每一个元素为一个用例
@data(*case)
def test_register(self,case):
#对象.属性调用日志方法
self.logger.info('用例[{}]开始测试>>>>>>>'.format(case['title']))
#1.测试数据
#判断是否要生成手机号
if '#phone#' in case['request']:
#要动态生成手机号码
phone=generate_no_use_phone()
#替换槽位
case['request']=case['request'].replace('#phone#',phone)
#替换一下sql
if case['sql']:
case['sql']=case['sql'].replace('#phone#',phone)
#将json数据转换成python对象
case['request']=json.loads(case['request'])
case['expect_data'] = json.loads(case['expect_data'])
#2.测试步骤
#方便调试的时候查看发送的请求内容
self.logger.debug('url:{},method:{},request:{}'.format(case['url'],case['method'],case['request']))
#调用send_http_requests方法发送请求
re=send_http_requests(url=case['url'],method=case['method'],**case['request'])
#3.断言
re_data=re.json()
#3.1断言状态码
try:
self.assertEqual(200,re.status_code)
except AssertionError as e:
#将报错内容输出到控制台,并打印错误日志
#self.logger.warn('状态码断言失败',exc_info=e)
#将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('状态码断言失败')
raise e
#定义无异常时打印日志内容
else:
self.logger.info('状态码断言成功')
#3.2响应结果断言
res={'code':re_data['code'],'msg':re_data['msg']}
try:
self.assertEqual(case['expect_data'],res)
#当断言失败时,抛出异常,输出日志
except AssertionError as e:
self.logger.exception('请求结果断言失败')
self.logger.debug('期望数据:{}'.format(case['expect_data']))
self.logger.debug('实际数据:{}'.format(res))
self.logger.debug('响应数据:{}'.format(re_data))
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('请求结果断言成功')
#3.3校验数据库
if case['sql']:
#查询数据
try:
db_res=self.db.exist(case['sql'])
self.assertTrue(db_res)
except Exception as e:
self.logger.exception('数据库断言失败')
self.logger.debug('执行的sql是:{}'.format(case['sql']))
raise e
else:
self.logger.info('数据库断言成功')
self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))
if __name__ == '__main__':
unittest.main()
不同测试环境中项目的url路径不同,为提高项目的复用性,对项目代码进行优化。
在settings.py文件中新增如下内容
#项目域名
PROJECT_HOST='http://test.lemonban.com/futureloan/mvc/api'
#接口地址,将/符号放在接口地址中
INTERFACES={
'register':'/member/register',
'login':'/member/login',
'recharge':'/member/recharge'
}
修改test_data文件夹下test_data.xlsx表格中url
在代码中对配置的url进行处理(测试数据处理处新增代码)
修改 test_cases下test_register.py文件,使用设置中的 PROJECT_HOST 和 INTERFACES 动态的拼接url。
# 动态拼接url
case['url'] = settings.PROJECT_HOST + settings.INTERFACES[case['url']]
import json
import unittest
import settings
from common.make_request import send_http_requests
from common.myddt import ddt,data
from common.test_data_handler import get_test_data_from_excel,generate_no_use_phone
from common import logger,db
#从excel中提取用例数据
case=get_test_data_from_excel(settings.TEST_DATA_FILE,'register')
@ddt
class TestRegister(unittest.TestCase):
logger=logger
db=db
@classmethod
def setUpClass(cls) -> None:
#类方法调用
cls.logger.info('注册接口开始测试')
@classmethod
def tearDownClass(cls) -> None:
# 类方法调用
cls.logger.info('注册接口测试结束')
#ddt中data修饰测试用例,并将case列表解包传入 ,解包后的每一个元素为一个用例
@data(*case)
def test_register(self,case):
#对象.属性调用日志方法
self.logger.info('用例[{}]开始测试>>>>>>>'.format(case['title']))
#1.测试数据
#判断是否要生成手机号
if '#phone#' in case['request']:
#要动态生成手机号码
phone=generate_no_use_phone()
#替换槽位
case['request']=case['request'].replace('#phone#',phone)
#替换一下sql
if case['sql']:
case['sql']=case['sql'].replace('#phone#',phone)
#将json数据转换成python对象
case['request']=json.loads(case['request'])
case['expect_data'] = json.loads(case['expect_data'])
#拼接url
case['url']=settings.PROJECT_HOST+settings.INTERFACES[case['url']]
#2.测试步骤
#方便调试的时候查看发送的请求内容
self.logger.debug('url:{},method:{},request:{}'.format(case['url'],case['method'],case['request']))
#调用send_http_requests方法发送请求
re=send_http_requests(url=case['url'],method=case['method'],**case['request'])
#3.断言
re_data=re.json()
#3.1断言状态码
try:
self.assertEqual(200,re.status_code)
except AssertionError as e:
#将报错内容输出到控制台,并打印错误日志
#self.logger.warn('状态码断言失败',exc_info=e)
#将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('状态码断言失败')
raise e
#定义无异常时打印日志内容
else:
self.logger.info('状态码断言成功')
#3.2响应结果断言
res={'code':re_data['code'],'msg':re_data['msg']}
try:
self.assertEqual(case['expect_data'],res)
#当断言失败时,抛出异常,输出日志
except AssertionError as e:
self.logger.exception('请求结果断言失败')
self.logger.debug('期望数据:{}'.format(case['expect_data']))
self.logger.debug('实际数据:{}'.format(res))
self.logger.debug('响应数据:{}'.format(re_data))
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('请求结果断言成功')
#3.3校验数据库
if case['sql']:
#查询数据
try:
db_res=self.db.exist(case['sql'])
self.assertTrue(db_res)
except Exception as e:
self.logger.exception('数据库断言失败')
self.logger.debug('执行的sql是:{}'.format(case['sql']))
raise e
else:
self.logger.info('数据库断言成功')
self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))
if __name__ == '__main__':
unittest.main()
接口测试中存在接口依赖,发送请求接口时需要携带上一个接口返回的数据。
可以将多个需要按顺序执行的用例放在一个测试用例类中,将前一个接口返回的数据保存在类属性中,后一个接口通过共同的类获取对应的属性,从而解决接口依赖。
充值接口分析:
充值接口相较于注册接口:
增加前置条件:注册用户、登录用户
权限验证
项目中除注册、登录接口外,其他接口都需要注册用户、登录用户的前置条件,故对注册和登录功能进行封装。
#在common文件夹下新建fixture.py文件
import requests
import settings
from common import logger
#名称定义为_type可以避免与python中关键参数冲突
def register(mobilephone,pwd,reg_name=None,_type=None):
'''
注册用户
:param mobile_phone: 电话号码
:param pwd: 密码
:param reg_name: 昵称
:param _type: 类型
:return:
'''
#1.构造发送注册请求的参数
data={
'mobilephone':mobilephone,
'pwd':pwd
}
if reg_name:
data['reg_name']=reg_name
#使用if _type:进行判断时,传值为0,无法传入,故判断不为空
if _type is not None:
data['type']=_type
headers={"x-Lemonban-Media-Type": "lemonban.v2"}
url=settings.PROJECT_HOST+settings.INTERFACES['register']
try:
res=requests.post(url=url,data=data,headers=headers)
if res.json()['msg'] == '注册成功':
logger.info('注册用户成功')
return True
return False
except Exception as e:
logger.exception('注册用户失败')
raise e
def login(mobilephone,pwd):
'''
登录用户
:param mobilephone: 用户手机号
:param pwd: 用户密码
:return:
'''
#构造请求数据
data = {
'mobilephone': mobilephone,
'pwd': pwd
}
headers = {'x-Lemonban-Media-Type':'lemonban.v2'}
url = settings.PROJECT_HOST + settings.INTERFACES['login']
try:
res=requests.post(url=url,data=data,headers=headers)
if res.json()['msg'] == '登录成功':
logger.info('登录用户成功')
return res.json()
except Exception as e:
logger.exception('登录用户失败')
raise e
if __name__ == '__main__':
from common.test_data_handler import generate_no_use_phone
phone=generate_no_use_phone()
pwd='12345678'
register_res=register(mobilephone=phone,pwd=pwd)
if register_res:
login_res=login(mobilephone=phone,pwd=pwd)
print(login_res)
else:
print('注册失败')
在testcase文件夹下新建test_recharge.py文件
import json
import unittest
import settings
from common import logger,db
from common.fixture import register,login
from common.make_request import send_http_requests
from common.test_data_handler import generate_no_use_phone,get_test_data_from_excel
from common.myddt import ddt,data
cases=get_test_data_from_excel(settings.TEST_DATA_FILE,'recharge')
@ddt
class TestRcharge(unittest.TestCase):
logger=logger
db=db
@classmethod
def setUpClass(cls) -> None:
cls.logger.info('充值接口开始测试')
#类前置
#1.注册一个普通用户
mobile_phone=generate_no_use_phone()
pwd='12345678'
if not register(mobilephone=mobile_phone,pwd=pwd):
cls.logger.error('注册用户{}失败'.format(mobile_phone))
raise ValueError('注册用户{}失败'.format(mobile_phone))
cls.logger.info('注册用户{}成功'.format(mobile_phone))
#2.登录这个用户
data=login(mobilephone=mobile_phone,pwd=pwd)
if data is None:
cls.logger.error('登录用户{}失败'.format(mobile_phone))
raise ValueError('登录用户{}失败'.format(mobile_phone))
cls.logger.info('登录用户{}成功'.format(mobile_phone))
#3.保存需要的数据能够在用例中使用
#为了在用例间传递数据,我们将数据保存在类属性中
#cls.phone=mobile_phone
cls.member_id = data['id']
cls.token = data['token_info']['token']
@classmethod
def tearDownClass(cls) -> None:
# 类方法调用
cls.logger.info('充值接口测试结束')
@data(*cases)
def test_recharge(self,case):
self.logger.info('用例【{}】开始测试>>>>>'.format(case['title']))
#1.用例数据处理
# 替换member_id
if "#member_id#" in case['request']:
case['request'] = case['request'].replace('#member_id#',str(self.member_id))
# 替换token
if '#token' in case['request']:
case['request'] = case['request'].replace('#token#', self.token)
# 再将替换好的数据转换成python对象
case['request'] = json.loads(case['request'])
# 期望结果也转换成python对象
case['expect_data'] = json.loads(case['expect_data'])
# #替换数据
# #替换phone
# case['request']=case['request'].replace('#phone#',str(self.phone))
# #替换sql
# if case['sql']:
# case['sql']=case['sql'].replace('#phone#',str(self.phone))
#将json字符串转换为python对象
case['request']=json.loads(case['request'])
case['expect_data'] = json.loads(case['expect_data'])
#拼接url
case['url']=settings.PROJECT_HOST+settings.INTERFACES[case['url']]
# 2.测试步骤
# 方便调试的时候查看发送的请求内容
self.logger.debug('url:{},method:{},request:{}'.format(case['url'], case['method'], case['request']))
# 调用send_http_requests方法发送请求
re = send_http_requests(url=case['url'], method=case['method'], **case['request'])
# 3.断言
re_data = re.json()
# 3.1断言状态码
try:
self.assertEqual(200, re.status_code)
except AssertionError as e:
# 将报错内容输出到控制台,并打印错误日志
# self.logger.warn('状态码断言失败',exc_info=e)
# 将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('状态码断言失败')
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('状态码断言成功')
# 3.2响应结果断言
res = {'code': re_data['code'], 'msg': re_data['msg']}
try:
self.assertEqual(case['expect_data'], res)
# 当断言失败时,抛出异常,输出日志
except AssertionError as e:
self.logger.exception('请求结果断言失败')
self.logger.debug('期望数据:{}'.format(case['expect_data']))
self.logger.debug('实际数据:{}'.format(res))
self.logger.debug('响应数据:{}'.format(re_data))
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('请求结果断言成功')
# 3.3校验数据库
if case['sql']:
# 查询数据
try:
db_res = self.db.exist(case['sql'])
self.assertTrue(db_res)
except Exception as e:
self.logger.exception('数据库断言失败')
self.logger.debug('执行的sql是:{}'.format(case['sql']))
raise e
else:
self.logger.info('数据库断言成功')
self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))
if __name__ == '__main__':
unittest.main()
通过类属性实现了接口依赖数据的传递,但在传递过程中将属性名写死,无法动态替换测试数据中的参数,无法动态根据类属性进行接口依赖数据的传递。
设计思路:
用例数据中的槽位名要和类属性名一致
找出用例数据中所有槽位
根据槽位依次获取类中对应属性,存在则替换用例数据中相应槽位
通过正则表达式可以判断给定的字符串是否符合正则表达式的过滤逻辑(邮箱、电话号码),或通过正则表达式从字符串中获取我们想要的特定部分。
python中正则表达式需要使用re库
import re
s1 = "testingtest\n123"
s2 = "Testingtest123"
#普通字符
# 表示在s1中找到"test"的所有字符串
r = re.findall("test", s1)
print(r)
#表示在s2中找到"test"的所有字符串,对大小写敏感
r = re.findall("test", s2)
print(r)
#表示在s2中找到"test"的所有字符串,添加re.I,对大小写不敏感
r = re.findall("test", s2,re.I)
print(r)
#元字符
#通配符. 匹配除\n之外的任何单个字符
r=re.findall('.',s1)
print(r)
#修饰符re.S使.匹配包括换行在内的所有字符
r=re.findall('.',s1,re.S)
print(r)
#重复元字符
# * 匹配前面的子表达式任意次
s1 = "z\nzo\nzoo"
# 匹配o,o为0到任意次,当值不为o时,往下重新匹配
r = re.findall("zo*", s1)
print(r)
# + 匹配前面的子表达式一次或多次(至少一次)
r = re.findall("zo+", s1)
print(r)
# ? 匹配前面的子表达式0次或1次
r = re.findall("zo?", s1)
print(r)
#贪婪模式,元字符*,+会尽可能多的匹配前面的子表达式
s = "abcadcaec"
r = re.findall(r"ab.*c", s) # 贪婪模式,尽可能多的匹配字符(.*或者.+)
print(r)
# ? 实现非贪婪模式,尽可能少的匹配字符
r = re.findall(r"ab.*?c", s) # 非贪婪模式,尽可能少的匹配字符
print(r)
r = re.findall(r"ab.+?c", s) # 非贪婪模式,尽可能少的匹配字符
print(r)
# () 分组元字符 将括号之间的表达式定义为组(group),并且将匹配这个子表达式的字符返回
s='phone:#phone#,member_id:#member_id#'
r=re.findall('#.*?#',s)
print(r)
r=re.findall('#(.*?)#',s)
print(r)
#在common文件夹下test_data_handler.py文件中新增如下内容
import re
def replace_args_by_re(json_s,obj):
'''
通过正则表达式动态的替换参数
:param json_s:包含替换内容的数据
:param obj:
:return:
'''
#1.找出所有的槽位中的变量名(若公司项目中#为特殊字符,可将用例和此次#替换为其他特殊符号
args=re.findall('#(.*?)#',json_s)
for arg in args:
#2.找到obj中对应的属性,若无对应的属性,则返回None
value=getattr(obj,arg,None)
if value:
json_s=json_s.replace('#{}#'.format(arg),str(value))
return json_s
if __name__ == '__main__':
class E:
name='zhangsan'
age=17
b='name is #name#,age is #age#'
res=replace_args_by_re(b,E)
print(res)
(因传入参数与视频讲解要求不一致,仅参考写法)
test_cases文件夹中test_recharge.py 模块中导入上面的函数,然后修改替换槽位的代码如下:
from common.test_data_handler import (generate_no_use_phone,
get_test_data_from_excel,
replace_args_by_re)
#替换member_id
if "#member_id#" in case['request']:
case['request'] = case['request'].replace('#member_id#',str(self.member_id))
# 替换token
if '#token#' in case['request']:
case['request'] = case['request'].replace('#token#', self.token)
修改为
#传入对象属性(对象属性继承类属性)
case['request'] = replace_args_by_re(case['request'], self)
if case['sql']:
case['sql'] = replace_args_by_re(case['sql'], self)
审核项目接口的前提条件是:
1.注册普通用户
2.登录普通用户
3.普通用户创建项目
4.注册管理员账户
5.登录管理员账号
6.管理员审核项目
类级前置条件是在一个测试用例类中的所有用例前执行一次,定义在setUpClass中。
方法级前置条件是在一个测试用例类中的每一个单元测试函数前都要执行一次,定义在setUp方法中。
方法级前置条件:创建项目
在common文件夹下fixture.py文件中新增如下代码
def add_loan(member_id, token, title='借钱实现财富自由', amount=5000,
loan_rate=12.0, loan_term=3, loan_date_type=1, bidding_days=5):
"""
添加一个项目
:param member_id:
:param token:
:param title:
:param amount:
:param loan_rate:
:param loan_term:
:param loan_date_type:
:param bidding_days:
:return:
"""
data = {
'member_id': member_id,
'title': title,
'amount': amount,
'loan_rate': loan_rate,
'loan_term': loan_term,
'loan_date_type': loan_date_type,
'bidding_days': bidding_days,
}
headers = {"X-Lemonban-Media-Type": "lemonban.v2", "Authorization": token}
url = settings.PROJECT_HOST + settings.INTERFACES['add']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
logger.info('创建项目成功')
return res.json()['data']
except Exception as e:
logger.exception('创建项目失败')
raise e
# 接口地址
INTERFACES = {
...
'add': '/loan/add',
'audit': '/loan/audit'
}
在testcase文件夹下新增test_audit.py文件
import json
import unittest
import settings
from common import logger,db
from common.fixture import register,login,add_loan
from common.make_request import send_http_requests
from common.test_data_handler import (generate_no_use_phone,
get_test_data_from_excel,
replace_args_by_re)
from common.myddt import ddt,data
cases=get_test_data_from_excel(settings.TEST_DATA_FILE,'audit')
@ddt
class TestRcharge(unittest.TestCase):
logger=logger
db=db
@classmethod
def setUpClass(cls) -> None:
cls.logger.info('审核接口开始测试')
#类前置
#1.注册一个普通用户
mobile_phone=generate_no_use_phone()
pwd='12345678'
if not register(mobilephone=mobile_phone,pwd=pwd):
cls.logger.error('注册用户{}失败'.format(mobile_phone))
raise ValueError('注册用户{}失败'.format(mobile_phone))
cls.logger.info('注册用户{}成功'.format(mobile_phone))
#2.登录这个用户
data=login(mobilephone=mobile_phone,pwd=pwd)
if data is None:
cls.logger.error('登录用户{}失败'.format(mobile_phone))
raise ValueError('登录用户{}失败'.format(mobile_phone))
cls.logger.info('登录用户{}成功'.format(mobile_phone))
# 3. 保存需要的数据能够在这个类的生命周期类共享
# 将数据保存在类属性中
# 普通用户的member_id和token
cls.normal_member_id = data['id']
cls.normal_token = data['token_info']['token']
# 4. 注册一个管理员用户
mobile_phone = generate_no_use_phone()
if not register(mobilephone=mobile_phone, pwd=pwd, _type=0):
cls.logger.error('注册管理员用户{}失败'.format(mobile_phone))
raise ValueError('注册管理员用户{}失败'.format(mobile_phone))
cls.logger.info('注册管理员用户{}成功'.format(mobile_phone))
# # 5. 登录管理员用户
data = login(mobilephone=mobile_phone, pwd=pwd)
if data is None:
cls.logger.error('登录管理员用户{}失败'.format(mobile_phone))
raise ValueError('登录管理员用户{}失败'.format(mobile_phone))
# 6. 保存数据
# 管理员用户的token
cls.token = data['token_info']['token']
def setUp(self) -> None:
'''
方法级前置,每个用例前创建一个项目
:return:
'''
res=add_loan(member_id=self.normal_member_id, token=self.normal_token)
if res:
self.logger.info('添加项目成功')
# 保存项目id到类属性中,传递给当前的测试方法
# 属性名loan_id与用例数据中的槽位名一致
self.__class__.loan_id = res['id']
else:
self.logger.error('添加项目失败!')
raise ValueError('添加项目失败')
@classmethod
def tearDownClass(cls) -> None:
# 类方法调用
cls.logger.info('审核接口测试结束')
@data(*cases)
def test_audit(self,case):
self.logger.info('用例【{}】开始测试>>>>>'.format(case['title']))
#1.用例数据处理
#替换数据
case['request']=replace_args_by_re(case['request'],self)
#替换phone
case['request']=case['request'].replace('#phone#',str(self.phone))
#替换sql
if case['sql']:
case['sql']=case['sql'].replace('#phone#',str(self.phone))
#将json字符串转换为python对象
case['request']=json.loads(case['request'])
case['expect_data'] = json.loads(case['expect_data'])
#拼接url
case['url']=settings.PROJECT_HOST+settings.INTERFACES[case['url']]
# 2.测试步骤
# 方便调试的时候查看发送的请求内容
self.logger.debug('url:{},method:{},request:{}'.format(case['url'], case['method'], case['request']))
# 调用send_http_requests方法发送请求
re = send_http_requests(url=case['url'], method=case['method'], **case['request'])
# 3.断言
re_data = re.json()
# 3.1断言状态码
try:
self.assertEqual(200, re.status_code)
except AssertionError as e:
# 将报错内容输出到控制台,并打印错误日志
# self.logger.warn('状态码断言失败',exc_info=e)
# 将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('状态码断言失败')
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('状态码断言成功')
# 3.2响应结果断言
res = {'code': re_data['code'], 'msg': re_data['msg']}
try:
self.assertEqual(case['expect_data'], res)
# 当断言失败时,抛出异常,输出日志
except AssertionError as e:
self.logger.exception('请求结果断言失败')
self.logger.debug('期望数据:{}'.format(case['expect_data']))
self.logger.debug('实际数据:{}'.format(res))
self.logger.debug('响应数据:{}'.format(re_data))
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('请求结果断言成功')
# 3.3校验数据库
if case['sql']:
# 查询数据
try:
db_res = self.db.exist(case['sql'])
self.assertTrue(db_res)
except Exception as e:
self.logger.exception('数据库断言失败')
self.logger.debug('执行的sql是:{}'.format(case['sql']))
raise e
else:
self.logger.info('数据库断言成功')
self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))
观察发现审核接口用例测试类中测试方法的代码与充值接口用例测试类中测试方法的代码完全一致,说明测试逻辑相同,重复代码可以通过抽象成基类进行共享,实现代码复用。
抽取测试用例基类思路:
1.将公用模块都封装到基类中便于子类直接调用
日志器、数据库处理器、项目配置
2.将测试方法中的每个步骤单独封装成一个对象方法
测试数据处理、测试步骤、响应状态码断言、响应数据断言、数据库断言
3.为了对响应状态码进行解耦,在测试用例数据中增加一列status_code来校验响应状态码
不同项目请求成功返回响应的状态码可能不一致,为了实现解耦
故在测试数据中增加一行status_code
修改test_cases文件夹下测试的所有.py文件
修改前:
# 3.1断言状态码
try:
self.assertEqual(200, re.status_code)
except AssertionError as e:
# 将报错内容输出到控制台,并打印错误日志
# self.logger.warn('状态码断言失败',exc_info=e)
# 将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('状态码断言失败')
raise e
修改后:
# 3.1断言状态码
try:
self.assertEqual(case['status_code'], re.status_code)
except AssertionError as e:
# 将报错内容输出到控制台,并打印错误日志
# self.logger.warn('状态码断言失败',exc_info=e)
# 将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('状态码断言失败')
raise e
将公用模块都封装到基类中便于子类直接调用:日志器、数据库处理器、项目配置
将测试方法中的每个步骤单独封装成一个对象方法:测试数据处理、测试步骤、响应状态码断言、响应数据断言、数据库断言
#在testcase文件夹下新建base_case.py文件,内容如下
import json
import unittest
import settings
from common import logger,db
from common.make_request import send_http_requests
from common.test_data_handler import (
replace_args_by_re,
generate_no_use_phone
)
class BaseCase(unittest.TestCase):
name='base用例' #这个属性应该被覆盖
logger=logger
db=db
settings=settings
@classmethod
def setUpClass(cls) -> None:
cls.logger.info('{}接口开始测试'.format(cls.name))
@classmethod
def tearDownClass(cls) -> None:
cls.logger.info('{}接口结束测试'.format(cls.name))
def checkout(self,case):
#绑定对象属性,便于下面的测试流程函数去处理
self.case=case
self.logger.info('用例[{}]开始测试>>>>>>>'.format(case['title']))
# 1.测试数据处理
self.pre_test_data()
# 2.测试步骤
self.step()
# 3.响应状态码断言
self.assert_status_code()
# 4.响应数据断言
self.assert_json_response()
# 5.数据库断言
self.assert_db_true()
self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))
def pre_test_data(self):
'''
预处理数据
:return:
'''
# 1.用例数据处理
# 替换数据
self.case['request'] = replace_args_by_re(self.case['request'], self)
# 替换一下sql
if self.case.get('sql'):
self.case['sql'] = replace_args_by_re(self.case['sql'], self)
# 判断是否要生成手机号码
if '#phone#' in self.case['request']:
# 要动态生成手机号码
phone = generate_no_use_phone()
# 替换槽位,因未将phone定义为属性,故直接替换
self.case['request'] = self.case['request'].replace('#phone#', phone)
# 替换一下sql
if self.case.get('sql'):
self.case['sql'] = self.case['sql'].replace('#phone#', phone)
# 将json字符串转换为python对象
try:
self.case['request'] = json.loads(self.case['request'])
self.case['expect_data'] = json.loads(self.case['expect_data'])
except Exception as e:
self.logger.exception('用例[{}]json格式有误'.format(self.case['title']))
self.logger.debug('case["request"]:{}'.format(self.case['request']))
self.logger.debug('case["expect_data"]:{}'.format(self.case['expect_data']))
raise ValueError('用例[{}]json格式有误'.format(self.case['title']))
# 拼接url
self.case['url'] = settings.PROJECT_HOST + settings.INTERFACES[self.case['url']]
def step(self):
'''
测试步骤
:return:
'''
#发送请求,并将响应信息保存为对象属性
try:
self.response=send_http_requests(url=self.case['url'], method=self.case['method'], **self.case['request'])
except Exception as e:
self.logger.exception('用例[{}]发送http请求错误'.format(self.case['title']))
self.logger.debug('url:{},method:{},request:{}'.format(self.case['url'], self.case['method'], self.case['request']))
raise e
def assert_status_code(self):
'''
响应状态码断言
:return:
'''
try:
self.assertEqual(self.case['status_code'], self.response.status_code)
except AssertionError as e:
# 将报错内容输出到控制台,并打印错误日志
# self.logger.warn('状态码断言失败',exc_info=e)
# 将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('用例[{}]状态码断言失败'.format(self.case['title']))
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('用例[{}]状态码断言成功'.format(self.case['title']))
def assert_json_response(self):
'''
断言json响应数据(根据项目实际情况改写)
:return:
'''
re_data=self.response.json()
#注:此处仅针对演示项目,逻辑写死,需根据实际项目改写代码
#1.拼装实际结果字典
res = {'code': re_data['code'], 'msg': re_data['msg']}
try:
self.assertEqual(self.case['expect_data'], res)
# 当断言失败时,抛出异常,输出日志
except AssertionError as e:
self.logger.exception('用例[{}]请求json结果断言失败'.format(self.case['title']))
self.logger.debug('期望数据:{}'.format(self.case['expect_data']))
self.logger.debug('实际数据:{}'.format(res))
self.logger.debug('响应数据:{}'.format(re_data))
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('用例[{}]请求json结果断言成功'.format(self.case['title']))
def assert_db_true(self):
'''
断言数据库存在数据
:return:
'''
if self.case.get('sql'):
#查询数据
try:
db_res = self.db.exist(self.case['sql'])
self.assertTrue(db_res)
except Exception as e:
self.logger.exception('用例[{}]数据库断言失败'.format(self.case['title']))
self.logger.debug('执行的sql是:{}'.format(self.case['sql']))
raise e
else:
self.logger.info('数据库断言成功')
from base_case import BaseCase
from common.myddt import ddt,data
from common.test_data_handler import get_test_data_from_excel
#从excel中提取用例数据
case=get_test_data_from_excel(BaseTest.settings.TEST_DATA_FILE,'register')
@ddt
#继承测试用例基类
class TestRegister(BaseCase):
#重新定义名字
name='注册'
#ddt中data修饰测试用例,并将case列表解包传入 ,解包后的每一个元素为一个用例
@data(*case)
def test_register(self,case):
self.checkout(case)
from base_case import BaseCase
from common.fixture import register,login
from common.test_data_handler import (generate_no_use_phone,
get_test_data_from_excel)
from common.myddt import ddt,data
cases=get_test_data_from_excel(BaseTest.settings.TEST_DATA_FILE,'recharge')
@ddt
class TestRcharge(BaseCase):
name='审计'
@classmethod
def setUpClass(cls) -> None:
cls.logger.info('充值接口开始测试')
#类前置
#1.注册一个普通用户
mobile_phone=generate_no_use_phone()
pwd='12345678'
if not register(mobilephone=mobile_phone,pwd=pwd):
cls.logger.error('注册用户{}失败'.format(mobile_phone))
raise ValueError('注册用户{}失败'.format(mobile_phone))
cls.logger.info('注册用户{}成功'.format(mobile_phone))
#2.登录这个用户
data=login(mobilephone=mobile_phone,pwd=pwd)
if data is None:
cls.logger.error('登录用户{}失败'.format(mobile_phone))
raise ValueError('登录用户{}失败'.format(mobile_phone))
cls.logger.info('登录用户{}成功'.format(mobile_phone))
#3.保存需要的数据能够在用例中使用
#可通过该方式将响应信息中需要的数据保存为类属性
# cls.member_id = data['id']
# cls.token = data['token_info']['token']
#为了在用例间传递数据,我们将数据保存在类属性中
cls.phone=mobile_phone
@data(*cases)
def test_recharge(self,case):
self.checkout(case)
from testcase.base_case import BaseCase
from common.myddt import ddt, data
from common.fixture import register, login, add_loan
from common.test_data_handler import (
generate_no_use_phone,
get_test_data_from_excel,
)
# 从excel中提取用例数据
cases = get_test_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'audit')
class TestRcharge(BaseCase):
name='审核'
@classmethod
def setUpClass(cls) -> None:
cls.logger.info('审核接口开始测试')
#类前置
#1.注册一个普通用户
mobile_phone=generate_no_use_phone()
pwd='12345678'
if not register(mobilephone=mobile_phone,pwd=pwd):
cls.logger.error('注册用户{}失败'.format(mobile_phone))
raise ValueError('注册用户{}失败'.format(mobile_phone))
cls.logger.info('注册用户{}成功'.format(mobile_phone))
#2.登录这个用户
data=login(mobilephone=mobile_phone,pwd=pwd)
if data is None:
cls.logger.error('登录用户{}失败'.format(mobile_phone))
raise ValueError('登录用户{}失败'.format(mobile_phone))
cls.logger.info('登录用户{}成功'.format(mobile_phone))
# 3. 保存需要的数据能够在这个类的生命周期类共享
# 将数据保存在类属性中
# 普通用户的member_id和token
cls.normal_member_id = data['id']
cls.normal_token = data['token_info']['token']
# 4. 注册一个管理员用户
mobile_phone = generate_no_use_phone()
if not register(mobilephone=mobile_phone, pwd=pwd, _type=0):
cls.logger.error('注册管理员用户{}失败'.format(mobile_phone))
raise ValueError('注册管理员用户{}失败'.format(mobile_phone))
cls.logger.info('注册管理员用户{}成功'.format(mobile_phone))
# # 5. 登录管理员用户
data = login(mobilephone=mobile_phone, pwd=pwd)
if data is None:
cls.logger.error('登录管理员用户{}失败'.format(mobile_phone))
raise ValueError('登录管理员用户{}失败'.format(mobile_phone))
# 6. 保存数据
# 管理员用户的token
cls.token = data['token_info']['token']
def setUp(self) -> None:
'''
方法级前置,每个用例前创建一个项目
:return:
'''
#普通用户创建项目
res=add_loan(member_id=self.normal_member_id, token=self.normal_token)
if res:
self.logger.info('添加项目成功')
# 保存项目id到类属性中,传递给当前的测试方法
# 属性名loan_id与用例数据中的槽位名一致
self.__class__.loan_id = res['id']
else:
self.logger.error('添加项目失败!')
raise ValueError('添加项目失败')
@data(*cases)
def test_audit(self,case):
self.checkout(case)
当测试接口过多时,可在testcase文件夹下新建test_**__module.py文件,分模块编写测试用例,模块中每个用例为单独的一个类。**
在做接口自动化时,往往需要先测通核心业务流,再进行单接口测试。
接口测试业务流设计
站在用户角度
重视全局而非细节
先测主流程,后测分流程
只测正例
优点:逻辑清晰简答
缺点:当业务流很长时代码量庞大,业务发生改变需修改代码
示例:贷款流程
注册普通融资用户
登陆普通融资用户
普通用户创建项目
注册管理员用户
登录管理员用户
管理员审核项目
from testcase.base_case import BaseCase
class TestLoanFlow(BaseCase):
name = '贷款业务流'
def test_01register_normal_user(self):
'''
注册普通用户
:return:
'''
case = {
'title': '注册普通用户',
'url': 'register',
'method': 'post',
'request': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":#phone#,"pwd":"12345678"}}',
'status_code': 200,
'expect_data': '{"code":0,"msg":"OK"}',
'sql': 'SELECT id from member where mobile_phone = #phone#'
}
self.checkout(case)
# 如果通过了就将电话号码保存到类属性中,共享给下一个用例
self.__class__.normal_mobile_phone = self.case['request']['json']['mobile_phone']
def test_02login_normal_user(self):
'''
普通用户登录
:return:
'''
case = {
'title': '普通用户登录',
'url': 'login',
'method': 'post',
'request': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json":{"mobile_phone":#normal_mobile_phone#,"pwd":"12345678"}}',
'status_code': 200,
'expect_data': '{"code":0,"msg":"OK"}',
}
self.checkout(case)
# 如果通过了就将用户id,token保存到类属性中,共享给下一个用例
self.__class__.normal_member_id = self.response.json()['data']['id']
self.__class__.normal_token = self.response.json()['data']['token_info']['token']
def test_03add_loan(self):
'''
添加项目
:return:
'''
case = {
'title': '添加项目',
'url': 'add',
'method': 'post',
'request': '''
{
"headers": {"X-Lemonban-Media-Type":
"lemonban.v2","Authorization":"Bearer #normal_token#"},
"json":{
"member_id":#normal_member_id#,
"title":"实现财富自由",
"amount":5000,
"loan_rate":18.0,
"loan_term":6,
"loan_date_type":1,
"bidding_days":10}
}
''',
'status_code': 200,
'expect_data': '{"code":0,"msg":"OK"}'
}
self.checkout(case)
# 如果通过了就将loan_id保存到类属性中,共享给后面的用例
self.__class__.loan_id = self.response.json()['data']['id']
def test_04register_admin_user(self):
'''
注册管理员用户
:return:
'''
case = {
'title': '注册管理员用户',
'url': 'register',
'method': 'post',
'request': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json":{"mobile_phone":#phone#,"pwd":"12345678","type":0}}',
'status_code': 200,
'expect_data': '{"code":0,"msg":"OK"}',
'sql': 'SELECT id from member where mobile_phone = #phone#'
}
self.checkout(case)
# 如果通过了就将电话号码保存到类属性中,共享给下一个用例
self.__class__.admin_mobile_phone = self.case['request']['json']['mobile_phone']
def test_05login_admin_user(self):
'''
管理员用户登录
:return:
'''
case = {
'title': '管理员用户登录',
'url': 'login',
'method': 'post',
'request': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json":{"mobile_phone":#admin_mobile_phone#,"pwd":"12345678"}}',
'status_code': 200,
'expect_data': '{"code":0,"msg":"OK"}',
}
self.checkout(case)
# 如果通过了就将token保存到类属性中,共享给下一个用例
self.__class__.admin_token = self.response.json()['data']['token_info']['token']
def test_06audit_loan(self):
'''
审核项目
:return:
'''
case = {
'title': '审核项目',
'url': 'audit',
'method': 'patch',
'request': '''
{
"headers": {"X-Lemonban-Media-Type":
"lemonban.v2","Authorization":"Bearer #admin_token#"},
"json":{"loan_id":#loan_id#,"approved_or_not":true}
}
''',
'status_code': 200,
'expect_data': '{"code":0,"msg":"OK"}'
}
self.checkout(case)
观察发现,测试用例类中根据业务流定义对应的单元测试方法时基本流程不变,只有在接口依赖处提取了响应中的数据并保存到类属性中提供给后一个测试用例访问。
优点:代码复用性高,基本流程不变;当业务流发生改变时,只需要修改用例数据不需要修改代码。
缺点:学习成本高,需要对代码逻辑了解才能编写用例
在用例数据中添加一列extract表示提取响应数据并保存在对应的类属性中,规则如下
[{"name":"member_id","exp":"$..id"},{"name":"token","exp":"$..token"}]
上面的提取规则中,name表示绑定到到类中的属性名,exp表示从响应数据中提取的数据表达式。
功能分析:动态提取数据,动态绑定数据,通过jsonpath获取返回中数据进行处理
jsonpath的安装:
#在dos命令窗口
pip install jsonpath
jsonpath | 描述 |
---|---|
$ | 根节点 |
@ | 当前元素 |
. 或[] | 下一个节点 |
... | 不考虑位置,符合条件的元素 |
* | 匹配所有元素节点 |
[] | 数组下标,根据内容选值 |
jsonpath输出的是一个列表[]
from jsonpath import jsonpath
a={ "store": {
"book": [
{ "category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{ "category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{ "category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{ "category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}
#获取根节点下中store的值
print(jsonpath(a,'$.store'))
#获取根节点下中store下bicycle下price的值
print(jsonpath(a,'$.store.bicycle.price'))
#获取根节点下中store下book的第一个值
print(jsonpath(a,'$.store.book[0]'))
#获取根节点下中store下book下所有的price值
print(jsonpath(a,'$.store.book[*].price'))
#获取数据中所有的price值
print(jsonpath(a,'$..price'))
#获取根节点下中store下中子节点的值
print(jsonpath(a,'$.store.*'))
#获取根节点下中store下book的最后一个值
print(jsonpath(a,'$..book[-1:]'))
#获取根节点下中store下book的最后一个值
# @.length获取当前元素长度
print(jsonpath(a,'$..book[(@.length-1)]'))
#获取根节点下中store下book的第一个和第三个值
print(jsonpath(a,'$..book[0,2]'))
#获取根节点下中store下book中包含isbn的值
print(jsonpath(a,'$..book[?(@.isbn)]'))
#获取根节点下中store下book中price小于10的值
print(jsonpath(a,'$..book[?(@.price<10)]'))
#获取根节点下中store下所有值,递归取值
print(jsonpath(a,'$..*'))
修改testcase文件夹下base_case.py文件( 新增extract_data函数)
import json
import unittest
from jsonpath import jsonpath
import settings
from common import logger,db
from common.make_request import send_http_requests
from common.test_data_handler import (
replace_args_by_re,
generate_no_use_phone
)
class BaseCase(unittest.TestCase):
name='base用例' #这个属性应该被覆盖
logger=logger
db=db
settings=settings
@classmethod
def setUpClass(cls) -> None:
cls.logger.info('{}接口开始测试'.format(cls.name))
@classmethod
def tearDownClass(cls) -> None:
cls.logger.info('{}接口结束测试'.format(cls.name))
def checkout(self,case):
#绑定对象属性,便于下面的测试流程函数去处理
self.case=case
self.logger.info('用例[{}]开始测试>>>>>>>'.format(case['title']))
# 1.测试数据处理
self.pre_test_data()
# 2.测试步骤
self.step()
# 3.响应状态码断言
self.assert_status_code()
# 4.响应数据断言
self.assert_json_response()
# 5.数据库断言
self.assert_db_true()
# 6.提取数据
self.extract_data()
self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))
def pre_test_data(self):
'''
预处理数据
:return:
'''
# 1.用例数据处理
# 替换数据
self.case['request'] = replace_args_by_re(self.case['request'], self)
# 替换一下sql
if self.case.get('sql'):
self.case['sql'] = replace_args_by_re(self.case['sql'], self)
# 判断是否要生成手机号码
if '#phone#' in self.case['request']:
# 要动态生成手机号码
phone = generate_no_use_phone()
# 替换槽位,因未将phone定义为属性,故直接替换
self.case['request'] = self.case['request'].replace('#phone#', phone)
# 替换一下sql
if self.case.get('sql'):
self.case['sql'] = self.case['sql'].replace('#phone#', phone)
# 将json字符串转换为python对象
try:
self.case['request'] = json.loads(self.case['request'])
self.case['expect_data'] = json.loads(self.case['expect_data'])
except Exception as e:
self.logger.exception('用例[{}]json格式有误'.format(self.case['title']))
self.logger.debug('case["request"]:{}'.format(self.case['request']))
self.logger.debug('case["expect_data"]:{}'.format(self.case['expect_data']))
raise ValueError('用例[{}]json格式有误'.format(self.case['title']))
# 拼接url
self.case['url'] = settings.PROJECT_HOST + settings.INTERFACES[self.case['url']]
def step(self):
'''
测试步骤
:return:
'''
#发送请求,并将响应信息保存为对象属性
try:
self.response=send_http_requests(url=self.case['url'], method=self.case['method'], **self.case['request'])
except Exception as e:
self.logger.exception('用例[{}]发送http请求错误'.format(self.case['title']))
self.logger.debug('url:{},method:{},request:{}'.format(self.case['url'], self.case['method'], self.case['request']))
raise e
def assert_status_code(self):
'''
响应状态码断言
:return:
'''
try:
self.assertEqual(self.case['status_code'], self.response.status_code)
except AssertionError as e:
# 将报错内容输出到控制台,并打印错误日志
# self.logger.warn('状态码断言失败',exc_info=e)
# 将报错内容输出到控制台,并打印错误日志,日志等级为error
self.logger.exception('用例[{}]状态码断言失败'.format(self.case['title']))
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('用例[{}]状态码断言成功'.format(self.case['title']))
def assert_json_response(self):
'''
断言json响应数据(根据项目实际情况改写)
:return:
'''
re_data=self.response.json()
#注:此处仅针对演示项目,逻辑写死,需根据实际项目改写代码
#1.拼装实际结果字典
res = {'code': re_data['code'], 'msg': re_data['msg']}
try:
self.assertEqual(self.case['expect_data'], res)
# 当断言失败时,抛出异常,输出日志
except AssertionError as e:
self.logger.exception('用例[{}]请求json结果断言失败'.format(self.case['title']))
self.logger.debug('期望数据:{}'.format(self.case['expect_data']))
self.logger.debug('实际数据:{}'.format(res))
self.logger.debug('响应数据:{}'.format(re_data))
raise e
# 定义无异常时打印日志内容
else:
self.logger.info('用例[{}]请求json结果断言成功'.format(self.case['title']))
def assert_db_true(self):
'''
断言数据库存在数据
:return:
'''
if self.case.get('sql'):
#查询数据
try:
db_res = self.db.exist(self.case['sql'])
self.assertTrue(db_res)
except Exception as e:
self.logger.exception('用例[{}]数据库断言失败'.format(self.case['title']))
self.logger.debug('执行的sql是:{}'.format(self.case['sql']))
raise e
else:
self.logger.info('数据库断言成功')
def extract_data(self):
'''
提取响应中的数据并保存到用例类属性中
:return:
'''
if self.case.get('extract'):
try:
exps=json.loads(self.case['extract'])
except Exception as e:
self.logger.exception('用例[{}]的extract提取表达式格式不正确'.format(self.case['title']))
self.logger.debug('case["extract"]:{}'.format(self.case['extract']))
raise ValueError('用例[{}]的extract提取表达式格式不正确'.format(self.case['title']))
for i in exps:
#要保存的类属性的名称
name=i['name']
#要提取数据的jsonpath表达式
exp=i['exp']
res=jsonpath(self.response.json(),exp)
if res:
#保存到类属性
setattr(self.__class__,name,res[0])
else:
raise ValueError('用例[{}]提取表达式错误'.format(self.case['title']))
在testcase文件夹下新建test_invest_flow.py文件
from testcase.base_case import BaseCase
from common.myddt import ddt,data
from common.test_data_handler import get_test_data_from_excel
cases=get_test_data_from_excel(BaseCase.settings.TEST_DATA_FILE,'invest_flow')
@ddt
class TestInvestFlow(BaseCase):
name = '投资业务流'
@data(*cases)
def test_invest_flow(self,case):
self.checkout(case)
会话对象可以跨请求保持某些参数。也会在同一个session实例发出的所有请求之间保持cookie。
import requests
s = requests.session()
login_url = 'https://v4.ketangpai.com/UserApi/login'
data = {"email":"账号","password":"密码","remember":0}
# 登录
res = s.post(url=login_url, data=data)
print(res.text)
courses_url = 'https://v4.ketangpai.com/CourseApi/lists'
# 获取课程列表
res = s.get(courses_url)
print(res.json())
1. 在testcase文件夹下,修改base_case.py文件
#导入requests库
import requests
2.实例化一个 requests.Session 类的对象,然后赋值给 BaseCase 的 session 属性
class BaseCase(unittest.TestCase):
...
#会话对象
session=requests.session()
...
3.创建方法 send_http_request
def send_http_requests(self,url, method, **kwargs) -> requests.Response:
"""
发送http请求
:param url:请求路径
:param method:请求方式
:param kwargs:params、data、json、headers.....
:return:response
"""
# 把方法名小写化,防止误传
method = method.lower()
# 获取对应的方法
return getattr(self.session, method)(url, **kwargs)
4.修改 step 方法里的调用 send_http_request 为 self.send_http_request
测试数据中request为空时,表格中内容写{}
项目中可能会出现多个域名,故修改base_case.py中 pre_test_data 中拼接url的部分:
#只有非全路径的时候才拼接
if not self.case['url'].startswith('http'):
# 拼接url
self.case['url'] = settings.PROJECT_HOST + settings.INTERFACES[self.case['url']]
在项目中存在使用timestamp+token+sign进行接口鉴权
http数据在传递过程中使用的是明文,安全性较低,https在http的基础上增加了加密通道安全性,但客户端在发送请求时数据依然是明文。为了安全性,对敏感的数据会在发送前先加密再传递。
解决方式:
找开发协调提供加密的模块,如jar包(java语言),然后在python中调用
找开发了解加密过程,自己用python代码封装
安装:
pip install rsa
使用:
import rsa
#1.生成密钥对
#返回数据为元祖
(pub,pri)=rsa.newkeys(1024)
#打印公钥
print(pub)
#打印私钥
print(pri)
#2.加密
#2.1需要把待加密的数据转换为字节
#消息加密前需要转换成字节数据
#转换为字节数据
message='测试'.encode('utf-8')
#加密,返回二进制格式
res=rsa.encrypt(message,pub)
print(res)
#加密后的数据一般会转换成字符串传输
#二进制数据转换为字符串,使用base64编码 a-z A-Z 0-9 + =
import base64
#2.2一般需要把加密后的字节转换为base64编码
b64_res=base64.b64encode(res) #字节,但是都是ascii码
print(b64_res)
#要转换为字符串
#2.3将base64编码字节转换为字符串
b64_str_res=b64_res.decode()
print(b64_str_res)
#3.解密
#一般拿到的加密数据都是base64编码的字符串
#3.1将base64编码字节转换为字符串
#转换为字节数据
b64_bytes=b64_str_res.encode()
#3.2将base64字节数据解码为密文
#把b64解码
r=base64.b64decode(b64_bytes)
#3.3解密
d_res=rsa.decrypt(r,pri)
print(d_res) #字节数据
#3.4如果你加密的是字符串,需要对应的字符编码进行解码为字符串
print(d_res.decode('utf-8')) #解码
pub_key='''
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQENQujkLfZfc5Tu9Z1LprzedE
O3F7gs+7bzrgPsMl29LX8UoPYvIG8C604CprBQ4FkfnJpnhWu2lvUB0WZyLq6sBr
tuPorOc42+gLnFfyhJAwdZB6SqWfDg7bW+jNe5Ki1DtU7z8uF6Gx+blEMGo8Dg+S
kKlZFc8Br7SHtbL2tQIDAQAB
-----END PUBLIC KEY-----
'''
#一般提供的公钥格式都是上面这种pem格式
#1.先转换成字节数据
pub_key=pub_key.encode() #因为都是ascii码所以不用指定特定的字符编码
#2.调用方法加载
pub=rsa.PublicKey.load_pkcs1_openssl_pem(pub_key)
print(type(pub),pub)
在根目录下settings.py中添加公钥配置
SERVER_RSA_PUB_KEY = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQENQujkLfZfc5Tu9Z1LprzedE
O3F7gs+7bzrgPsMl29LX8UoPYvIG8C604CprBQ4FkfnJpnhWu2lvUB0WZyLq6sBr
tuPorOc42+gLnFfyhJAwdZB6SqWfDg7bW+jNe5Ki1DtU7z8uF6Gx+blEMGo8Dg+S
kKlZFc8Br7SHtbL2tQIDAQAB
-----END PUBLIC KEY-----
"""
在common文件夹下新建 encrypt_handler.py 模块,定义如下函数:
import base64
import time
import rsa
def rsa_encrypt(msg:str,pub_key:str):
'''
公钥加密
:param msg: 要加密的内容 str
:param pub_key: pem格式的公钥字符串
:return:
'''
#1.生成公钥对象
#把公钥字符串转换为字节数据
pub_key_bytes=pub_key.encode()
pub=rsa.PublicKey.load_pkcs1_openssl_pem(pub_key_bytes)
#待加密的数据转换为字节数据
content=msg.encode('utf-8')
#2.加密
crypt_msg=rsa.encrypt(content,pub)
#3.base64编码并转换成字符串格式
return base64.b64encode(crypt_msg).decode()
def generate_sign(token,pub_key):
'''
生成签名
:param token: token字符串
:param pub_key: pem格式的公钥
:return:
'''
#1.获取token的前50位
token_50=token[:50]
#获取timestamp
timestamp=int(time.time())
#拼接token的前50位和生成的时间戳,转换成整数
msg=token_50+str(timestamp)
#进行rsa加密
sign=rsa_encrypt(msg,pub_key)
return sign,timestamp
if __name__ == '__main__':
import requests
import settings
from common.fixture import register, login
from common.test_data_handler import generate_no_use_phone
mobile_phone = generate_no_use_phone()
pwd = '12345678'
if not register(mobile_phone, pwd):
raise ValueError('注册错误')
res = login(mobile_phone, pwd)
if not res:
raise ValueError('登录失败')
token = res['token_info']['token']
sign, timestamp = generate_sign(token, settings.SERVER_RSA_PUB_KEY)
headers = {
"X-Lemonban-Media-Type": "lemonban.v3",
"Authorization": "Bearer " + token
}
data = {
'member_id': res['id'],
'amount': 5000,
'timestamp': timestamp,
'sign': sign
}
url = settings.PROJECT_HOST + settings.INTERFACES['recharge']
res = requests.post(url=url, json=data, headers=headers)
print(res.status_code)
print(res.text)
修改用例数据:
根据业务流方案2,接口前置条件可以看成是一条用例,实现完全的数据驱动。
V3版本、多条sql验证投资接口:
复写BaseCase中step方法,aseert_db_true方法
在testcase文件夹下新建test_invest.py文件
import json
from testcase.base_case import BaseCase
from common.test_data_handler import get_test_data_from_excel
from common.myddt import ddt,data
from common.encrypt_handler import generate_sign
cases=get_test_data_from_excel(BaseCase.settings.TEST_DATA_FILE,'invest')
@ddt
class TestInvest(BaseCase):
name = '投资'
@data(*cases)
def test_invest(self,case):
self.checkout(case)
def step(self):
'''
用v3版本发送请求
:return:
'''
if self.case['request']['headers']['X-Lemonban-Media-Type'] =='lemonban.v3':
#因为发送请求在预处理的后面,所以对应的token一定会被替换到headers中
token = self.case['request']['headers']['Authorization'].split(' ')[-1]
sign, timestamp = generate_sign(token,self.settings.SERVER_RSA_PUB_KEY)
#要添加到json格式的请求体中(根据实际情况的格式来)
self.case['request']['json']['sign'] = sign
self.case['request']['json']['timestamp'] = timestamp
super().step()
# try:
# self.response = self.send_http_request(url=self.case['url'],method=self.case['method'], **self.case['request'])
# except Exception as e:
# self.logger.exception('用例【{}】发送http请求错误'.format(self.case['title']))
# self.logger.debug('url:{}'.format(self.case['url']))
# self.logger.debug('method:{}'.format(self.case['method']))
# self.logger.debug('args:{}'.format(self.case['request']))
# raise e
def assert_db_true(self):
'''
多条sql校验
:return:
'''
if self.case.get('sql'):
sqls = json.loads(self.case['sql'])
for sql in sqls:
# 查询数据
try:
db_res = self.db.exist(sql)
self.assertTrue(db_res)
except Exception as e:
self.logger.exception('用例【{}】数据库断言失败'.format(self.case['title']))
self.logger.debug('执行的sql是:{}'.format(sql))
raise e
self.logger.info('用例【{}】数据库断言成功'.format(self.case['title']))
mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。
典型的应用场景:
当某个单元测试依赖另外一个函数,而这个函数还未开发完成,那么可以使用这个函数的mock对
象来完成测试。
当某个接口测试依赖另一个接口,而这个接口未开发完成,或不方便调用(例如第三方的支付接
口),那么可以使用mock服务模拟这个依赖接口来完成。
from unittest.mock import create_autospec
#需要被mock的函数
def some_function(a,b,c):
pass
#通过create_autospec函数对其进行mock,并定义返回值
mock_func=create_autospec(some_function,return_value='成功')
#然后进行调用,注意调用时传入的参数需符合原来的参数
res=mock_func(a=1,b=2,c=3)
print(res)
目前在实际测充值业务中,用户会通过网银,微信,支付宝等三方支付平台进行支付,业务平台会调用这些第三方平台的支付接口,当用户支付成功之后会返回对应的结果,然后再处理平台业务。在前期未上线测试前一般不会直接调用支付接口,而是通过mock测试。
1. 首先在 test_cases/test_recharge.py 模块中导入 create_autospec
from unittest.mock import create_autospec
2.然后复写 step 方法
def step(self):
# 假设第三方支付接口地址是
alipay_url = 'https://www.fastmock.site/mock/8845a245139a327f793421b16e6f63d8/futureloan/alipay'
# mock一下发送http请求,mock一下send_http_request方法
# 自定义返回数据,这个要和项目实际情况结合,实际返回是什么就定义为什么样的
alipay_mock = create_autospec(self.send_http_request, return_value={"code": 0, "msg": "支付成功"})
# 执行mock方法
pay_res = alipay_mock(alipay_url, method=self.case['method'],
**self.case['request'])
if not pay_res.json()['code'] == 0:
raise RuntimeError('支付宝支付不成功!')
super().step()
在进行接口测试的时候,还有一种mock测试就是使用mock服务。
本质上mock服务就是一个web应用程序,可以自定义接口地址,发送参数,以及响应结果,实现技术简单。
目前市面上有很多对应的产品,大多数都可以免费使用。百度mock服务,很多产品,使用大同小异,这里就不一一讲解。