【接口自动化测试实战】python接口自动化(自建库)
一、自动化项目介绍
1.涉及技术栈
python
requests
pytest
allure
2.实现的功能概述
支持requests各种请求
支持用例之间依赖关系
支持切换环境
支持按脚本的形式编写测试用例
支持生成html报告
二、框架及项目结构
项目目录
api_object:接口参数及扩展参数
config:配置文件,环境、账号、路径设置等
data:测试数据
files:测试文件
log:日志文件目录
report:测试报告目录
test_case:测试用例、测试集相关目录
utils:工具类,基础数据创建及获取等操作。
自建库
- 自建库(base-api)
- base_case
- 通用执行用例类
- 判断token状态及获取token
- 断言封装
- base_requests
- send_request(请求参数处理)
- 用例参数解析
- allure报告展示字段设置
- send_api(请求封装)
- base_case
- 自建库(tools)
- 测试数据构建工具
- DataProcess(数据处理)
- 提取参数处理
- 文件操作
无自建库可参考目录
api: 主程序目录
comm:公共函数,包括:接口请求基类、请求及相应数据操作基类等
intf_handle:接口操作层,包含:接口初始化、断言等
business:业务实现部分
utils:工具类,包括:读取文件、发送邮件、excel操作、数据库操作、日期时间格式化等
config:配置文件目录,包含yaml配置文件、以及路径配置
data:测试数据目录,用于存放测试数据
temp:临时文件目录,用于存放临时文件
result:结果目录
report:测试报告目录,用于存放生成的html报告
details:测试结果详情目录,用于存放生成的测试用例执行结果excel文件
log:日志文件目录
test:测试用例、测试集相关目录,启动test_suite执行用例文件存放在此
test_case:测试用例存放路径
test_suite:测试模块集,按模块组装用例
三、核心方法设计
接口请求基类
# -*- coding:utf-8 -*-
import datetime
import urllib
import requests
from tools import allure_step, allure_title, logger, allure_step_no, updateDict, get_r, form_data_type_judge
from tools.data_process import DataProcess
from tools.read_file import ReadFile
class BaseRequest(object):
session = None
@classmethod
def get_session(cls):
"""
单例模式保证测试过程中使用的都是一个session对象
:return:
"""
if cls.session is None:
cls.session = requests.Session()
return cls.session
@classmethod
def send_request(cls, case: dict, reverse=True):
"""
请求参数处理
:param case: 读取出来的每一行用例内容,可进行解包
:param env: 环境名称 默认使用config.yaml server下的 test 后面的基准地址
return: 响应结果, 预期结果
"""
title = case.get('title')
header = case.get('header')
path = case.get('path')
method = case.get('method')
file_obj = case.get('files')
params = case.get('params')
data = case.get('data')
jsonData = case.get('json')
extract = case.get('extract')
expect = case.get('expect')
step = case.get('step')
# 关联功能用例
relation = case.get('relation')
# 自定义断言
expect_custom = case.get('expect_custom')
logger.debug(
f"\n用例进行处理前数据: \n用例标题:{title} \n 请求头:{header} \n 接口路径:{path} \n params:{params} \n data:{data} \n json:{jsonData} \n file:{file_obj} \n 提取参数:{extract} \n 预期结果:{expect} \n")
# 如果不传title,就不设置
if title is not None:
# allure报告 用例标题
allure_title(title)
if step is not None:
# 报告右侧步骤中增加步骤
allure_step_no(f'步骤: {step}')
# 如果传method,默认为GET请求
if method is None:
method = 'GET'
# 处理url、header、data、files、的前置方法
url = DataProcess.handle_path(path)
params = DataProcess.handle_data(params)
data = DataProcess.handle_data(data)
jsonData = DataProcess.handle_data(jsonData)
# 每个请求增加_r参数
params = updateDict(params, "_r", get_r())
# 判断是否有resign,有的话就添加在params中
resign = ReadFile.read_config('$.resign')
if resign != False:
params = updateDict(params, "resign", resign)
# 判断是否有bazi参数,有就增加在params,这个参数广告的需要,响应解密
bazi = ReadFile.read_config('$.bazi')
if bazi != False:
params = updateDict(params, "bazi", bazi)
# 有请求数据再在报告中展示
requestsData = {}
if params:
requestsData.update({'params': params})
if data:
# allure报告数据需要
requestsData.update({'data': data})
# 再判断data中有没有传"data_type": "multipart/form-data",如果有就处理为二进制方式提交数据(上传文件方式)
data_handle = form_data_type_judge(data)
if isinstance(data_handle, tuple):
# 如果是元组,说明是这种类型,第一个数据是data转为二进制的数据,第二个是content-type的类型
data = data_handle[0]
content_type = data_handle[1]
if header is None:
header = {}
header.update({"Content-Type": content_type})
else:
data = data_handle
# 请求头处理
header = DataProcess.handle_header(header)
if jsonData:
requestsData.update({'json': jsonData})
allure_step('请求数据', requestsData)
allure_step_no(f'请求时间: {datetime.datetime.now()}')
file = DataProcess.handler_files(file_obj)
# 发送请求
response = cls.send_api(url, method, header, params, data, jsonData, file)
# 处理请求前extract在报告中展示
if extract is not None:
allure_step("请求前extract", extract)
# 提取参数
report_extract = DataProcess.handle_extra(extract, response)
# 设置报告替换的extract
if report_extract is not None or report_extract != {} or report_extract != "None":
logger.info("请求后的extract" + str(report_extract))
allure_step("请求后extract", report_extract)
logger.info("当前可用参数池" + str(DataProcess.extra_pool))
allure_step("当前可用参数池", DataProcess.extra_pool)
# 如果没有填预期结果,默认断言响应中的success=True
if expect is None and expect_custom is None:
if reverse:
expect = {"$.success": True}
else:
expect = {"$.success": False}
if expect is not None:
allure_step("请求前expect", expect)
if expect_custom is not None:
allure_step("请求前expect_custom", expect_custom)
# 存储关联的用例id和维护人员
if relation is not None:
relationId = relation.get("id")
if relationId is not None:
if relation not in DataProcess.extra_pool.get("relations"):
DataProcess.extra_pool.get("relations").append(relation)
return response, expect, expect_custom
@classmethod
def send_api(cls, url, method, header=None, params=None, data=None, jsonData=None, file=None, allure=True) -> dict:
"""
封装请求
:param url: url
:param method: get、post...
:param header: 请求头
:param params: 查询参数类型,明文传输,一般在url?参数名=参数值
:param data: 一般用于form表单类型参数
:param jsonData: json类型参数
:param file: 文件参数
:return: 响应结果
"""
session = cls.get_session()
res = session.request(method, url, params=params, data=data, json=jsonData, files=file, headers=header)
try:
response = res.json()
except Exception:
# 这里return 二进制内容,文件下载需要接收
response = res.content
if allure:
allure_step_no(f'响应耗时(s): {res.elapsed.total_seconds()}')
if isinstance(response, bytes):
if "html" in str(response):
response_result = res.text
else:
response_result = "响应结果为二进制文件"
else:
response_result = response
if allure:
allure_step("响应结果", response_result)
logger.info(
f'\n最终请求地址:{urllib.parse.unquote(res.url)}\n 请求方法:{method}\n 请求头:{res.request.headers}\n params:{params}\n data:{data}\n json:{jsonData}\n file:{file}\n 响应数据:{response_result}')
return response
执行用例类
@classmethod
def execute(cls, data=None, reverse=True):
"""
通用执行用例的方法:默认通用断言,断言响应中的success为True
:param data:
:return:
"""
# 运行前先判断accessToken
cls.__grant_access_token()
# 调用方法合并path和method
data = cls.basic_attr(data)
# 执行用例->所有参数字典丢给send_request处理即可
response, expect, expect_custom = BaseRequest.send_request(data, reverse)
# 通用断言
if expect is not None:
DataProcess.assert_result(response, expect)
# 自定义断言
if expect_custom is not None:
DataProcess.expect_keyword(response, expect_custom)
return response, expect, expect_custom
tool-字典合并
def merge_dict(dic1, dic2):
"""
递归合并两个字典所有数据,有相同的就更新,不相同的就添加
:param dic1: 基本数据
:param dic2: 以dic2数据为准,dic1和dic2都有的数据,合并后以dic2为准
:return: 合并后的字典
"""
# 类型不同就直接赋值,返回第2个参数数据,是因为我们以第2个数据为准,来更新第1个数据的。
if type(dic1) != type(dic2):
return dic2
# 两个字典深拷贝一下,避免影响之前数据
obj1 = copy.deepcopy(dic1)
obj2 = copy.deepcopy(dic2)
# 都是字典时处理
if isinstance(obj2, dict):
for k, v in obj2.items():
obj1_value = obj1.get(k)
if obj1_value is None:
obj1[k] = v
else:
obj1[k] = merge_dict(obj1[k], obj2[k])
elif isinstance(obj2, list):
for i in range(len(obj2)):
try:
obj1[i] = merge_dict(obj1[i], obj2[i])
except IndexError:
obj1.append(obj2[i])
elif isinstance(obj2, tuple):
for i in range(len(obj2)):
try:
# 元组不能修改,先转list再修改后再转回元组
obj1 = list(obj1)
obj1[i] = merge_dict(obj1[i], obj2[i])
obj1 = tuple(obj1)
except IndexError:
obj1 += (obj2[i],)
else:
# 以第2个参数数据为准,返回obj2
return obj2
return obj1
tool-文件处理
import yaml
from tools import extractor
class ReadFile:
"""
文件操作
"""
config_dict = None
@classmethod
def get_config_dict(cls, config_path='config/config.yaml') -> dict:
"""
读取配置文件,并且转换成字典,缓存至config_dict
:param config_path: yaml文件地址, 默认使用当前项目目录下的config/config.yaml
return cls.config_dict
"""
if cls.config_dict is None:
# 指定编码格式解决,win下跑代码抛出错误
with open(config_path, 'r', encoding='utf-8') as file:
cls.config_dict = yaml.load(
file.read(), Loader=yaml.FullLoader)
return cls.config_dict
@classmethod
def read_config(cls, expr: str = '.'):
"""
默认读取config目录下的config.yaml配置文件,根据传递的expr jsonpath表达式可任意返回任何配置项
:param expr: 提取表达式, 使用jsonpath语法,默认值提取整个读取的对象
return 根据表达式返回的值
"""
return extractor(cls.get_config_dict(), expr)
@classmethod
def read_yaml(cls, path):
"""
读取yaml文件
:param path: 文件路径
:return: 返回读取的文件数据,dict类型
"""
# 指定编码格式解决,win下跑代码抛出错误
with open(path, 'r', encoding='utf-8') as file:
data = yaml.load(file.read(), Loader=yaml.FullLoader)
return data
用例编写-conftest.py
import pytest
@pytest.fixture
def test_h5_2xxx():
test_h5_1xxx.execute(data) # 前置执行用例
yield # 以下为后置执行用例
expect = DataProcess.extra_pool.get('expect')
data = {
"data": {
"searchTimeScope": "0",
"scope": "2",
'pageType': 'first',
'pageSize': '15'
},
'expect': expect
}
test_h5_3xxx.execute(data)
用例编写
import pytest
from tools import case_data_generate, merge_dict
class Test_xxx:
@pytest.mark.parametrize("params", case_data_generate(Api_xxx.normal)) # 参数化
def test_xxx(
self, params, test_h5_2xxx): # 前置执行conftest中“test_h5_2xxx用例”
param, data, expect = params
request_data = merge_dict(Api_xxx.params_required, {param: data}) # 提取并合并用例参数
data = {
"title": f"第一条用例",
"params": request_data,
"expect": expect # 断言
}
Api_xxx.execute(data) # 用例执行请求
============================= 提升自己 ==========================
进群交流、获取更多干货, 请关注微信公众号:
> > > 咨询交流、进群,请加微信,备注来意:sanshu1318 (←点击获取二维码)
> > > 学习路线+测试实用干货精选汇总:
https://www.cnblogs.com/upstudy/p/15859768.html
> > > 【自动化测试实战】python+requests+Pytest+Excel+Allure,测试都在学的热门技术:
https://www.cnblogs.com/upstudy/p/15921045.html
> > > 【热门测试技术,建议收藏备用】项目实战、简历、笔试题、面试题、职业规划:
https://www.cnblogs.com/upstudy/p/15901367.html
> > > 声明:如有侵权,请联系删除。
============================= 升职加薪 ==========================
更多干货,正在挤时间不断更新中,敬请关注+期待。
进群交流、获取更多干货, 请关注微信公众号:
> > > 咨询交流、进群,请加微信,备注来意:sanshu1318 (←点击获取二维码)
> > > 学习路线+测试实用干货精选汇总:
https://www.cnblogs.com/upstudy/p/15859768.html
> > > 【自动化测试实战】python+requests+Pytest+Excel+Allure,测试都在学的热门技术:
https://www.cnblogs.com/upstudy/p/15921045.html
> > > 【热门测试技术,建议收藏备用】项目实战、简历、笔试题、面试题、职业规划:
https://www.cnblogs.com/upstudy/p/15901367.html
> > > 声明:如有侵权,请联系删除。
============================= 升职加薪 ==========================
更多干货,正在挤时间不断更新中,敬请关注+期待。