目录

一、需注意事项

二、单元测试框架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中调试时,存在项目路径容易报错问题,故单独打开项目文件夹:

导入模块时,顺序是系统模块 > 第三方模块 > 自定义模块:

在函数中,设置三引号回车方式函数注释自动生成:

  1. 修改File > Settings > Tools > Python Integrated Tools > Docstring format > reStructuredText

  2. 在函数方法名下输入三个双(单)引号,回车之后自动生成

项目运行方式选择:

编码格式报错:

  1. 在.py文件中最上方定义编码格式:#coding = utf-8

  2. 修改pycharm工作编码规则,File>Settings>Editor>File Encodings,修改编码为utf-8

项目相关内容:

项目参考柠檬班教材,标题十五处开始可搭配pdf24查看。

接口无法使用,仅参考学习接口自动化思路

测试接口过多时处理方法:

当测试接口过多时,可在testcase文件夹下新建test_**__module.py文件,分模块编写测试用例,模块中每个用例为单独的一个类。

二、单元测试框架unittest

unittest是Python自带的一个单元测试框架

2.1 作用

  • 管理用例

  • 批量执行用例

  • 组织运行结果/报告

  • 让代码更稳健

  • 可拓展

2.2 测试用例(TestCase)

unittest提供了一个基类TestCase用来创建测试用例;

创建测试用例,导入unittest模块后,需要创建一个类,来继承TestCase;

2.2.1 单元测试函数

类中每个方法代表一个测试用例,方法名必须以test开头,unittest只会识别以test开头的方法为测试用例

在test_cases文件夹下新建test_abs.py文件,文件内容如下:

import unittestclass TestAbs(unittest.TestCase):name='测试abs函数'def test_negative_num(self):'''测试负数:return:'''# 1.测试数据data=-1except_data=1# 2.测试步骤res=abs(data)# 3.断言print(1)self.assertEqual(res,except_data,msg='断言运行结果')def test_zero(self):'''测试0:return:'''# 1.测试数据data=0except_data=0# 2.测试步骤res=abs(data)# 3.断言print(2)self.assertEqual(res,except_data,msg='断言运行结果')def test_postive_num(self):'''测试正数:return:'''# 1.测试数据data=1except_data=1# 2.测试步骤res=abs(data)# 3.断言print(3)self.assertEqual(res,except_data,msg='断言运行结果')

2.2.2 测试函数的执行顺序

测试用例执行时不是按照定义的顺序执行,而是按照ascii码排序,可以在test_后加上数字,定义执行顺序

test_1.... 、 test_2.....

2.3 用例收集器(TestLoader)

定义main.py文件内容,运行main.py文件,可执行测试用例

'''
项目入口
1.收集所有的用例
2.执行所有的用例
'''import unittestif __name__ == '__main__':#test_cases为文件路径re=unittest.TestLoader().discover('test_cases')print(re)

收集规则可以通过patterns自定义,默认是以test开头的模块,一般不做修改。

会到指定目录下递归去寻找符合patterns规则的模块中继承了TestCase的类。

注意:如果有嵌套的文件夹,一定要在文件夹中加入__init__.py,其实就是把文件夹变成一个Python的包。

2.4 测试套件(TestSuite)

用例收集器,会返回能够收集到的所有的用例,封装成测试套件类型。

测试套件,其实就是一系列的测试用例类。

添加单个测试用例:

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))

2.5 测试运行器(TextTestRunner)

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)

2.6 测试脚手架(FIXture)

可以在测试用例执行之前自动调用指定的函数,在测试用例执行之后自动调用指定的函数。

对象、方法:

-setUp会在每个单元函数测试开始前执行。

import unittestclass 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 unittestclass 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 unittestclass MyTest(unittest.TestCase):@classmethoddef setUpClass(cls) -> None:print('setupclass自动调用了')def test_01(self):print('测试用例01')def test_02(self):print('测试用例02')if __name__ == '__main__':MyTest()

-tearDownclass在整个测试用例类执行结束之后执行。

import unittestclass MyTest(unittest.TestCase):@classmethoddef tearDownClass(cls):print('teardownclass自动调用了')def test_01(self):print('测试用例01')def test_02(self):print('测试用例02')if __name__ == '__main__':MyTest()

模块方法:

-setUpModule在.py文件开始执行前时调用

import unittestdef 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 unittestdef 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()

2.7 断言

断言方法 断言描述
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文件,作为入口函数,方便项目调试。

3.1 编写测试用例

在test_cases文件夹下新建test_abs.py文件,内容如下

