diff --git a/.gitignore b/.gitignore index 9d26d66..f5630bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # created by virtualenv automatically .idea/ .venv/ -poetry.lock \ No newline at end of file +poetry.lock +.pytest_cache/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6aeb237 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# interfaceAutoTest + +## 简介 + +... + +## 技术特点 + +... + +## 环境搭建 + +... + +## 使用方法 + +### 1,创建测试项目 + +### 2,创建测试文件 + +- test_开头 +- 文件以名字排序,并决定执行顺序 +- 文件后缀.yaml + +### 3,编写用例内容 + +**必填字段** + +| 字段名 | 用途 | 备注 | +|----------|------|------------------| +| title | 用例名称 | | +| request | 请求参数 | | +| extract | 遍历提取 | 保存在extract.yaml中 | +| validate | 接口断言 | 断言定义在CaseInfo中 | + +**选填字段** + +| 字段名 | 用途 | 备注 | +|-------------|--------|----| +| parametrize | 数据驱动测试 | | +| epic | 项目名称 | | +| feature | 模块名称 | | +| story | 功能名称 | | + +**示例** + +```yaml +... + +``` + +### 4,执行用例 + + diff --git a/TestCases/test_1_user.yaml b/TestCases/test_1_user.yaml new file mode 100644 index 0000000..a695b71 --- /dev/null +++ b/TestCases/test_1_user.yaml @@ -0,0 +1,51 @@ +feature: 特征 +story: 事件 +title: 查询用户信息 +request: + method: get + url: http://119.91.19.171:40065/answer/api/v1/connector/info + headers: + Accept-Encoding: gzip, deflate + Accept-Language: zh_CN + Content-Type: application/json + Cookie: psession=33c6c2de-7e5d-40e2-9bbc-3c637a690c3f; lang=zh-CN; 3x-ui=MTcyNjU2NDcwOHxEWDhFQVFMX2dBQUJFQUVRQUFCMV80QUFBUVp6ZEhKcGJtY01EQUFLVEU5SFNVNWZWVk5GVWhoNExYVnBMMlJoZEdGaVlYTmxMMjF2WkdWc0xsVnpaWExfZ1FNQkFRUlZjMlZ5QWYtQ0FBRUVBUUpKWkFFRUFBRUlWWE5sY201aGJXVUJEQUFCQ0ZCaGMzTjNiM0prQVF3QUFRdE1iMmRwYmxObFkzSmxkQUVNQUFBQUdQLUNGUUVDQVFkNGRXa3lNREkwQVFkNGRXa3lNREkwQUE9PXwLOhLRIDjzvQ3oI-UF-GhkMheEENkxRJ8GkAZ79eFHvg== + Host: 119.91.19.171:40065 + Origin: http://119.91.19.171:40065 + Referer: http://119.91.19.171:40065/users/login + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,like + Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0 +extract: # 提取变量 + code: + - "json" + - "$.code" + - 0 + msg: + - "json" + - "$.msg" + - 0 + + +validate: + equals: # 断言相等 + 状态码等于200: + - 200 + - ${code} + not_equals: # 断言不相等 + 状态码不等于404: + - 404 + - ${code} + contains: # 断言包含 + 包含关系: + - 404 + - ${code} + not_contains: # 断言不包含 + 不包含关系: + - 404 + - ${code} + +parametrize: # 数据驱动测试 + - [ "title","username","password","code" ] # 变量名 + - [ "测试1","user1","pass1","code1" ] # 变量值 + - [ "测试2","user2","pass2","code2" ] # 变量值 + - [ "测试3","user3","pass3","code3" ] # 变量值 + - [ "测试4","user4","pass4","code4" ] # 变量值 \ No newline at end of file diff --git a/TestCases/test_2_url.yaml b/TestCases/test_2_url.yaml new file mode 100644 index 0000000..dd0165f --- /dev/null +++ b/TestCases/test_2_url.yaml @@ -0,0 +1,15 @@ +title: 查询用户信息 + +request: + method: get + url: "https://api.kuleu.com/api/action" + headers: + user-agent: 'Mozilla / 5.0(Windows NT 10.0;Win64;x64) AppleWebKit / 537.36(KHTML, like Gecko) Chrome / 128.0.0.0Safari / 537.36' + params: + text: ${url_unquote(code)} +# data: ${code} +extract: + status_code: [ json, $.data,0 ] + +validate: + codes: 200 \ No newline at end of file diff --git a/TestCases/test_3_sql.yaml b/TestCases/test_3_sql.yaml new file mode 100644 index 0000000..c906159 --- /dev/null +++ b/TestCases/test_3_sql.yaml @@ -0,0 +1,30 @@ +title: 查询用户信息 +request: + method: get + url: http://119.91.19.171:40065/answer/api/v1/connector/info + headers: + Accept-Encoding: gzip, deflate + Accept-Language: zh_CN + Content-Type: application/json + Cookie: psession=33c6c2de-7e5d-40e2-9bbc-3c637a690c3f; lang=zh-CN; 3x-ui=MTcyNjU2NDcwOHxEWDhFQVFMX2dBQUJFQUVRQUFCMV80QUFBUVp6ZEhKcGJtY01EQUFLVEU5SFNVNWZWVk5GVWhoNExYVnBMMlJoZEdGaVlYTmxMMjF2WkdWc0xsVnpaWExfZ1FNQkFRUlZjMlZ5QWYtQ0FBRUVBUUpKWkFFRUFBRUlWWE5sY201aGJXVUJEQUFCQ0ZCaGMzTjNiM0prQVF3QUFRdE1iMmRwYmxObFkzSmxkQUVNQUFBQUdQLUNGUUVDQVFkNGRXa3lNREkwQVFkNGRXa3lNREkwQUE9PXwLOhLRIDjzvQ3oI-UF-GhkMheEENkxRJ8GkAZ79eFHvg== + Host: 119.91.19.171:40065 + Origin: http://119.91.19.171:40065 + Referer: http://119.91.19.171:40065/users/login + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,like + Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0 +extract: # 提取变量 + reason: + - "json" + - "$.reason" + - 0 + +validate: + # 断言 sql + contains: # 断言包含 + 用户在数据库中: + - "ltcs" + - ${sql(select username from user where id=1)} + not_contains: # 断言包含 + 用户不存在在数据库中: + - "ltcs" + - ${sql(select username from user where id=1)} \ No newline at end of file diff --git a/a_test_case.py b/a_test_case.py new file mode 100644 index 0000000..f680ad3 --- /dev/null +++ b/a_test_case.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: a_test_case.py +@date: 2024 2024/9/15 19:15 +@desc: +""" +from requests import Session +import requests + +session = Session() + + +def test_1(): + base_url = "https://jsonplaceholder.typicode.com" + session.params = { + 'Content-Type': 'application/json;charset=utf-8' + } + + url = f"{base_url}/users" + + payload = {} + + # response = requests.request("POST", url, headers=headers, data=payload) + response = session.get(url, json=payload) + print(response.json()[0]["username"]) + assert response.status_code == 200 + + +def test_2(): + base_url = r'https://api.kuleu.com/api/action' + params = {"text": "爱情"} + header = { + "user-agent": 'Mozilla / 5.0(Windows NT 10.0;Win64;x64) AppleWebKit / 537.36(KHTML, like Gecko) ' + 'Chrome / 128.0.0.0Safari / 537.36' + } + response = requests.get(base_url, headers=header, params=params) + # print(response.text) + print(response.json()) + print(response.request.url) + assert response.status_code == 200 diff --git a/api.py b/api.py new file mode 100644 index 0000000..fc09b3f --- /dev/null +++ b/api.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: api.py +@date: 2024 2024/9/12 22:52 +@desc: +""" +from commons.session import Session + +# session = requests.session() +session = Session("https://jsonplaceholder.typicode.com") +session.params = { + 'Content-Type': 'application/json;charset=utf-8' +} + +url = "/users" + +payload = {} + +# response = requests.request("POST", url, headers=headers, data=payload) +response = session.get(url, json=payload) +# print(response.text) +# print(response.url) +# print(response) diff --git a/commons/__init__.py b/commons/__init__.py new file mode 100644 index 0000000..d59d11c --- /dev/null +++ b/commons/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: __init__.py.py +@date: 2024 2024/9/15 21:13 +@desc: +""" diff --git a/commons/cases.py b/commons/cases.py new file mode 100644 index 0000000..04ad604 --- /dev/null +++ b/commons/cases.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: cases.py +@date: 2024 2024/9/16 9:57 +@desc: 动态生成用例 +""" +from pathlib import Path +import logging + +import allure +import pytest +from commons.files import YamlFile +from commons.models import CaseInfo +from commons.session import Session +from commons.exchange import Exchange +from commons import settings + +logger = logging.getLogger(__name__) + +session = Session(settings.base_url) + + +_case_path = Path(settings.case_path) +exchanger = Exchange(settings.exchanger) + + +@allure.epic("项目名称:answer") +class TestAPI: + ... + + @classmethod + def find_yaml_case(cls, case_path: Path = _case_path): + """ + 搜索和加载yaml文件 + :return: + """ + yaml_path_list = case_path.glob("**/test_*.yaml") # 搜索当前目录及其子目录下以test_开头yaml为后缀的文件 + for yaml_path in yaml_path_list: + logger.info(f"load file {yaml_path=}") + + file = YamlFile(yaml_path) # 自动读取yaml文件 + case_info = CaseInfo(**file) # 校验yaml格式 + + logger.debug(f"case_info={case_info.to_yaml()}") # 把case_info 转成字符串,然后记录日志 + + case_func = cls.new_case(case_info) # 从yaml格式转换为pytest格式 + print(yaml_path.name) + setattr(cls, f"{yaml_path.name}", case_func) # 把pytest格式添加到类中 + + @classmethod + def new_case(cls, case_info: CaseInfo): + ddt_data = case_info.ddt() + print(ddt_data) + ddt_title = [data.title for data in ddt_data] + + @allure.feature(case_info.feature) + @allure.story(case_info.story) + @pytest.mark.parametrize("case_info", ddt_data, ids=ddt_title) + def test_func(self, case_info: CaseInfo): + allure.dynamic.title(case_info.title) + + logger.info(f"用例开始执行:{case_info.title}".center(80, "=")) + + # 0,变量替换 + new_case_info = exchanger.replace(case_info) + logger.info(f"1,正在注入变量...") + + # 1,发送请求 + logger.info(f"2,正在请求接口...") + resp = session.request(**new_case_info.request) + + logger.info(f"3,正在提取变量...") + # 2,保存变量(接口关联) + for var_name, extract_info in new_case_info.extract.items(): + print(var_name, extract_info) + exchanger.extract(resp, var_name, *extract_info) + # 3,断言 + logger.info(f"4,正在断言...") + assert_case_info = exchanger.replace(case_info) # 为断言加载变量 + print(assert_case_info) + assert_case_info.assert_all() # 执行断言 + + logger.info(f"用例执行结束:{case_info.title}".center(80, "=")) + + return test_func + + +# TestAPI.find_yaml_case() +if __name__ == '__main__': + TestAPI.find_yaml_case() + # print(TestAPI.__dict__) diff --git a/commons/databases.py b/commons/databases.py new file mode 100644 index 0000000..fe65d82 --- /dev/null +++ b/commons/databases.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: CNWei +@Software: PyCharm +@contact: t6i888@163.com +@file: databases +@date: 2025/2/16 20:53 +@desc: +""" +import logging +import pymysql as MySQLdb + +from commons import settings + +logger = logging.getLogger(__name__) + + +class DBServer: + def __init__(self, host, port, user, password, database): + self.db = MySQLdb.connect(host=host, port=port, user=user, password=password, database=database) + self.cursor = self.db.cursor() # 创建新的会话 + + def execute_sql(self, sql): + logger.info(f"执行sql:{sql}") + self.cursor.execute(sql) # 执行sql命令 + + # res = self.cursor.fetchone() # 返回单行结果 + res = self.cursor.fetchall() # 返回多行结果 + return res + + +db = DBServer( + host=settings.db_host, # ip + port=3306, # 端口 + user='root', # 用户名 + password='mysql_hNahSe', # 密码 + database='answer' # 库名 +) + +if __name__ == '__main__': + ... + res = db.execute_sql('select username from user where id=1;') + print(res[0]) diff --git a/commons/exchange.py b/commons/exchange.py new file mode 100644 index 0000000..97da88c --- /dev/null +++ b/commons/exchange.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: exchange.py +@date: 2024 2024/9/18 21:58 +@desc: +""" +import copy +import json +import logging +import re + +import allure + +from commons.templates import Template +import jsonpath + +from commons.files import YamlFile +from commons.models import CaseInfo + +logger = logging.getLogger(__name__) + + +class Exchange: + def __init__(self, path): + self.file = YamlFile(path) + + @allure.step("提取变量") + def extract(self, resp, var_name, attr, expr: str, index): + # resp中json是方法不是属性,需要手动更改为属性 + resp = copy.deepcopy(resp) + try: + resp.json = resp.json() + except json.decoder.JSONDecodeError: + resp.json = {"msg": "is not json data"} + + data = getattr(resp, attr) + # print(data) + if expr.startswith("/"): # xpath + res = None + elif expr.startswith("$"): # jsonpath + data = dict(data) + res = jsonpath.jsonpath(data, expr) + else: # 正则 + res = re.findall(expr, str(data)) + # print(res) + if res: # 如果有数据 + value = res[index] + else: # 如果没有数据 + value = "not data" + + logger.debug(f"{var_name} = {value}") # 记录变量名和变量值 + + self.file[var_name] = value # 保存变量 + self.file.save() # 持久化存储到文件 + @allure.step("替换变量") + def replace(self, case_info: CaseInfo): + ... + + # 1,将case_info转换为字符串 + case_info_str = case_info.to_yaml() + # 2,替换字符串 + case_info_str = Template(case_info_str).render(self.file) + # 3,将字符串转换成case_info + new_case_info = case_info.by_yaml(case_info_str) + return new_case_info + + +if __name__ == '__main__': + class MockResponse: + text = '{"name":"张三","age":"18","data":[3,4,5],"aaa":null}' + + def json(self): + return json.loads(self.text) + + + mock_resp = MockResponse() + + # print(mock_resp.text) + # print(mock_resp.json()) + exchanger = Exchange(r"E:\PyP\InterfaceAutoTest\extract.yaml") + exchanger.extract(mock_resp, "name", "json", '$.name', 0) + exchanger.extract(mock_resp, "age", "json", '$.age', 0) + exchanger.extract(mock_resp, "data", "json", '$.data', 0) + exchanger.extract(mock_resp, "aaa", "json", '$.aaa', 0) + case_info = CaseInfo( + title="单元测试", + request={ + "data": + {"name": "${name}", "age": "${str(age)}", "time": "${add(1,2)}"} + }, + extract={}, + validate={} + ) + new_case_info = exchanger.replace(case_info) + print(new_case_info) diff --git a/commons/files.py b/commons/files.py new file mode 100644 index 0000000..7639efc --- /dev/null +++ b/commons/files.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: files.py +@date: 2024 2024/9/15 21:28 +@desc: 读取和保存yaml文件 +""" +import logging + +import yaml +from commons.models import CaseInfo + +logger = logging.getLogger(__name__) + +class YamlFile(dict): + def __init__(self, path): + super().__init__() + self.path = path + self.load() + + def load(self): + with open(self.path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) # 字典 + if data: + self.update(data) # 把两个字段的内容合并 + + def save(self): + with open(self.path, "w", encoding="utf-8") as f: + yaml.safe_dump( + dict(self), + stream=f, + allow_unicode=True, # allow_unicode:使用unicode编码正常显示中文 + sort_keys=False) # sort_keys:保持原有排序 + + +if __name__ == '__main__': + yaml_path = r'E:\PyP\InterfaceAutoTest\TestCases\test_1_user.yaml' + yaml_file = YamlFile(yaml_path) + # yaml_file.load() + case_info = CaseInfo(**yaml_file) + yaml_file["title"] = "查询用户信息" + yaml_file.save() diff --git a/commons/funcs.py b/commons/funcs.py new file mode 100644 index 0000000..38b7a8c --- /dev/null +++ b/commons/funcs.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: funcs.py +@date: 2024 2024/9/22 22:46 +@desc: +""" +import base64 +import logging +import time +import urllib.parse +import hashlib + +from commons.databases import db + +# from commons.files import YamlFile +from commons import settings + +logger = logging.getLogger(__name__) + + +def url_unquote(s: str) -> str: + return urllib.parse.unquote(s) + + +def time_str() -> str: + return str(time.time()) + + +def add(a, b): + return str(int(a) + int(b)) + + +def sql(s: str) -> str: + res = db.execute_sql(s) + + return res[0][0] + + +def new_id(): + # 自增,永不重复 + id_file = YamlFile(settings.id_path) + id_file["id"] += 1 + id_file.save() + + return id_file["id"] + + +def last_id() -> str: + # 不自增,只返回结果 + + id_file = YamlFile("id.yaml") + return id_file["id"] + + +def md5(content: str) -> str: + # 1,原文转为字节 + content = content.encode("utf-8") + result = hashlib.md5(content).hexdigest() + return result + + +def base64_encode(content: str) -> str: + # 1,原文转二进制 + content = content.encode("utf-8") + # 2,base64编码(二进制) + encode_value = base64.b64encode(content) + # 3,转为字符串 + encode_str = encode_value.decode("utf-8") + + return encode_str + + +def base64_decode(content: str) -> str: + # 1,原文转二进制 + content = content.encode("utf-8") + # 2,base64解码(二进制) + decode_value = base64.b64decode(content) + # 3,转为字符串 + decode_str = decode_value.decode("utf-8") + + return decode_str + + +def rsa_encode(content: str) -> str: + ... + + +def rsa_decode(content: str) -> str: + ... + + +if __name__ == '__main__': + # res = url_unquote("%E6%88%90%E5%8A%9F%E3%80%82") + # print(res) + a = "这是中文dddddd" + bb = base64_encode(a) + print(bb) + cc = base64_decode(bb) + print(cc) diff --git a/commons/models.py b/commons/models.py new file mode 100644 index 0000000..6b3dbf2 --- /dev/null +++ b/commons/models.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: models.py +@date: 2024 2024/9/15 21:14 +@desc: 声明yaml用例格式 +""" +import logging +from dataclasses import dataclass, asdict + +import allure +import yaml + +from commons.templates import Template +from commons import settings + +logger = logging.getLogger(__name__) + + +@dataclass +class CaseInfo: + title: str + request: dict + extract: dict + validate: dict + parametrize: list = "" + epic: str = settings.allure_epic + feature: str = settings.allure_feature + story: str = settings.allure_story + + def to_yaml(self) -> str: + # 序列化成yaml字符串 + yaml_str = yaml.safe_dump( + asdict(self), + allow_unicode=True, # allow_unicode:使用unicode编码正常显示中文 + sort_keys=False) + return yaml_str + + @classmethod + def by_yaml(cls, yaml_str): + # 反序列化 + obj = cls(**yaml.safe_load(yaml_str)) + return obj + + @allure.step("断言") + def assert_all(self): + if not self.validate: + return + for assert_type, assert_value in self.validate.items(): + for msg, data in assert_value.items(): + a, b = data[0], data[1] + # print(assert_type, a, b, msg) + match assert_type: + case 'equals': + logger.info(f"assert {a} == {b}, {msg}") + assert a == b, msg + case 'not_equals': + logger.info(f"assert {a} != {b}, {msg}") + assert a != b, msg + case 'contains': + logger.info(f"assert {a} in {b}, {msg}") + assert a in b, msg + case 'not_contains': + logger.info(f"assert {a} not in {b}, {msg}") + assert a not in b, msg + # case "xxxxx + + def ddt(self) -> list: # 返回一个列表,列表中应该包含N个注入了变量的caseInfo + case_list = [] + if not self.parametrize: # 没有使用数据驱动测试 + case_list.append('') + else: # 使用数据驱动测试 + args_name = self.parametrize[0] + args_value_list = self.parametrize[1:] + for args_value in args_value_list: + d = dict(zip(args_name, args_value)) + # d 就是数据驱动测试的变量,应输入到用例中 + case_info_str = self.to_yaml() # 转字符串 + case_info_str = Template(case_info_str).render(d) # 输入变量 + case_info = self.by_yaml(case_info_str) # 转成类 + + case_list.append(case_info) # 加入到返回值 + return case_list + + +if __name__ == '__main__': + with open(r'E:\PyP\InterfaceAutoTest\TestCases\test_1_user.yaml', encoding='utf-8') as f: + data = yaml.safe_load(f) + # print(data) + case_info = CaseInfo(**data) + s = case_info.to_yaml() + print(s) + new_case_info = case_info.by_yaml(s) + print(new_case_info) diff --git a/session.py b/commons/session.py similarity index 96% rename from session.py rename to commons/session.py index 62366a7..ac39670 100644 --- a/session.py +++ b/commons/session.py @@ -12,6 +12,7 @@ from urllib.parse import urljoin import logging import requests +import allure from requests import Response, PreparedRequest @@ -24,6 +25,7 @@ class Session(requests.Session): super().__init__() # 先执行父类的初始化 self.base_url = base_url # 在执行子类的初始化操作 + @allure.step("发送请求") def request(self, method, url: str, *args, **kwargs) -> Response: if not url.startswith("http"): # 自动添加baseurl @@ -39,7 +41,7 @@ class Session(requests.Session): logger.info(f"接收响应 <<<<<< 状态码 = {resp.status_code}") logger.info(f"接收响应 <<<<<< 响应头 = {resp.headers}") - logger.info(f"接收响应 <<<<<< 响应正文 = {resp.content}") + logger.info(f"接收响应 <<<<<< 响应正文 = {resp.json()}") return resp diff --git a/commons/settings.py b/commons/settings.py new file mode 100644 index 0000000..39baca6 --- /dev/null +++ b/commons/settings.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: CNWei +@Software: PyCharm +@contact: t6i888@163.com +@file: settings +@date: 2025/2/23 21:34 +@desc: +""" +base_url = 'http://127.0.0.1:8000' +case_path = r"E:\PyP\InterfaceAutoTest\TestCases" +exchanger = r"E:\PyP\InterfaceAutoTest\extract.yaml" +id_path =r"E:\PyP\InterfaceAutoTest\id.yaml" + +db_host = '119.91.19.171' # ip +db_port = 3306 # 端口 +db_user = 'root' # 用户名 +db_password = 'mysql_hNahSe' # 密码 +db_database = 'answer' # 库名 + +allure_epic: str = "项目名称:answer" +allure_feature: str = "默认特征(feature)" +allure_story: str = "默认事件(story)" + +rsa_public = "" +rsa_private = "" diff --git a/commons/templates.py b/commons/templates.py new file mode 100644 index 0000000..d7dd096 --- /dev/null +++ b/commons/templates.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: templates.py +@date: 2024 2024/9/22 22:20 +@desc: +""" +import copy +import logging +import re +import string + +logger = logging.getLogger(__name__) + + +def _str(s) -> str: + # 将数据转换为str类型。 + return f"'{s}'" + + +class Template(string.Template): + """ + 1,支持函数调用 + 2,参数也可以是变量 + """ + func_mapping = { + "str": _str, + "int": int, + "float": float, + "bool": bool + } # 内置函数有的,直接放入mapping;内置函数没有的,在funcs中定义,自动放入mapping + + call_pattern = re.compile(r"\${(?P.*?)\((?P.*?)\)}") + + def render(self, mapping: dict) -> str: + s = self.safe_substitute(mapping) # 原有方法替换变量 + s = self.safe_substitute_funcs(s, mapping) + + return s + + def safe_substitute_funcs(self, template, mapping) -> str: + """ + 解析字符串中的函数名和参数,并将函数调用结果进行替换 + :param template: 字符串 + :param mapping: 上下文,提供要使用的函数和变量 + :return: 替换后的结果 + """ + mapping = copy.deepcopy(mapping) + mapping.update(self.func_mapping) # 合并两个mapping + + def convert(mo): + func_name = mo.group("func_name") + func_args = mo.group("func_args").split(",") + func = mapping.get(func_name) # 读取指定函数 + func_args_value = [mapping.get(arg, arg) for arg in func_args] + + if func_args_value == [""]: # 处理没有参数的func + func_args_value = [] + + if not callable(func): + return mo.group() # 如果是不可调用的假函数,不进行替换 + else: + return str(func(*func_args_value)) # 否则用函数结果进行替换 + + return self.call_pattern.sub(convert, template) + + +def hot_load(): + from commons import funcs + for func_name in dir(funcs): # 遍历模块中的所有函数 + if func_name.startswith("_"): + continue + func_code = getattr(funcs, func_name) # 取到函数对象 + if callable(func_code): # 如果是一个可以调用的函数 + Template.func_mapping[func_name] = func_code # 函数放到Template中 + + +hot_load() diff --git a/extract.yaml b/extract.yaml new file mode 100644 index 0000000..21305a4 --- /dev/null +++ b/extract.yaml @@ -0,0 +1,3 @@ +code: 200 +msg: 成功。 +reason: base.success diff --git a/id.yaml b/id.yaml new file mode 100644 index 0000000..e858b93 --- /dev/null +++ b/id.yaml @@ -0,0 +1 @@ +"id":0 \ No newline at end of file diff --git a/logs/pytest.log b/logs/pytest.log new file mode 100644 index 0000000..568e68e --- /dev/null +++ b/logs/pytest.log @@ -0,0 +1,18 @@ +''02/23/2025 10:17:34 PM' [commons.cases] INFO cases.find_yaml_case:44 - load file yaml_path=WindowsPath('E:/PyP/InterfaceAutoTest/TestCases/test_1_user.yaml')' +''02/23/2025 10:17:34 PM' [commons.cases] INFO cases.find_yaml_case:44 - load file yaml_path=WindowsPath('E:/PyP/InterfaceAutoTest/TestCases/test_2_url.yaml')' +''02/23/2025 10:17:34 PM' [commons.cases] INFO cases.find_yaml_case:44 - load file yaml_path=WindowsPath('E:/PyP/InterfaceAutoTest/TestCases/test_3_sql.yaml')' +''02/23/2025 10:17:34 PM' [pytest_result_log] INFO plugin.pytest_runtest_setup:122 - ---------------Start: main.py::TestAPI::test_1_user.yaml[查询用户信息0]---------------' +''02/23/2025 10:17:34 PM' [commons.cases] INFO cases.test_func:67 - =================================用例开始执行:查询用户信息==================================' +''02/23/2025 10:17:34 PM' [commons.cases] INFO cases.test_func:71 - 1,正在注入变量...' +''02/23/2025 10:17:34 PM' [commons.cases] INFO cases.test_func:74 - 2,正在请求接口...' +''02/23/2025 10:17:34 PM' [requests.session] INFO session.send:36 - 发送请求>>>>>> 接口地址 = GET http://119.91.19.171:40065/answer/api/v1/connector/info' +''02/23/2025 10:17:34 PM' [requests.session] INFO session.send:37 - 发送请求>>>>>> 请求头 = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Accept-Language': 'zh_CN', 'Content-Type': 'application/json', 'Cookie': 'psession=33c6c2de-7e5d-40e2-9bbc-3c637a690c3f; lang=zh-CN; 3x-ui=MTcyNjU2NDcwOHxEWDhFQVFMX2dBQUJFQUVRQUFCMV80QUFBUVp6ZEhKcGJtY01EQUFLVEU5SFNVNWZWVk5GVWhoNExYVnBMMlJoZEdGaVlYTmxMMjF2WkdWc0xsVnpaWExfZ1FNQkFRUlZjMlZ5QWYtQ0FBRUVBUUpKWkFFRUFBRUlWWE5sY201aGJXVUJEQUFCQ0ZCaGMzTjNiM0prQVF3QUFRdE1iMmRwYmxObFkzSmxkQUVNQUFBQUdQLUNGUUVDQVFkNGRXa3lNREkwQVFkNGRXa3lNREkwQUE9PXwLOhLRIDjzvQ3oI-UF-GhkMheEENkxRJ8GkAZ79eFHvg==', 'Host': '119.91.19.171:40065', 'Origin': 'http://119.91.19.171:40065', 'Referer': 'http://119.91.19.171:40065/users/login'}' +''02/23/2025 10:17:34 PM' [requests.session] INFO session.send:38 - 发送请求>>>>>> 请求正文 = None ' +''02/23/2025 10:17:34 PM' [requests.session] INFO session.send:42 - 接收响应 <<<<<< 状态码 = 200' +''02/23/2025 10:17:34 PM' [requests.session] INFO session.send:43 - 接收响应 <<<<<< 响应头 = {'Content-Type': 'application/json; charset=utf-8', 'Date': 'Sun, 23 Feb 2025 14:17:34 GMT', 'Content-Length': '64'}' +''02/23/2025 10:17:34 PM' [requests.session] INFO session.send:44 - 接收响应 <<<<<< 响应正文 = {'code': 200, 'reason': 'base.success', 'msg': '成功。', 'data': []}' +''02/23/2025 10:17:34 PM' [commons.cases] INFO cases.test_func:77 - 3,正在提取变量...' +''02/23/2025 10:17:34 PM' [commons.cases] INFO cases.test_func:83 - 4,正在断言...' +''02/23/2025 10:17:34 PM' [commons.models] INFO models.assert_all:59 - assert 200 == code1, 状态码等于200' +''02/23/2025 10:17:34 PM' [pytest_result_log] ERROR plugin.pytest_result_log:190 - test status is FAILED (main.py::TestAPI::test_1_user.yaml[查询用户信息0]): AssertionError' +''02/23/2025 10:17:34 PM' [pytest_result_log] INFO plugin.pytest_runtest_teardown:128 - ----------------End: main.py::TestAPI::test_1_user.yaml[查询用户信息0]----------------' diff --git a/luffy.py b/luffy.py new file mode 100644 index 0000000..0a8e859 --- /dev/null +++ b/luffy.py @@ -0,0 +1,64 @@ +import os +import time +from logging.handlers import TimedRotatingFileHandler + + +class LufffyTimedRotatingFileHandler(TimedRotatingFileHandler): + def doRollover(self): + """ + do a rollover; in this case, a date/time stamp is appended to the filename + when the rollover happens. However, you want the file to be named for the + start of the interval, not the current time. If there is a backup count, + then we have to get a list of matching filenames, sort them and remove + the one with the oldest suffix. + """ + if self.stream: + self.stream.close() + self.stream = None + # get the time that this sequence started at and make it a TimeTuple + currentTime = int(time.time()) + dstNow = time.localtime(currentTime)[-1] + t = self.rolloverAt - self.interval + if self.utc: + timeTuple = time.gmtime(t) + else: + timeTuple = time.localtime(t) + dstThen = timeTuple[-1] + if dstNow != dstThen: + if dstNow: + addend = 3600 + else: + addend = -3600 + timeTuple = time.localtime(t + addend) + """ + dfn = self.rotation_filename(self.baseFilename + "." + + time.strftime(self.suffix, timeTuple)) + if os.path.exists(dfn): + os.remove(dfn) + self.rotate(self.baseFilename, dfn) + """ + # 多进程会导致误删日志,将上面代码重写为如下代码(判断如果不存在则重命名) + # 注意:如果改写的代码会影响其他模块则不能采用该方法 + dfn = self.rotation_filename(self.baseFilename + "." + + time.strftime(self.suffix, timeTuple)) + if not os.path.exists(dfn): + self.rotate(self.baseFilename, dfn) + + if self.backupCount > 0: + for s in self.getFilesToDelete(): + os.remove(s) + if not self.delay: + self.stream = self._open() + newRolloverAt = self.computeRollover(currentTime) + while newRolloverAt <= currentTime: + newRolloverAt = newRolloverAt + self.interval + #If DST changes and midnight or weekly rollover, adjust for this. + if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc: + dstAtRollover = time.localtime(newRolloverAt)[-1] + if dstNow != dstAtRollover: + if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour + addend = -3600 + else: # DST bows out before next rollover, so we need to add an hour + addend = 3600 + newRolloverAt += addend + self.rolloverAt = newRolloverAt \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..ae209c0 --- /dev/null +++ b/main.py @@ -0,0 +1,18 @@ +import os +import shutil +import datetime +import pytest + +from commons.cases import TestAPI + +TestAPI.find_yaml_case() # 加载yaml文件 + +if __name__ == '__main__': + now = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S') + # 1,启动框架(生成临时文件) + pytest.main([__file__, "-x", "-v"]) # -x表示有一个用例失败后面将不执行;-v表示展示用例名称;-c,配置文件所在目录:指定pytest.ini路径 + # 2,生成HTML报告 + os.system('allure generate temp -o report --clean') # java程序只能借助操作系统执行 + + # 3,备份日志 + shutil.copy2("logs/pytest.log", f"logs/pytest_{now}.log") diff --git a/pyproject.toml b/pyproject.toml index 2459fa9..a2f6a83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,12 @@ python = "^3.10" requests = "^2.32.3" +pyyaml = "^6.0.2" +pytest = "^8.3.3" +jsonpath = "^0.82.2" +pymysql = "^1.1.1" +pytest-result-log = "^1.2.2" +allure-pytest = "^2.13.5" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..74e6858 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,10 @@ +[pytest] +addopts = -q --show-capture=no + + +log_file = logs/pytest.log +log_file_level = info +log_file_format = '%(asctime)s [%(name)s] %(levelname)s %(module)s.%(funcName)s:%(lineno)d - %(message)s' +log_file_date_format = '%m/%d/%Y %I:%M:%S %p' + +disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true \ No newline at end of file diff --git a/utils/header_transition.py b/utils/header_transition.py new file mode 100644 index 0000000..760c31c --- /dev/null +++ b/utils/header_transition.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: chen wei +@Software: PyCharm +@contact: t6i888@163.com +@file: header_transition.py +@date: 2024 2024/9/17 17:34 +@desc: +""" +import re + +headerStr = ''' +Accept-Encoding: gzip, deflate +Accept-Language: zh_CN +Content-Type: application/json +Cookie: psession=33c6c2de-7e5d-40e2-9bbc-3c637a690c3f; lang=zh-CN; 3x-ui=MTcyNjU2NDcwOHxEWDhFQVFMX2dBQUJFQUVRQUFCMV80QUFBUVp6ZEhKcGJtY01EQUFLVEU5SFNVNWZWVk5GVWhoNExYVnBMMlJoZEdGaVlYTmxMMjF2WkdWc0xsVnpaWExfZ1FNQkFRUlZjMlZ5QWYtQ0FBRUVBUUpKWkFFRUFBRUlWWE5sY201aGJXVUJEQUFCQ0ZCaGMzTjNiM0prQVF3QUFRdE1iMmRwYmxObFkzSmxkQUVNQUFBQUdQLUNGUUVDQVFkNGRXa3lNREkwQVFkNGRXa3lNREkwQUE9PXwLOhLRIDjzvQ3oI-UF-GhkMheEENkxRJ8GkAZ79eFHvg== +Host: 119.91.19.171:40065 +Origin: http://119.91.19.171:40065 +Referer: http://119.91.19.171:40065/users/login +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0 +''' +ret = "" +for i in headerStr: + if i == '\n': + i = "',\n" + ret += i + +ret = re.sub(": ", ": '", ret) +print(ret[3: -3])