python+pytest+requests+jinja2+jsonpath+jenkins接口自动化测试框架
简单介绍
- 支持各个环境变量参数分离
- 支持参数化,yaml测试用例文件内支持全局参数化,使用jinja2实现变量参数化,{{}} 表示变量
- 支持前置、后置条件
- 现支持get/post请求,上传文件
- 支持code断言、响应模糊断言、sql断言、json提取断言
- 支持提取上个接口响应参数给所有请求使用
- 只需要编写yaml测试用例数据文件
目录

逻辑流程图

各个文件说明
assert_type.py:断言方法
# coding=utf-8
import pytest
import jsonpath
from config import Config
"""
1.assert_data: 断言数据
2.expect_value: 期望值
code= 接口响应code
result= 接口响应result
message= 接口响应全部信息message
sql= sql查询语句查询结果判断是否包含预期
json_extract= json提取表达式提接口响应值判断是否包含预期
3.actual_value: 实际返回值
"""
def assert_type(assert_data, expect):
# assert_data = assert_data[0].get('assert')
if assert_data.get('code'):
expect_value = assert_data.get('code')
print('code 断言期望值: {}'.format(expect_value))
# print('期望值:{}'.format(expect))
actual_value = expect.get('code')
print('code 返回实际值: {}'.format(actual_value))
assert_result = pytest.assume(str(expect_value) == str(actual_value))
print('code 断言结果: {}\n'.format(assert_result))
if assert_data.get('result'):
expect_value = assert_data.get('result')
print('result 断言期望值: {}'.format(expect_value))
actual_value = expect.get('result')
print('result 返回实际值: {}'.format(actual_value))
assert_result = pytest.assume(expect_value == actual_value)
print('result 断言结果: {}\n'.format(assert_result))
if assert_data.get('message'):
expect_value = assert_data.get('message')
print('message 断言期望值: {}'.format(expect_value))
print('message 返回实际值: {}'.format(expect))
assert_result = pytest.assume(str(expect_value) in str(expect))
print('message 断言结果: {}\n'.format(assert_result))
if assert_data.get('sql'):
expect_value = assert_data.get('sql').get('expect')
select = assert_data.get('sql').get('select')
print('sql 断言期望值: {}'.format(expect_value))
print('sql 断言查询语句: {}'.format(select))
actual_value = Config().mysql(select)
print('sql 返回实际值: {}'.format(actual_value))
assert_result = pytest.assume(expect_value in actual_value)
print('sql 断言结果: {}\n'.format(assert_result))
if assert_data.get('json_extract'):
expect_value = assert_data.get('json_extract').get('expect')
print('json提取 断言期望值: {}'.format(expect_value))
extract_expression = assert_data.get('json_extract').get('extract_expression')
actual_value = jsonpath.jsonpath(expect, assert_data.get('json_extract').get('extract_expression'))
print('json提取 断言提取表达式: {}'.format(extract_expression))
print('json提取 断言返回实际值: {}'.format(actual_value))
assert_result = pytest.assume(expect_value in actual_value)
print('json提取 断言结果: {}\n'.format(assert_result))
function_variable.py:时间戳、随机字符串等方法
# coding=utf-8
import time
import uuid
import random
# 当前时间
def now_time():
now_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
return now_time
# 10位时间戳
def timestamp_10():
timestamp_10 = str(int(round(time.time())))
print(timestamp_10)
return timestamp_10
# 13位时间戳
def timestamp_13():
timestamp_13 = str(int(round(time.time() * 1000)))
print(timestamp_13)
return timestamp_13
# 8位随机数
def str_8():
str_8 = str(uuid.uuid4()).replace('-', '')[1:9]
print(str_8)
return str_8
# 16位随机数
def str_16():
str_16 = str(uuid.uuid4()).replace('-', '')[1:17]
print(str_16)
return str_16
# 随机手机号
def phone_number():
phone_number_list = ['133', '153', '180', '181', '189', '177', '173', '149',
'130', '131', '132', '155', '156', '176', '185', '186',
'135', '136', '137', '138', '150', '151', '152', '157']
phone_number = random.choice(phone_number_list) + str(random.sample(range(0, 10), 8)).replace('[', '').replace(']', '').replace(',', '').replace(' ', '')
print(phone_number)
return phone_number
utility.py:工具方法
# coding=utf-8
import jsonpath
import yaml
from common import function_variable
# 响应提取参数组装
def response_extract(response_data, extract):
extract_dict = {}
for i in extract:
extract_dict.update({i: jsonpath.jsonpath(response_data, extract.get(i))[0]})
# print(extract_dict)
return extract_dict
# 处理yaml文件
class DisposeYamlFile:
def __init__(self, path):
self.path = path
# 写入 yaml
def write_yaml_file(self, data):
with open(self.path, 'w', encoding='utf-8') as f:
yaml.dump(data, f, allow_unicode=True)
# 读取 yaml
def read_yaml_file(self):
with open(self.path, 'r', encoding='utf-8') as f:
data = f.read()
data = yaml.load(data, Loader=yaml.FullLoader)
return data
# 处理文件
class DisposeFile:
def __init__(self, path):
self.path = path
# 写入 文件
def write_file(self, data):
with open(self.path, 'w', encoding='utf-8') as f:
yaml.dump(data, f, allow_unicode=True)
# 读取 文件
def read_file(self):
with open(self.path, 'r', encoding='utf-8') as f:
data = f.read()
return data
replace_case_param.py:用例参数处理,替换变量等
# coding=utf-8
import os
import ast
import yaml
import jinja2
from config import env
from common import function_variable
class ReadReplace:
def __init__(self, case_file_name=''):
path = os.path.abspath(os.path.dirname(__file__)).replace('replace_param', '')
self.case_path = path + 'test_data/' + case_file_name + '.yaml'
# print(self.path)
if env() == 'test': self.variate_path = path + 'config/test/variate_data.yaml'
if env() == 'dev': self.variate_path = path + 'config/dev/variate_data.yaml'
# print(self.variate_path)
if env() == 'test': self.response_path = path + 'config/test/response_data.yaml'
if env() == 'dev': self.response_path = path + 'config/dev/response_data.yaml'
# print(self.response_path)
def read_response(self):
with open(self.response_path, 'r', encoding='utf-8') as f:
data = f.read()
data = yaml.load(data, Loader=yaml.FullLoader)
# print('响应提取参数:\n {}'.format(data))
return data
def function_variable(self):
for i in dir(function_variable):
print(i)
function_variable_value = ''
def read_variate(self):
with open(self.variate_path, 'r', encoding='utf-8') as f:
data = f.read()
data = yaml.load(data, Loader=yaml.FullLoader)
data.update(self.read_response())
# print('变量参数:\n {}'.format(data))
return data
def read_case(self):
case_list_data = []
with open(self.case_path, 'r', encoding='utf-8') as f:
data = f.read()
data = yaml.load(data, Loader=yaml.FullLoader)
for case_data in data:
case_list_data.append([case_data])
# print('用例参数:\n {}'.format(case_list_data))
return case_list_data
def replace_data(self):
data = ast.literal_eval(jinja2.Template(str(self.read_case())).render(self.read_variate()))
for i in data:
if i[0].get('type') == 'api_info':
module = i[0].get('module')
submodule = i[0].get('submodule')
name = i[0].get('name')
path = i[0].get('path')
request_type = i[0].get('request_type')
head = i[0].get('head')
if not i[0].get('type'):
i[0].update({'module': module})
i[0].update({'submodule': submodule})
i[0].update({'name': name})
i[0].update({'path': path})
i[0].update({'request_type': request_type})
i[0].update({'head': head})
if i[0].get('type') == 'befor' or i[0].get('type') == 'after':
i[0].update({'module': module})
i[0].update({'submodule': submodule})
if i[0].get('type') == 'api_info':
del i[0]
data = list(filter(None, data))
print('替换变量后用例参数:\n {}'.format(data))
return data
request_api.py:请求处理
# coding=utf-8
import json
import requests
from common.utility import DisposeFile
def __request(_url, _head, _type, _param):
if _type == 'get' and 'application/x-www-form-urlencoded' == _head.get('Content-Type'):
try:
r = requests.get(
url= _url,
headers= _head,
data={'jsonString': json.dumps(_param)})
return {'response_result' : r.json(), 'request_body' : r.request.body}
except Exception as error_msg:
print('请求错误: {}'.format(error_msg))
if _type == 'get' and 'application/json' == _head.get('Content-Type'):
try:
r = requests.get(
url= _url,
headers= _head,
json= _param)
return {'response_result': r.json(), 'request_body': r.request.body}
except Exception as error_msg:
print('请求错误: {}'.format(error_msg))
if _type == 'post' and 'application/x-www-form-urlencoded' == _head.get('Content-Type'):
try:
if 'file_path' in _param:
r = requests.post(url = _url,
headers = _head,
data = json.dumps(_param),
files = DisposeFile(_param.get('file')).read_file())
else:
r = requests.post(url = _url,
headers = _head,
data = {'jsonString': json.dumps(_param)})
return {'response_result' : r.json(), 'request_body' : r.request.body}
except Exception as error_msg:
print('请求错误: {}'.format(error_msg))
if _type == 'post' and 'application/json' == _head.get('Content-Type'):
try:
if 'file_path' in _param:
r = requests.post(url = _url,
headers = _head,
json = _param,
files = DisposeFile(_param.get('file')).read_file())
else:
r = requests.post(url=_url,
headers=_head,
json=_param)
return {'response_result' : r.json(), 'request_body' : r.request.body}
except Exception as error_msg:
print('请求错误: {}'.format(error_msg))
config.py:配置相关
# coding=utf-8
import os
import yaml
import pymysql
def env():
return 'test'
class Config:
def __init__(self):
self.path = os.path.abspath(os.path.dirname(__file__))
if env() == 'test':
self.config_path = self.path + '/config/test/config.yaml'
else:
self.config_path = self.path + '/config/dev/config.yaml'
with open(self.config_path, 'r', encoding='utf-8') as f:
data = f.read()
self.data = yaml.load(data, Loader=yaml.FullLoader)
def get_domain(self):
domain = self.data.get('domain')
# print('域名: {}'.format(domain))
return domain
def mysql(self, select):
mysql = self.data.get('mysql')
host = mysql.get('ip')
port = mysql.get('port')
user = mysql.get('user')
pw = mysql.get('pw')
# print('mysql: {}'.format(mysql))
try:
connect = pymysql.connect(host=host, port=port, user=user, password=pw, charset='utf8', connect_timeout=5)
cur = connect.cursor()
cur.execute(select)
result = str(cur.fetchall()).replace('(', '').replace(')', '').replace(',', '')
# print('sql 语句: \n{}\n'.format(select))
# print('sql 语句执行结果: \n{}\n'.format(result))
return result
except Exception as error_msg:
print('连接数据库错误: {}\n'.format(error_msg))
finally:
connect.close()
def write_response(self, data):
if env() == 'test':
write_response_path = self.path + '/config/test/response_data.yaml'
else:
write_response_path = self.path + '/config/dev/response_data.yaml'
with open(write_response_path, 'w', encoding='utf-8') as f:
yaml.dump(data, f, allow_unicode=True)
conftest.py:前置处理
# coding=utf-8
import json
import pytest
from config import Config
from assert_type.assert_type import assert_type
from common.utility import response_extract
from request_api.request_api import __request
@pytest.fixture(scope='module')
def api_data(request):
data = request.param
for api_info in data:
module = api_info.get('module')
print('\n接口模块: {}'.format(module))
submodule = api_info.get('submodule')
print('接口子模块: {}'.format(submodule))
name = api_info.get('name')
print('接口名称: {}'.format(name))
domain = Config().get_domain()
print('域名: {}'.format(domain))
request_type = api_info.get('request_type')
print('请求方式: {}'.format(request_type))
head = api_info.get('head')
print('请求头: {}'.format(json.dumps(head)))
path = api_info.get('path')
print('路径: {}\n'.format(path))
url = Config().get_domain() + str(api_info.get('path'))
# print('接口请求url: {}'.format(url))
case_type = api_info.get('case_type')
print('用例类型: {}'.format(case_type))
title = api_info.get('title')
print('用例标题: {}'.format(title))
param = api_info.get('param')
# print('请求参数: {}'.format(param))
assert_data = api_info.get('assert')
# print('断言参数: \n{}'.format(assert_data))
response = __request(url, head, request_type, param)
# request_body = response.get('request_body')
response_result = response.get('response_result')
print('\n请求参数: \n{}\n'.format(json.dumps(param, ensure_ascii=False)))
# print('请求参数: \n{}'.format(urllib.parse.unquote(request_body)).replace('+', ''))
print('响应结果: \n{}\n'.format(json.dumps(response_result, ensure_ascii=False)))
if api_info.get('assert'):
assert_type(assert_data, response_result)
# print('断言结果: {}'.format(assert_type(assert_data, response_result)))
if api_info.get('response_extract'):
response_extract_dta = response_extract(response_result, api_info.get('response_extract'))
print('响应提取表达式: {}'.format(api_info.get('response_extract')))
print('响应提取值: {}'.format(response_extract_dta))
Config().write_response(response_extract_dta)
return '{} - {}'.format(case_type, title)
pytest.ini:pytest运行配置
[pytest] testpaths = test_case python_files = test*.py *test.py python_classes = Test* *Test python_functions = test* *test
.py测试文件
# coding=utf-8
import allure
import pytest
from replace_param.replace_case_param import ReadReplace
@allure.epic('模块')
@pytest.mark.parametrize('api_data', ReadReplace('yaml用例文件').replace_data(), indirect=True)
@allure.story('子模块')
@allure.title("{api_data}")
def test(api_data):
pass
.yaml测试用例数据文件
- type: "api_info" # api_info是接口信息
module: "模块"
submodule: "子模块"
name: "接口名称"
path : "接口路径"
request_type : "请求方式"
head :
Content-Type : "application/x-www-form-urlencoded"
groupCode : "BR"
- type: "befor" befor是前置条件
case_type: '前置条件'
name: "前置条件名称"
title: "保存 test 供 test 使用"
path : "路径"
request_type: "请求方式"
head:
Content-Type: "application/x-www-form-urlencoded"
groupCode: "BR"
param : param是参数
puser:
userId : "{{userId}}"
userName : "{{userName}}"
assert : assert是断言
code : "200"
message : "success"
- title : "成功" # 用例标题
case_type : "正" # 用例类型
param :
puser:
userId : "{{userId}}"
userName : "{{userName}}"
assert :
code : "00000"
message : "success"
sql : # sql断言参数
expect : "期望值"
select : "sql语句"
json_extract : # json path提取断言参数
expect : "期望值"
extract_expression : "json path提取表达式"
html报告


浙公网安备 33010602011771号