import unittest#定义一个类继承unittest.TestCase
class TestAbs(unittest.TestCase):name='测试abs函数'def test_01negative_num(self):'''测试负数:return:'''# 1.测试数据data=-1except_data=1# 2.测试步骤res=abs(data)# 3.断言print(1)self.assertEqual(res,except_data,msg='断言运行结果')def test_02zero(self):'''测试0:return:'''# 1.测试数据data=0except_data=0# 2.测试步骤res=abs(data)# 3.断言print(2)self.assertEqual(res,except_data,msg='断言运行结果')def test_03postive_num(self):'''测试正数:return:'''# 1.测试数据data=1except_data=1# 2.测试步骤res=abs(data)# 3.断言print(3)self.assertEqual(res,except_data,msg='断言运行结果')

3.2 定义main.py

'''
项目入口
1.收集所有的用例
2.执行所有的用例
'''#创建main.py入口函数
import unittestif __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)

3.3 生成html报告

3.3.1 HTMLTestRunner报告

将HTMLTestRunner.py文件放在common文件夹下,修改main.py文件,并运行main.py文件

#main.py文件
import unittestfrom common.HTMLTestRunner import HTMLTestRunnerif __name__ == '__main__':re=unittest.TestLoader().discover('test_cases')with open('reports/测试报告.html','wb') as f:runner=HTMLTestRunner(f)runner.run(re)

可修改HTMLTestRunner.py中默认信息

3.3.2 BeautifulReport报告

安装:

pip install beautifulreport

使用:

#main.py文件
import unittestfrom BeautifulReport import BeautifulReportif __name__ == '__main__':re=unittest.TestLoader().discover('test_cases')br=BeautifulReport(re)br.report(description='项目描述',filename='reports/测试报告.html')

四、ddt的使用(项目参数化)

实现数据和测试脚本的分离,将测试数据加载到脚本中,一组数据对应生成一个测试用例。

只有测试流程完全一致时,才可以使用ddt。

4.1 安装

pip install ddt

4.2 使用

修改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()

4.3 ddt生成报告定义标题信息

在使用ddt模块时,通过main.py入口文件调用并生成beautifulreport时,beautifulreport中标题为null,可将ddt.py文件复制并重命名一份放在common文件夹下,修改图片标记处代码,导入修改之后的模块,用例数据传入的title字段为标题内容。

4.3.1 复制ddt.py到common文件夹下,并修改标记处代码

4.3.2 修改测试用例调用的模块信息

修改前信息:

import unittest
#导入ddt
from ddt import ddt,data

修改后信息:

import unittest
#导入ddt
from common.myddt import ddt,data

五、http请求的封装及使用

5.1 http请求的封装

因为发送请求在多个模块中使用,为了对代码进行复用,故对该功能进行封装。

在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)

5.2 注册接口测试用例编写

在test_cases文件夹下新建test_register.py文件,文件内容如下

#注册接口测试用例
import unittestfrom common.make_request import send_http_requests
from common.myddt import ddt,datacases=[{'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):#类方法@classmethoddef setUpClass(cls) -> None:print('注册接口开始测试')@classmethoddef 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中的用例数据

excel是二进制文件,有特殊的编码

需要使用第三方库,openpyxl 支持的格式,Excel 2010 xlsx/xlsm/xltx/xltm

6.1 安装

pip install openpyxl

6.2 测试数据编写

在test_data文件夹下新建test_data.xlsx文件

字段信息:

id :序号

title:测试用例标题

method:请求方式post/get/...

url:请求的url

request:请求数据,包括请求头、请求体

expect_data:期望数据

测试数据中引号使用" "英文双引号,方便读取后转换为json格式

6.3 excel文件的基本操作

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)

6.4 读取excel文件功能封装

在common文件夹下新建test_data_handler.py文件

import jsonfrom openpyxl import load_workbookdef get_test_data_from_excel(file,sheet_name):'''获取excel表格中用例数据:param file: 文件名:param sheet_name: 表名:return: [{dict},{dict}]'''# 1.打开工作簿wb=load_workbook(filename=file)# 2.获取sheetsh=wb[sheet_name]#获取最大行数row=sh.max_row#获取最大列数column=sh.max_column# 3.获取数据data=[]# 获取第一行,拿到所有的keykeys=[]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]=valuetemp[keys[j-1]]=sh.cell(i,j).value# 把request,expect_data  json数据转换成python对象# 编写时容易存在json格式错误,故加入trytry: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 dataif __name__ == '__main__':re=get_test_data_from_excel('../test_data/test_data.xlsx','register')print(re)

6.5 将封装的功能应用到项目中

修改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自带)

7.1 日志的基本操作

7.1.1 日志的级别

logging.debug() #调试:诊断问题的时候用,最详细的日志

logging.info() #普通信息:确定程序按照预定的流程运行

logging.warning() #警告信息:可能会出问题,程序还可以继续运行

logging.error() #错误信息:某些功能可能不能正确的执行

logging.critical() #危险信息:一个严重的错误

