文章目录
接着 上一篇文章,继续进行框架封装
一、框架封装规则总结
接口自动化测试框架 yaml 用例编写规则:
1、必须有的四个一级关键字:name,base_url,request,validate
2、在 request 一级关键字下必须包括两个二级关键字:method,url
3、传参方式:在 request 一级关键字下,通过二级关键字传参:
若是 get 请求,通过 params 关键字传参
若是 post 请求:
传 json 格式,通过 json 关键字传参
传表单格式,通过 data 关键字传参
传文件格式,通过 files 关键字传参
4、如果需要接口关联的话,必须使用一级关键字:extract
提取值:
如:json 提取方式
extract:
access_token: access_token
如:正则表达式提取方式
extract:
access_token: '"access_token":"(.*?)"'
取值:
如:access_token: "{{access_token}}"
5、热加载,当 yaml 需要使用动态参数时,那么可以在 debug_talk.py 文件中写方法调用
注意:传参时,需要什么类型的数据,需要做强转。 int(min),int(max)
如:
# 获取随机数的方法
def get_random_number(self,start,stop):
name = random.randrange(int(start),int(stop))
return name
6、断言,以数组的方式编写每个断言
如:
validate:
- equals: {"status_code": 200}
- contains: url
二、框架代码简单实现
yaml_util.py - 处理 yaml 数据
import os
import yaml
# 获取项目根路径
def get_object_path():
return os.getcwd().split('common')[0]
# 读取 config.yml 文件
def read_config_yaml(first_node,second_node):
with open(get_object_path()+'/config.yml', 'r', encoding='utf-8') as f:
yaml_config = yaml.load(f, Loader=yaml.FullLoader)
return yaml_config[first_node][second_node]
# 读取 extract.yml 文件
def read_extract_yaml(first_node):
with open(get_object_path()+'/extract.yml', 'r', encoding='utf-8') as f:
yaml_config = yaml.load(f, Loader=yaml.FullLoader)
return yaml_config[first_node]
# 写入 extract.yml 文件
def write_extract_yaml(data):
with open(get_object_path()+'/extract.yml', 'a', encoding='utf-8') as f:
yaml.dump(data, f,allow_unicode=True)
# 清空 extract.yml 文件
def clear_extract_yaml():
with open(get_object_path()+'/extract.yml', 'w', encoding='utf-8') as f:
f.truncate()
# 读取 YAML 测试用例文件
def read_testcase_yaml(yaml_path):
with open(get_object_path()+yaml_path, 'r', encoding='utf-8') as f:
yaml_value = yaml.load(f, Loader=yaml.FullLoader)
return yaml_value
debug_talk.py - 热加载函数
import random
import time
class DebugTalk:
# 获取随机数的方法
def get_random_number(self, start, stop):
name = random.randrange(int(start), int(stop))
return name
# 获取时间戳的方法
def get_timestamp_str(self):
name = time.strftime("_%Y%m%d_%H%M%S")
return name
requests_util.py - 将请求封装在同一个方法中
import json
import re
import jsonpath
import requests
from common.yaml_util import read_extract_yaml, write_extract_yaml
from debug_talk import DebugTalk
class RequestsUtil:
# 创建 session 会话对象
session = requests.Session()
# 寻找字典中某个 key 的值
def find_key(self,key,data):
# 处理字典类型(对象)
if isinstance(data, dict):
if key in data:
return data[key]
# 递归遍历所有值
for value in data.values():
result = self.find_key(key,value)
if result is not None:
return result
# 其他类型(如字符串、数字、数组等)直接跳过
return None
# 规范功能测试 YAML 测试用例文件的写法
def analysis_yaml(self, case_info):
# 1、必须有的四个一级关键字:name,base_url,request,validate
case_info_keys = dict(case_info).keys()
if 'name' in case_info_keys and 'base_url' in case_info_keys and 'request' in case_info_keys and 'validate' in case_info_keys:
# 2、在 request 一级关键字下必须包括两个二级关键字:method,url
request_keys = dict(case_info['request']).keys()
if 'method' in request_keys and 'url' in request_keys:
method = case_info['request']['method']
url = case_info['base_url'] + case_info['request']['url']
# 应该把method、url、headers、files四个参数从case_info['request']去掉后,剩下的数据传给kwargs
case_info['request'].pop('method')
case_info['request'].pop('url')
# 参数(params、data、json),请求头,文件上传这些都不能做约束
headers = None
if jsonpath.jsonpath(case_info, '$..headers') :
headers = case_info['request']['headers']
case_info['request'].pop('headers')
files = None
if jsonpath.jsonpath(case_info, '$..files'):
files = case_info['request']['files']
for key, value in dict(files).items():
files[key] = open(value,"rb")
case_info['request'].pop('files')
res = self.send_request(method, url, headers=headers, files=files, **case_info['request'])
# 提取接口关联的变量,既要支持正则表达式也要支持json提取
if 'extract' in case_info_keys:
for key, value in dict(case_info['extract']).items():
if '(.+?)' in value or '(.*?)' in value: # 正则表达式提取
re_value = re.search(value, res.text).group(1)
if re_value:
extract_data = {key: re_value}
write_extract_yaml(extract_data)
else: # json 提取
# 把中间变量写入 extract.yml 文件
extract_value = self.find_key(value,res.json())
if extract_value:
extract_data = {key: extract_value}
write_extract_yaml(extract_data)
# 断言的封装调用
self.validate_result(case_info['validate'], res.json(), res.status_code)
return res
else:
print('2、在 request 一级关键字下必须包括两个二级关键字:method,url')
else:
print('1、必须有的四个一级关键字:name,base_url,request,validate')
# 统一替换的方法,data 可以是 url(string),也可以是参数(字典,字典中包含列表),也可以是请求头(字典)
# 比如把 access_token: "{{access_token}}" 中的{{access_token}} 替换成 真实的值
def replace_value(self,data_value):
# 不管是什么类型统一转换成字符串格式
if data_value and isinstance(data_value, dict): # 如果 data 不为空且 data 类型是一个字典
str_data = json.dumps(data_value)
else:
str_data = data_value
# 替换值
for a in range(1, str_data.count('{{')+1):
if '{{' in str_data and '}}' in str_data:
start_index = str_data.index('{{')
end_index = str_data.index('}}')
old_value = str_data[start_index:end_index+2]
new_value = read_extract_yaml(old_value[2:-2])
str_data = str_data.replace(old_value, str(new_value))
# 还原数据类型
if data_value and isinstance(data_value, dict): # 如果 data 不为空且 data类型是一个字典
data_value = json.loads(str_data)
else:
data_value = str_data
return data_value
# 热加载替换解析
# 比如{"name": "hc${get_random_number(10000,99999)}"} 会执行函数 getattr(self,"get_random_number")(10000,99999)
def replace_load(self, data_value):
# 不管是什么类型统一转换成字符串格式
if data_value and isinstance(data_value, dict): # 如果 data 不为空且 data 类型是一个字典
str_data = json.dumps(data_value)
else:
str_data = data_value
# 替换 ${函数调用} 为函数调用返回的值
for a in range(1, str_data.count('${') + 1):
if '${' in str_data and '}' in str_data:
start_index = str_data.index('${')
end_index = str_data.index('}')
old_value = str_data[start_index:end_index + 1]
func_name = old_value[2:old_value.index('(')]
args_value = old_value[old_value.index('(')+1:old_value.index(')')]
# 反射(通过一个函数的字符串直接去调用这个方法)
if args_value:
new_value = getattr(DebugTalk(), func_name)(*args_value.split(','))
else:
new_value = getattr(DebugTalk(), func_name)()
str_data = str_data.replace(old_value, str(new_value))
# 还原数据类型
if data_value and isinstance(data_value, dict): # 如果 data 不为空且 data 类型是一个字典
data_value = json.loads(str_data)
else:
data_value = str_data
return data_value
# 统一发送请求的方法:
def send_request(self, method, url, headers=None, **kwargs):
# 处理 method 统一为小写
lower_method = str(method).lower()
# 处理基础路径
# base_url = read_config_yaml("url", "base")
# second_url = self.replace_value(read_config_yaml("url", url))
# 处理请求头
if headers:
headers = self.replace_value(headers)
# 最核心的地方:请求数据如何去替换,可能是 params、data、json
for k, v in kwargs.items():
if k in ['params', 'data', 'json']:
value = self.replace_value(v)
result_value = self.replace_load(value)
kwargs[k] = result_value
# 发送请求
res = RequestsUtil.session.request(method=lower_method,url=url,headers=headers,**kwargs)
return res
# 断言封装
def validate_result(self,expect_result,real_result,status_code):
print("预期结果:"+str(expect_result))
print("实际结果:"+str(real_result))
# 断言的标记,flag = 0 断言成功,flag 不等于 0 断言失败
flag = 0
# 解析
if expect_result and isinstance(expect_result,list):
for expect_value in expect_result:
for key, value in dict(expect_value).items():
if key == "equals":
for assert_key,assert_value in dict(value).items():
if assert_key == "status_code":
if status_code == assert_value:
print("状态码断言成功")
else:
flag = flag + 1
print("状态码断言失败:" + assert_key + "不等于"+ assert_value)
else:
key_list = jsonpath.jsonpath(real_result,"$..%s"%assert_key)
if key_list:
if assert_value not in key_list:
flag = flag + 1
print("断言失败:" + assert_key + "不等于" + assert_value)
else:
print(assert_key+"-断言成功")
else:
flag = flag + 1
print("断言失败:返回的结果中不存在" + assert_key)
elif key == "contains":
if value not in json.dumps(real_result):
flag = flag + 1
print("断言失败:返回的结果中不包含" + value)
else:
print(value + "-断言成功")
else:
print("框架不支持断言封装")
assert flag==0,"断言失败!"
get_token.yml 文件
- name: get correct user token
base_url: https://api.weixin.qq.com
request:
url: /cgi-bin/token
method: GET
params:
appid: wxcb292044d4fdfd11
secret: 69be902b0619de6bf75af4b0b9992645
grant_type: client_credential
validate:
- equals: {"status_code": 200}
- equals: {"expires_in": 7200}
- contains: access_token
extract:
access_token: access_token
# access_token: '"access_token":"(.*?)"'
create_tag.yml 文件
- name: Create the user's tag
base_url: https://api.weixin.qq.com
request:
url: /cgi-bin/tags/create
method: POST
params:
access_token: "{{access_token}}"
json: {"tag": {"name": "hc_create${get_random_number(10000,99999)}"}}
validate:
- equals: {"status_code": 200}
- contains: tag
extract:
tag_id: id
update_tag.yml 文件
- name: Update the user's tag
base_url: https://api.weixin.qq.com
request:
url: /cgi-bin/tags/update
method: POST
params:
access_token: "{{access_token}}"
json: {"tag": {"id": "{{tag_id}}","name": "hc_update${get_timestamp_str()}"}}
validate:
- equals: {"status_code": 200}
- equals: {"errcode": 0}
- equals: {"errmsg": "ok"}
get_tag.yml 文件
- name: Get user's tags
base_url: https://api.weixin.qq.com
request:
url: /cgi-bin/tags/get
method: GET
params:
access_token: "{{access_token}}"
validate:
- equals: {"status_code": 200}
- contains: tags
upload_file.yml 文件
- name: Upload image file
base_url: https://api.weixin.qq.com
request:
url: /cgi-bin/media/uploadimg
method: POST
params:
access_token: "{{access_token}}"
files:
media: "F:/Pycharm/TestAPI/screenshots/logo.png"
validate:
- equals: {"status_code": 200}
- contains: url
test_tag.py - 测试用例执行
# -*- coding: utf-8 -*-
import json
import allure
import pytest
import requests
import yaml
from common.requests_util import RequestsUtil
from common.yaml_util import read_testcase_yaml, write_extract_yaml
@allure.epic("项目名称:接口自动化测试")
@allure.feature("模块名称:用户模块")
class TestUser:
@allure.story("接口名称:获取用户token")
@allure.severity(allure.severity_level.BLOCKER)
@pytest.mark.user
@pytest.mark.parametrize("case_info",read_testcase_yaml("/testcases/get_token.yml"))
def test_get_token(self,case_info):
res = RequestsUtil().analysis_yaml(case_info)
allure.attach(body=str(res.status_code) + res.text,name="响应数据:",attachment_type=allure.attachment_type.TEXT)
@allure.story("接口名称:创建用户标签")
@allure.severity(allure.severity_level.NORMAL)
@pytest.mark.user
@pytest.mark.parametrize("case_info", read_testcase_yaml("/testcases/create_tag.yml"))
def test_create_tag(self, case_info):
res = RequestsUtil().analysis_yaml(case_info)
allure.attach(body=str(res.status_code) + res.text, name="响应数据:",attachment_type=allure.attachment_type.TEXT)
@allure.story("接口名称:更新用户标签")
@allure.severity(allure.severity_level.NORMAL)
@pytest.mark.user
@pytest.mark.parametrize("case_info",read_testcase_yaml("/testcases/update_tag.yml"))
def test_update_tag(self,case_info):
res = RequestsUtil().analysis_yaml(case_info)
allure.attach(body=str(res.status_code) + res.text, name="响应数据:",attachment_type=allure.attachment_type.TEXT)
@allure.story("接口名称:获取用户标签")
@allure.severity(allure.severity_level.NORMAL)
@pytest.mark.user
@pytest.mark.parametrize("case_info", read_testcase_yaml("/testcases/get_tag.yml"))
def test_get_tag(self, case_info):
res = RequestsUtil().analysis_yaml(case_info)
allure.attach(body=str(res.status_code) + res.text, name="响应数据:",attachment_type=allure.attachment_type.TEXT)
@allure.story("接口名称:文件上传")
@allure.severity(allure.severity_level.NORMAL)
@pytest.mark.user
@pytest.mark.parametrize("case_info", read_testcase_yaml("/testcases/upload_file.yml"))
def test_upload_file(self, case_info):
res = RequestsUtil().analysis_yaml(case_info)
allure.attach(body=str(res.status_code) + res.text, name="响应数据:",
attachment_type=allure.attachment_type.TEXT)
conftest.py - 会话之前清除数据
import pytest
from common.yaml_util import clear_extract_yaml
@pytest.fixture(scope="session",autouse=True)
def clear_extract():
""" 每次会话之前清除 extract.yml 数据 """
clear_extract_yaml()