1. 根目录
#main.py
from utils.verify_file_hash import File_Operate
import pytest
def run():
file_operate = File_Operate('./log/', 'md5.json')
file_operate.create_md5()
new_dict = file_operate.file_map_md5('./data/')
file_operate.create_and_write(new_dict, './data/', './case/')
if __name__ == "__main__":
import sys
sys.path.append(r'.')
run()
pytest.main(['-s'])
#conftest.py
import pytest
from utils.http_request import http_request
from config.path import pathList
from config.config import setting
from utils.encrypto import encryption
@pytest.fixture(scope='class')
def getCookie():
login_cfg = {
"method": "post",
"url": pathList['doLogin'],
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"data": {
"userName": setting['username'],
"password": encryption(setting['password'], setting['pubkey'])
},
"verify": False
}
res = http_request(login_cfg)
return res.headers.get('Set-Cookie')
# allure本身就给测试方法统计了时长,如果自定义统计,可以通过request获取上下文信息
@pytest.fixture(scope='function', autouse=True)
def timer(request):
start = time.time()
yield
end = time.time()
print(f"""classname:{request.cls.__name__}
function_name:{request.function.__name__}
request_name:{request.node.callspec.params['cfg']['name']}
duration:{(end-start)/1000}s""")
2.utils目录
from typing import Dict
def assert_http_response(expected: Dict, actual: Dict, failed: Dict = None) -> None:
"""
Asserts the HTTP response against the expected values.
Args:
expected: The expected values.
actual: The actual values.
failed: The failed values.
Raises:
AssertionError: If the expected values do not match the actual values.
"""
if failed is None:
failed = {}
for key, value in expected.items():
if key not in actual:
failed[key] = value
elif isinstance(value, Dict) and isinstance(actual[key], Dict):
assert_http_response(value, actual[key], failed)
elif value != actual[key]:
failed[key] = value
if failed:
raise AssertionError(f"Failed assertions: {failed}")
#encrypto.py
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
import base64
def encryption(text: str, public_key: bytes) -> str:
text = text.encode('utf-8')
# 构建公钥对象
cipher_public = PKCS1_v1_5.new(RSA.importKey(public_key))
# 加密
text_encrypted = cipher_public.encrypt(text)
# base64编码,并转为字符串
text_encrypted_base64 = base64.b64encode(text_encrypted).decode()
return text_encrypted_base64
def format_string(cfg: dict, store: dict) -> dict:
for k, v in cfg.items():
if isinstance(v, dict):
# Recursively call format_string for nested dictionaries
cfg[k] = format_string(v, store)
elif isinstance(v, str) and '{' in v and '}' in v:
try:
# Use string formatting to replace placeholders with values from the store dictionary
cfg[k] = v.format(**store)
except KeyError:
# Handle the case where a key is missing from the store dictionary
pass
return cfg
import requests
from requests.models import Response
from config.config import setting
import urllib3
baseUrl = setting['baseUrl']
timeout = setting['timeout']
# Create a session object to reuse the underlying TCP connection
session = requests.Session()
def http_request(config: dict) -> Response:
urllib3.disable_warnings()
# Construct the full URL by combining the base URL and the endpoint URL
url = f"{baseUrl}{config['url']}"
headers = config['headers']
verify = config['verify']
data = config['data']
try:
# Send the HTTP request using the session object
res = session.request(
method=config['method'],
url=url,
headers=headers,
verify=verify,
timeout=timeout,
json=data
)
return res
except requests.exceptions.Timeout:
# Handle the case where the request times out
print('time out')
import json
import jsonpath
from requests import Response
def save(d, d1, j, store) -> None:
"""
This function extracts the values from the JSON response using the given JSONPath expressions and stores them in the
`store` dictionary. It uses the `jsonpath` library to extract the values.
:param d: The dictionary containing the JSONPath expressions to extract the values.
:param d1: The list of JSONPath expressions to extract the values.
:param j: The JSON object to extract the values from.
:param store: The dictionary to store the extracted values in.
:return: None
"""
if d1 is not None:
for i in d1:
temp = jsonpath.jsonpath(j, f'$..{i}')
if isinstance(temp, list) and len(temp) == 1:
li_as = d.get('as', None)
if li_as is not None and (not li_as or i not in li_as):
store[i] = temp[0]
elif i in li_as:
var = li_as[i]
store[var] = temp[0]
else:
raise ValueError('the result of temp is not correct')
def parse(d: dict, r: Response, store=None) -> dict[str, object]:
"""
This function extracts the values from the request and response JSON objects using the JSONPath expressions specified
in the `d` dictionary. It then stores the extracted values in the `store` dictionary.
:param d: The dictionary containing the JSONPath expressions to extract the values.
:param r: The `Response` object containing the request and response JSON objects.
:param store: The dictionary to store the extracted values in.
:return: The `store` dictionary containing the extracted values.
"""
if store is None:
store = {}
li_req = d.get('req_extract', None)
if li_req:
req_body = json.loads(r.request.body)
save(d, li_req, req_body, store)
li_res = d.get('res_extract', None)
if li_res:
res_json = r.json()
save(d, li_res, res_json, store)
return store
import os
class Template:
@staticmethod
def check_todo_file(file_path: str) -> bool:
"""
检查文件内容中是否包含 '# TODO' 字符串
Args:
file_path (str): 文件路径
Returns:
bool: 如果包含 '# TODO' 字符串则返回 True,否则返回 False
"""
with open(file_path, mode='r+', encoding='utf-8') as file:
return '# TODO' in file.read()
@staticmethod
def create_test_file(file_path: str, target_path: str) -> None:
"""
创建测试文件
Args:
file_path (str): 文件路径
target_path (str): 目标路径
"""
if Template.check_todo_file(file_path):
print(f'Message: 发现 <TODO> 标记,文件 <{file_path}> 尚未完成')
return
file_name = os.path.basename(file_path).replace('.py', '')
import_name = f'{file_name}_cfg'
test_file_path = os.path.join(target_path, f'test_{file_name}.py')
with open(test_file_path, mode='w+', encoding='utf-8') as file:
file.write(f'''import pytest
import allure
from utils.http_request import http_request
from data.{file_name} import {import_name}
from utils.parse import parse
from utils.compare import assert_http_response
from utils.format import format_string
store = {{}}
@allure.suite('{file_name}')
class Test_{file_name.capitalize()}:
@allure.sub_suite('{import_name}')
@pytest.mark.parametrize('cfg', {import_name})
def test_{file_name}(self, getCookie, cfg):
format_string(cfg, store)
allure.dynamic.title(cfg['name'])
allure.dynamic.parameter('cfg', cfg)
cfg['config']['headers']['Cookie'] = getCookie
res = http_request(cfg['config'])
allure.dynamic.description(res.text)
parse(cfg, res, store)
assert_http_response(cfg['assert'], res.json())
''')
print(f"Message: 文件 <{test_file_path}> 创建成功")
3.config目录
setting = {
"dev": "",
"test": "https://test.lan",
"prd": "",
"baseUrl": '',
"timeout": 10,
"username": 'test',
"password": '123',
"pubkey": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCv88UkwZVIU8xOCW4O+6PydsRH"
"\nLpvZRrcwUm1xDWWQdGvYCuxlRqCJCdg6g37wFxSIjyui5yIsLwdx2ogpX214z22b\nThAtkZB70Igd"
"/yDJxs8PbKxZ6It5zlZKWDBPo3IYy9+RY9i7DRYUFlMyhSgNJQAL\nr9bApW8bG8PiCRkrzwIDAQAB\n-----END PUBLIC KEY----- ",
}
#path.py
pathList = {
"pubkey": "/api/auth/key",
"doLogin": "/api/auth/login",
"menu": "/api/grant/menu",
"currentUser": "/api/auth/currentUser",
"keyword": "/api/key"
}
4.data目录
#homepage.py
from config.path import pathList
from config.config import setting
from utils.encrypto import encryption
"""
params:
name:接口名称,可以在使用allure报告时,传给allure的title
config:接口必须的字段
as:给被提取的字段起别名
assert:断言,可以同时断言多个字段
req_extract:提取请求body中的字段,可以同时提取多个
res_extract:提取响应中的字段,可以同时提取多个
description:被提取的字段,可以在其下方的接口中以{字段名}的方式引用
"""
homepage_cfg = [
{
"name": "homepage",
"config": {
"method": "get",
"url": pathList['currentUser'],
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"data": {
},
"verify": False
},
"req_extract": [],
"res_extract": ["userId"],
"as": {
},
"assert": {"code": "0000"}
},
{
"name": "menu",
"config": {
"method": "get",
"url": pathList['menu'],
"headers": {
"Content-Type": "application/json;charset=UTF-8",
},
"data": {},
"verify": False
},
"req_extract": [],
"res_extract": [],
"as": {
'code': 'login_status'
},
"assert": {"code": "0000"}
},
{
"name": "keyword",
"config": {
"method": "post",
"url": pathList['keyword'],
"headers": {
"Content-Type": "application/json;charset=UTF-8",
},
"data": {"keyword": "te", "keywordName": "t"},
"verify": False
},
"req_extract": ["keywordName"],
"res_extract": ["data"],
"as": {
},
"assert": {"code": "0000"}
},
{
"name": "delete",
"config": {
"method": "delete",
"url": pathList['keyword'] + "/{data}?name={keywordName}",
"headers": {
"Content-Type": "application/json;charset=UTF-8",
},
"data": {},
"verify": False
},
"req_extract": [],
"res_extract": [],
"as": {
},
"assert": {"code": "0000"}
}
]
#pubkey.py
from config.path import pathList
pubkey_cfg = [
{
"name": "pubkey",
"config": {
"method": "get",
"url": pathList['pubkey'],
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"data": {},
"verify": False
},
"res_extract": ['code'],
"assert": {"code": '0000'},
"as": {
"code": "id"
}
}
]
5.可以另外创建一个目录用来存储接口中body的数据,然后在data目录中引用
整体项目目录如下
case目录和log目录及里面的文件均为自动生成
生成示例:
case/test_homepage.py
import pytest
import allure
from utils.http_request import http_request
from data.homepage import homepage_cfg
from utils.parse import parse
from utils.compare import assert_http_response
from utils.format import format_string
store = {}
@allure.suite('homepage')
class Test_Homepage:
@allure.sub_suite('homepage_cfg')
@pytest.mark.parametrize('cfg', homepage_cfg)
def test_homepage(self, getCookie, cfg):
format_string(cfg, store)
allure.dynamic.title(cfg['name'])
allure.dynamic.parameter('cfg', cfg)
cfg['config']['headers']['Cookie'] = getCookie
res = http_request(cfg['config'])
allure.dynamic.description(res.text)
parse(cfg, res, store)
assert_http_response(cfg['assert'], res.json())
log/md5.json
{ "homepage.py": "4b7901efc2f903329ab9a225c4c2f052", "pubkey.py": "592a34337edd854cd999c7cae95c1e31" }