7.1.2 日志的基本操作

#导入模块
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('这是一个危险信息')

7.1.3 日志导出到文件

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('这是一个调试信息')

7.1.4 日志的高阶用法

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('这是一个危险信息')

7.2日志模块的封装

封装日志模块,使日志内容既能够输出在控制台,又能够写入文件,并指定日志的路径、等级。

在common文件夹下新建log_handler.py文件

#coding=utf-8
import loggingdef 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.DEBUGconsole_level=logging.DEBUGelse:file_level=logging.WARNINGconsole_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 loggerif __name__ == '__main__':logger=get_logger(name='py',filename='../logs/py.log',debug=True)logger.info('我是普通信息')

7.3 封装的日志模块应用到项目中

修改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)#类方法@classmethoddef setUpClass(cls) -> None:cls.logger.info('注册接口开始测试')@classmethoddef 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)# 将报错内容输出到控制台,并打印错误日志,日志等级为errorself.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()

7.4 单例模式

单例模式:一个生命周期中,只有一个实例

python中的模块本身也是对象,是天然的单例模式

7.4.1 问题描述

直接在测试用例中调用封装的日志方法,名字、文件名相同时,会导致重复添加多个日志处理器,输出的日志文件重复。

7.4.2 处理生成重复日志问题

1.导入模块时,会执行模块所在文件__init__.py文件(初始化),故将日志创建放到__init__.py文件中,实现单例模式。

在common文件夹下新建__init__.py文件

from .log_handler import get_logger
logger=get_logger('ces','logs/ces.log',debug=True)

2.修改测试用例中导入模块、类属性定义方法

八、项目配置

目的:

  1. 处理不同的测试环境(开发环境、测试环境)

  2. 不同的项目,不改写代码

  3. 封装彻底,解耦合

8.1 配置文件

8.1.1 ini、conf、cnf、cfg

格式:

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'))

8.1.2 yaml、yml

yaml格式验证网站:YAML、YML在线编辑器(格式化校验)-BeJSON.com

  1. 区分大小写

  2. 使用缩进表示层级,不能使用tab键缩进,只能用空格(和python一样)

  3. 缩进没有数量的,只要前面是对齐的就行

  4. 注释是#

Map对象:

section:

键:(空格)值

键1:(空格)值1

log:name: pylogfilename: logs/pylog.logdebug: 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)

8.2 配置文件模块封装

简单功能直接封装成函数,复杂功能封装成类。

配置文件封装:能够处理多种类型配置文件,返回值数据结构一致

8.2.1 函数封装

在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 dataif __name__ == '__main__':res=get_config('../conf.yaml')print(res)

8.2.2 类封装

import yaml
from configparser import ConfigParserclass Config:def __init__(self,filename,encoding='utf-8'):#初始化工作self.filename=filenameself.encoding=encodingself.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 datadef __parse_yaml(self):'''解析yaml,yml文件:return:'''with open(self.filename,'r',encoding=self.encoding) as f:data=yaml.load(f,Loader=yaml.FullLoader)return datadef 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)

8.3 封装的配置文件应用到项目中

一个框架封装的彻不彻底的标准是能否复用,即应用到其他项目时,不需要修改框架的源码。

8.3.1 定义yaml配置文件

在项目文件夹下新建conf.yaml配置文件,文件内容

log:name: pylogfilename: logs/pylog.logdebug: true
testdata:file: test_data/test_data.xlsx
report:filename: reports/测试报告.htmldescription: 一个练手项目

将这些内容写到配置文件中,然后在框架代码中动态的获取配置文件的相对应设置,实现代码的解耦。

8.3.2 修改日志器调用时传参

修改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'])

8.3.3修改测试用例数据文件的路径

修改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')

8.3.4 修改生成报告时传入参数

修改根目录下main.py文件,并运行main.py

import unittestfrom BeautifulReport import BeautifulReportfrom common import configif __name__ == '__main__':re=unittest.TestLoader().discover('test_cases')br=BeautifulReport(re)# br.report(description='项目描述',filename='reports/测试报告2.html')br.report(**config['report'])

九、项目配置最终方案

可以单独使用一个python文件,在其中定义配置信息,框架中的其他核心模块,直接导入该配置模块中的配置变量进行使用,效率更高,更直观。

9.1 创建settings.py的配置文件

在根目录下新建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':'一个练手项目'
}

9.2 修改日志调用时的传参

修改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)

9.3 修改用例数据文件路径

修改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')

9.4 修改生成报告时传参

修改根目录下main.py文件

import unittestfrom BeautifulReport import BeautifulReport# from common import config
import settingsif __name__ == '__main__':re=unittest.TestLoader().discover('test_cases')br=BeautifulReport(re)# br.report(**config['report'])br.report(**settings.REPORT_CONFIG)

十、项目路径处理

在现有代码中,路径都是使用项目根目录的相对路径,在调试子模块时会出现路径错误的问题,为解决该问题,对项目路径进行处理。

10.1 路径基础知识

10.1.1 全局变量__file__

__file__返回当前模块的绝对路径。

#demo.py
print(__file__)

运行后输出当前模块的绝对路径,在其他模块中调用该模块时,值依然为这个模块的绝对路径。

可通过该属性动态获取项目的根目录,拼接项目下其他文件路径获取其他需要配置路径的绝对路径。

10.1.2 os模块

路径处理涉及到不同系统的格式问题,可以使用内置模块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'))

10.2 项目路径设置

在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':'一个练手项目'
}

十一、MySQL数据库验证

在接口测试中,除验证响应数据外,如过接口对数据库进行操作,且数据敏感则需要对数据库进行验证。

11.1 MySQL数据库的基本操作

11.1.1 安装

pip install pymysql

11.1.2 python操作mysql数据库基本流程

'''
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()

11.1.3 python查询mysql数据库

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)

11.1.4 Python修改mysql数据库

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)

11.1.5 操作mysql数据库时需注意事项

  1. sql语句不需要加分号

  2. execute执行后返回的是查询后更新的数据条数,结果需要通过cursor获取

  3. 需要释放资源,关闭游标与连接

  4. 事务的回滚与提交

11.2 封装数据库操作并应用到项目中

11.2.1 数据库操作的封装

import pymysqlclass 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 Trueelse:return Falsedef __del__(self):'''忘记关闭连接的时候,程序自动调用关闭连接:return:'''if __name__ == '__main__':import settingsdb=DB(settings.DB_CONFIG)sql='select id,reg_name from member limit 10'res=db.get_one(sql)print(res)

11.2.2 配置文件中增加数据库信息

在settings.py文件中新增如下内容

#数据库配置
DB_CONFIG={'host':'api.lemonban.com','user':'future','password':'123456','db':'futureloan','charset':'utf8','port':3306
}

11.2.3 修改数据库操作为单例模式

一个DB对象可实现对数据库的所有操作,通过一个DB对象创建一个连接,完成所有操作,达到节省资源的效果。

common文件夹下__init__.py文件新增如下内容

from .db_handler import DBdb=DB(settings.DB_CONFIG)

11.2.4 修改测试数据表格中内容

当要验证数据库时,在test_data文件夹下test_data.xlsx中第一行新增sql字段,注册成功的用了后写入对应的sql语句

11.2.5 调整注册接口断言信息

#注册接口测试用例
import unittestimport 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#类方法@classmethoddef setUpClass(cls) -> None:cls.logger.info('注册接口开始测试')@classmethoddef 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)# 将报错内容输出到控制台,并打印错误日志,日志等级为errorself.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 eelse:self.logger.info('数据库断言成功')self.logger.info('用例【{}】测试结束《《《《'.format(case['title']))if __name__ == '__main__':unittest.main()

十二、定义生成的报告

12.1 时间处理模块datetime的基础知识

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秒'))

12.2 封装测试报告并应用到项目中

12.2.1 测试报告的封装

在common文件夹下新建report_handler.py文件,内容如下:

import os
from datetime import datetimefrom BeautifulReport import BeautifulReportfrom .HTMLTestRunner import HTMLTestRunnerdef 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)

12.2.2 修改配置文件中报告相关信息

修改根目录下settings.py文件中测试报告信息

#测试报告
REPORT_CONFIG={'filename':'测试报告.html','description':'一个练手项目','report_dir':os.path.join(BASE_DIR,'reports')
}

12.2.3 修改出入口函数

修改根目录下main.py文件为如下内容:

import unittest# from BeautifulReport import BeautifulReportfrom common.repor_handler import report
import settingsif __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)

十三、动态生成手机号码

13.1 随机库random的基础知识

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))

13.2 封装随机生成手机号码并应用到项目中

13.2.1 生成随机号码的封装

common文件夹下test_data_handler.py文件中新增如下内容

import random
from common import dbdef 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 phoneif __name__ == '__main__':res=generate_no_use_phone()print(res)

13.2.2 修改测试数据中信息

在测试用例数据中生成手机号码的槽位,在代码中检测测试数据,如果出现操作则动态生成并替换。

13.2.3 修改测试数据处理模块中代码

因为使用槽位后,表格中数据不能正确转换为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.获取sheetsh=wb[sheet_name]row = sh.max_rowcolumn = sh.max_column#3.获取数据data=[]#获取第一行拿到所有的keykeys=[]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]=valuetemp[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)

13.2.4 修改注册用例代码

增加数据处理步骤,将槽位替换为随机生成的手机号码,修改test_cases 文件夹下test_register.py文件

import json
import unittestimport 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=loggerdb=db@classmethoddef setUpClass(cls) -> None:#类方法调用cls.logger.info('注册接口开始测试')@classmethoddef 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)#替换一下sqlif 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)#将报错内容输出到控制台,并打印错误日志,日志等级为errorself.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 eelse:self.logger.info('数据库断言成功')self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))if __name__ == '__main__':unittest.main()

十四、项目路径配置

不同测试环境中项目的url路径不同,为提高项目的复用性,对项目代码进行优化。

14.1在配置文件中配置项目域名、接口信息

在settings.py文件中新增如下内容

#项目域名
PROJECT_HOST='http://test.lemonban.com/futureloan/mvc/api'#接口地址,将/符号放在接口地址中
INTERFACES={'register':'/member/register','login':'/member/login','recharge':'/member/recharge'
}

14.2 修改用例数据中url信息

修改test_data文件夹下test_data.xlsx表格中url

14.3 修改测试用例代码

在代码中对配置的url进行处理(测试数据处理处新增代码)

修改 test_cases下test_register.py文件,使用设置中的 PROJECT_HOST 和 INTERFACES 动态的拼接url。

# 动态拼接url
case['url'] = settings.PROJECT_HOST + settings.INTERFACES[case['url']]
import json
import unittestimport 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=loggerdb=db@classmethoddef setUpClass(cls) -> None:#类方法调用cls.logger.info('注册接口开始测试')@classmethoddef 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)#替换一下sqlif 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'])#拼接urlcase['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)#将报错内容输出到控制台,并打印错误日志,日志等级为errorself.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 eelse:self.logger.info('数据库断言成功')self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))if __name__ == '__main__':unittest.main()

十五、类前置条件与充值接口

接口测试中存在接口依赖,发送请求接口时需要携带上一个接口返回的数据。

可以将多个需要按顺序执行的用例放在一个测试用例类中,将前一个接口返回的数据保存在类属性中,后一个接口通过共同的类获取对应的属性,从而解决接口依赖。

充值接口分析:

充值接口相较于注册接口:

增加前置条件:注册用户、登录用户

权限验证

15.1 封装注册、登录用户函数

项目中除注册、登录接口外,其他接口都需要注册用户、登录用户的前置条件,故对注册和登录功能进行封装。

#在common文件夹下新建fixture.py文件
import requestsimport 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']=_typeheaders={"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 Truereturn Falseexcept Exception as e:logger.exception('注册用户失败')raise edef 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 eif __name__ == '__main__':from common.test_data_handler import generate_no_use_phonephone=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('注册失败')

15.2 将注册、登录模块应用到项目中

15.2.1 新增recharge(充值)的测试数据

15.2.2 新增充值测试用例

在testcase文件夹下新建test_recharge.py文件

import json
import unittestimport 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,datacases=get_test_data_from_excel(settings.TEST_DATA_FILE,'recharge')@ddt
class TestRcharge(unittest.TestCase):logger=loggerdb=db@classmethoddef 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_phonecls.member_id = data['id']cls.token = data['token_info']['token']@classmethoddef tearDownClass(cls) -> None:# 类方法调用cls.logger.info('充值接口测试结束')@data(*cases)def test_recharge(self,case):self.logger.info('用例【{}】开始测试>>>>>'.format(case['title']))#1.用例数据处理# 替换member_idif "#member_id#" in case['request']:case['request'] = case['request'].replace('#member_id#',str(self.member_id))# 替换tokenif '#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'])#拼接urlcase['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)# 将报错内容输出到控制台,并打印错误日志,日志等级为errorself.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 eelse:self.logger.info('数据库断言成功')self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))if __name__ == '__main__':unittest.main()

十六、动态参数替换

通过类属性实现了接口依赖数据的传递,但在传递过程中将属性名写死,无法动态替换测试数据中的参数,无法动态根据类属性进行接口依赖数据的传递。

设计思路:

用例数据中的槽位名要和类属性名一致

找出用例数据中所有槽位

根据槽位依次获取类中对应属性,存在则替换用例数据中相应槽位

16.1 正则表达式的基本使用

通过正则表达式可以判断给定的字符串是否符合正则表达式的过滤逻辑(邮箱、电话号码),或通过正则表达式从字符串中获取我们想要的特定部分。

python中正则表达式需要使用re库

import res1 = "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)

16.2 动态参数模块的使用

16.2.1 封装动态获取槽位并替换槽位内容

#在common文件夹下test_data_handler.py文件中新增如下内容
import redef 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中对应的属性,若无对应的属性,则返回Nonevalue=getattr(obj,arg,None)if value:json_s=json_s.replace('#{}#'.format(arg),str(value))return json_sif __name__ == '__main__':class E:name='zhangsan'age=17b='name is #name#,age is #age#'res=replace_args_by_re(b,E)print(res)

16.2.2 修改充值接口用例类

(因传入参数与视频讲解要求不一致,仅参考写法)

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方法中。

17.1 封装创建项目函数

方法级前置条件:创建项目

在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

17.2审核项目接口用例数据

17.3配置文件中添加接口信息

# 接口地址
INTERFACES = {...'add': '/loan/add','audit': '/loan/audit'
}

17.4 创建审核接口

在testcase文件夹下新增test_audit.py文件

import json
import unittestimport 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,datacases=get_test_data_from_excel(settings.TEST_DATA_FILE,'audit')@ddt
class TestRcharge(unittest.TestCase):logger=loggerdb=db@classmethoddef 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和tokencls.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. 保存数据# 管理员用户的tokencls.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('添加项目失败')@classmethoddef 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)#替换phonecase['request']=case['request'].replace('#phone#',str(self.phone))#替换sqlif 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'])#拼接urlcase['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)# 将报错内容输出到控制台,并打印错误日志,日志等级为errorself.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 eelse:self.logger.info('数据库断言成功')self.logger.info('用例[{}]测试结束<<<<<<<<'.format(case['title']))

十八、测试用例基类抽取

观察发现审核接口用例测试类中测试方法的代码与充值接口用例测试类中测试方法的代码完全一致,说明测试逻辑相同,重复代码可以通过抽象成基类进行共享,实现代码复用。

抽取测试用例基类思路:

1.将公用模块都封装到基类中便于子类直接调用

日志器、数据库处理器、项目配置

2.将测试方法中的每个步骤单独封装成一个对象方法

测试数据处理、测试步骤、响应状态码断言、响应数据断言、数据库断言

3.为了对响应状态码进行解耦,在测试用例数据中增加一列status_code来校验响应状态码

18.1 项目状态码验证

不同项目请求成功返回响应的状态码可能不一致,为了实现解耦

故在测试数据中增加一行status_code

修改test_cases文件夹下测试的所有.py文件

修改前:

# 3.1断言状态码
try:self.assertEqual(200, re.status_code)
except AssertionError as e:# 将报错内容输出到控制台,并打印错误日志# self.logger.warn('状态码断言失败',exc_info=e)# 将报错内容输出到控制台,并打印错误日志,日志等级为errorself.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)# 将报错内容输出到控制台,并打印错误日志,日志等级为errorself.logger.exception('状态码断言失败')raise e

18.2 测试基类的定义

将公用模块都封装到基类中便于子类直接调用:日志器、数据库处理器、项目配置

将测试方法中的每个步骤单独封装成一个对象方法:测试数据处理、测试步骤、响应状态码断言、响应数据断言、数据库断言

#在testcase文件夹下新建base_case.py文件,内容如下
import json
import unittestimport 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=loggerdb=dbsettings=settings@classmethoddef setUpClass(cls) -> None:cls.logger.info('{}接口开始测试'.format(cls.name))@classmethoddef tearDownClass(cls) -> None:cls.logger.info('{}接口结束测试'.format(cls.name))def checkout(self,case):#绑定对象属性,便于下面的测试流程函数去处理self.case=caseself.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)# 替换一下sqlif 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)# 替换一下sqlif 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']))# 拼接urlself.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 edef 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)# 将报错内容输出到控制台,并打印错误日志,日志等级为errorself.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 eelse:self.logger.info('数据库断言成功')

18.3 修改其他用例类

18.3.1 修改注册接口用例类

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)

18.3.2 修改充值接口用例类

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,datacases=get_test_data_from_excel(BaseTest.settings.TEST_DATA_FILE,'recharge')@ddt
class TestRcharge(BaseCase):name='审计'@classmethoddef 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)

18.3.3修改审核接口用例类

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='审核'@classmethoddef 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和tokencls.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. 保存数据# 管理员用户的tokencls.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文件,分模块编写测试用例,模块中每个用例为单独的一个类。**

十九、业务流测试

在做接口自动化时,往往需要先测通核心业务流,再进行单接口测试。

接口测试业务流设计

  1. 站在用户角度

  2. 重视全局而非细节

  3. 先测主流程,后测分流程

  4. 只测正例

19.1 在测试用例类中根据业务流定义对应的单元测试方法

优点:逻辑清晰简答

缺点:当业务流很长时代码量庞大,业务发生改变需修改代码

示例:贷款流程

  1. 注册普通融资用户

  2. 登陆普通融资用户

  3. 普通用户创建项目

  4. 注册管理员用户

  5. 登录管理员用户

  6. 管理员审核项目

from testcase.base_case import BaseCaseclass 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)

19.2 将业务流做成数据驱动

观察发现,测试用例类中根据业务流定义对应的单元测试方法时基本流程不变,只有在接口依赖处提取了响应中的数据并保存到类属性中提供给后一个测试用例访问。

19.2.1 将业务流做成数据驱动:

优点:代码复用性高,基本流程不变;当业务流发生改变时,只需要修改用例数据不需要修改代码。

缺点:学习成本高,需要对代码逻辑了解才能编写用例

19.2.2 用例数据设计:

在用例数据中添加一列extract表示提取响应数据并保存在对应的类属性中,规则如下

[{"name":"member_id","exp":"$..id"},{"name":"token","exp":"$..token"}]

上面的提取规则中,name表示绑定到到类中的属性名,exp表示从响应数据中提取的数据表达式。

19.2.3 jsonpath的基础知识:

功能分析:动态提取数据,动态绑定数据,通过jsonpath获取返回中数据进行处理

jsonpath的安装:

#在dos命令窗口
pip install jsonpath
jsonpath 描述
$ 根节点
@ 当前元素
. 或[] 下一个节点
... 不考虑位置,符合条件的元素
* 匹配所有元素节点
[] 数组下标,根据内容选值

jsonpath输出的是一个列表[]

from jsonpath import jsonpatha={ "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,'$..*'))

19.2.4 读取响应数据后定义为类属性的封装

修改testcase文件夹下base_case.py文件( 新增extract_data函数)

import json
import unittestfrom jsonpath import jsonpathimport 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=loggerdb=dbsettings=settings@classmethoddef setUpClass(cls) -> None:cls.logger.info('{}接口开始测试'.format(cls.name))@classmethoddef tearDownClass(cls) -> None:cls.logger.info('{}接口结束测试'.format(cls.name))def checkout(self,case):#绑定对象属性,便于下面的测试流程函数去处理self.case=caseself.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)# 替换一下sqlif 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)# 替换一下sqlif 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']))# 拼接urlself.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 edef 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)# 将报错内容输出到控制台,并打印错误日志,日志等级为errorself.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 eelse: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']))

19.2.5 编写投资业务流代码

在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_excelcases=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鉴权的处理

20.1 requests的会话对象

会话对象可以跨请求保持某些参数。也会在同一个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())

20.2 封装session鉴权的http请求

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'):# 拼接urlself.case['url'] = settings.PROJECT_HOST + settings.INTERFACES[self.case['url']]

二一、V3版本鉴权与多条sql校验

在项目中存在使用timestamp+token+sign进行接口鉴权

http数据在传递过程中使用的是明文,安全性较低,https在http的基础上增加了加密通道安全性,但客户端在发送请求时数据依然是明文。为了安全性,对敏感的数据会在发送前先加密再传递。

解决方式:

  1. 找开发协调提供加密的模块,如jar包(java语言),然后在python中调用

  2. 找开发了解加密过程,自己用python代码封装

21.1 rsa加密

安装:

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)

22.2封装v3版本token签名函数及多条sql验证

在根目录下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 timeimport rsadef 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]#获取timestamptimestamp=int(time.time())#拼接token的前50位和生成的时间戳,转换成整数msg=token_50+str(timestamp)#进行rsa加密sign=rsa_encrypt(msg,pub_key)return sign,timestampif __name__ == '__main__':import requestsimport settingsfrom common.fixture import register, loginfrom common.test_data_handler import generate_no_use_phonemobile_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 jsonfrom 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_signcases=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'] = signself.case['request']['json']['timestamp'] = timestampsuper().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 edef 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 eself.logger.info('用例【{}】数据库断言成功'.format(self.case['title']))

二二、mock测试

mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。

典型的应用场景:

  1. 当某个单元测试依赖另外一个函数,而这个函数还未开发完成,那么可以使用这个函数的mock对

象来完成测试。

  1. 当某个接口测试依赖另一个接口,而这个接口未开发完成,或不方便调用(例如第三方的支付接

口),那么可以使用mock服务模拟这个依赖接口来完成。

22.1 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)

22.2将mock应用到项目中

目前在实际测充值业务中,用户会通过网银,微信,支付宝等三方支付平台进行支付,业务平台会调用这些第三方平台的支付接口,当用户支付成功之后会返回对应的结果,然后再处理平台业务。在前期未上线测试前一般不会直接调用支付接口,而是通过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()

22.3mock服务

在进行接口测试的时候,还有一种mock测试就是使用mock服务。

本质上mock服务就是一个web应用程序,可以自定义接口地址,发送参数,以及响应结果,实现技术简单。

目前市面上有很多对应的产品,大多数都可以免费使用。百度mock服务,很多产品,使用大同小异,这里就不一一讲解。

Python UnitTest接口自动化实战相关推荐

  1. python自动化读取和写入文件_基于Python的接口自动化实战-基础篇之读写配置文件...

    引言 在编写接口自动化测试脚本时,有时我们需要在代码中定义变量并给变量固定的赋值.为了统一管理和操作这些固定的变量,咱们一般会将这些固定的变量以一定规则配置到指定的配置文件中,后续需要用到这些变量和变 ...

  2. python 接口自动化的sql验证_基于Python的接口自动化实战-基础篇之pymysql模块操做数据库...

    引言 在进行功能或者接口测试时经常须要经过链接数据库,操做和查看相关的数据表数据,用于构建测试数据.核对功能.验证数据一致性,接口的数据库操做是否正确等.所以,在进行接口自动化测试时,咱们同样绕不开接 ...

  3. Jmeter系列之接口自动化实战

    VOL 139 24 2020-06 今天距2021年190天 这是ITester软件测试小栈第139次推文 点击上方蓝字"ITester软件测试小栈"关注我,每周一.三.五早上  ...

  4. python+pytest接口自动化之测试函数、测试类/测试方法的封装

    前言 今天呢,笔者想和大家聊聊python+pytest接口自动化中将代码进行封装,只有将测试代码进行封装,才能被测试框架识别执行. 例如单个接口的请求代码如下: import requestshea ...

  5. python接口自动化实战(框架)_python接口自动化框架实战

    python接口测试的原理,就不解释了,百度一大堆. 先看目录,可能这个框架比较简单,但是麻雀虽小五脏俱全. 各个文件夹下的文件如下: 一.理清思路 我这个自动化框架要实现什么 1.从excel里面提 ...

  6. python做接口自动化测试仪器经销商_Python接口自动化测试的实现

    接口测试的方式有很多,比如可以用工具(jmeter,postman)之类,也可以自己写代码进行接口测试,工具的使用相对来说都比较简单,重点是要搞清楚项目接口的协议是什么,然后有针对性的进行选择,甚至当 ...

  7. 跳槽涨薪技术之python+pytest接口自动化(6)-请求参数格式的确定

    [文章末尾给大家留下了大量的福利] 我们在做接口测试之前,先需要根据接口文档或抓包接口数据,搞清楚被测接口的详细内容,其中就包含请求参数的编码格式,从而使用对应的参数格式发送请求.例如某个接口规定的请 ...

  8. python实现接口自动化

    一.总述 Postman:功能强大,界面好看响应格式自主选择,缺点支持的协议单一且不能数据分离,比较麻烦的还有不是所有的公司都能上谷歌 SoupUI:支持多协议(http\soup\rest等),能实 ...

  9. python+pytest接口自动化框架(5)-requests发送post请求

    在HTTP协议中,与get请求把请求参数直接放在url中不同,post请求的请求数据需通过消息主体(request body)中传递. 且协议中并没有规定post请求的请求数据必须使用什么样的编码方式 ...

最新文章

  1. 移动互联网,安全厂商新战场
  2. (iOS-框架封装)AFN3.x 网络请求封装
  3. 蓝桥杯-5-1最小公倍数(java)
  4. 解决 No projects are available for deployment to this server!
  5. Opencv--CalcOpticalFlowPyrLK实现的光流法理解
  6. c++工作笔记001---c++相关零碎要点_endl、“\n”和‘\n’区别_extern int a关键字_-的意思_::的意思_指针和引用的区别
  7. Fixjs——事件回调的this
  8. 自学python能找到工作吗-25岁从零开始学习python还能找到工作吗?
  9. CheckBoxPreference组件
  10. 福昕pdf Acrobat DC pdf 右键菜单注册表
  11. usb万能驱动win7_给 win7 系统镜像添加驱动
  12. 电脑休眠和睡眠的区别
  13. KGB知识图谱通过数据可视化提升金融行业分析能力
  14. 贪吃蛇小游戏(HTML+CSS+JS)
  15. Creo9.0 绘制中心线
  16. linux系统scsi硬盘,Linux系统SCSI磁盘管理全攻略(一)
  17. 如何修改SnipeIT的部分设置
  18. 0基础实现微信推送天气,生日等(女朋友快乐眼)
  19. 修改chrome滚动条的样式
  20. 【数据结构】剑指 Offer P200——八皇后问题的排列解法

热门文章

  1. C盘被$ESTBAK$占用100G
  2. 要买还未买的书单——持续更新
  3. jzoj 5970.【北大2019冬令营模拟12.1】space 莫比乌斯反演
  4. 大数据行业的女程序媛:“愿未来能朝九晚五,也能浪迹天涯”
  5. 电子器件——钽电容的简介
  6. (转)智能制造大环境下PLC的发展趋势和路径
  7. Nova Suspend 和 Pause
  8. C++如何实现猜数游戏
  9. SAP中总账科目事务FS00/FSP0/FSS0的关系和区别
  10. Dell G3更换机械硬盘