更改用例通过率计算方式;添加插件pytest-repeat,支持用例重复执行;增加数据处理方法:获取图片base64格式,支持用例数据传参需求;

This commit is contained in:
floraachy
2023-12-22 16:45:10 +08:00
parent cfa9fa52cf
commit 9ce3c92302
27 changed files with 413 additions and 60 deletions

29
Pipfile
View File

@@ -1,26 +1,25 @@
[[source]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
verify_ssl = false
name = "pip_conf_index_global"
verify_ssl = true
name = "pypi"
[packages]
pymysql = "==1.1.0"
loguru = "==0.7.2"
requests-toolbelt = "==1.0.0"
requests = "*"
openpyxl = "==3.1.2"
sshtunnel = "==0.4.0"
yagmail = "==0.15.293"
pyyaml = "==6.0.1"
allure-pytest = "==2.9.45"
click = "==8.1.7"
faker = "==21.0.0"
jsonpath = "0.82.2"
pytest = "==6.2.5"
pytest-rerunfailures = "==12.0"
allure-pytest = "==2.9.45"
jsonpath = "==0.82.2"
loguru = "==0.7.2"
openpyxl = "==3.1.2"
pydantic = "==2.5.2"
xpinyin = "==0.7.6"
pymysql = "==1.1.0"
pytest-rerunfailures = "==12.0"
pyyaml = "==6.0.1"
requests-toolbelt = "==1.0.0"
"ruamel.yaml" = "==0.18.5"
sshtunnel = "==0.4.0"
xpinyin = "==0.7.6"
yagmail = "==0.15.293"
pytest-repeat = "==0.9.3"
[dev-packages]

View File

@@ -27,6 +27,7 @@
* 通过session会话方式解决了登录之后cookie关联处理`
* 动态多断言: 如接口需要同时校验响应数据和sql校验支持多场景断言
* 支持单独调试用例,支持用例的重复执行
* 框架天然支持接口动态传参、关联灵活处理
* 支持测试数据分析,测试数据不符合规范有预警机制
* 支持通过用例数据动态配置pytest.mark 包括自定义标记pytest.mark.skip以及pytest,mark.usefixtures
@@ -45,23 +46,22 @@
## 三、依赖库
```
pymysql = "*"
loguru = "*"
requests-toolbelt = "*"
requests = "*"
openpyxl = "*"
sshtunnel = "*"
yagmail = "*"
pyyaml = "*"
click = "*"
faker = "*"
jsonpath = "*"
pytest = "==6.2.5"
pytest-rerunfailures = "*"
allure-pytest = "==2.9.45"
pydantic = "*"
xpinyin = "*"
"ruamel.yaml" = "*"
click = "==8.1.7"
faker = "==21.0.0"
jsonpath = "==0.82.2"
loguru = "==0.7.2"
openpyxl = "==3.1.2"
pydantic = "==2.5.2"
pymysql = "==1.1.0"
pytest-rerunfailures = "==12.0"
pyyaml = "==6.0.1"
requests-toolbelt = "==1.0.0"
"ruamel.yaml" = "==0.18.5"
sshtunnel = "==0.4.0"
xpinyin = "==0.7.6"
yagmail = "==0.15.293"
pytest-repeat = "*"
```
@@ -421,6 +421,12 @@ excel表单2名称是示例模块
顾名思义虚拟环境就是虚拟出来的一个隔离的Python环境每个项目都可以有自己的虚拟环境用pip安装各自的第三方包不同项目之间也不会存在冲突。
创建虚拟环境需要一些工具, 我们使用pipenv来创建虚拟环境和管理依赖包。
- [如何调试单个用例?](https://www.gitlink.org.cn/zone/tester/newdetail/512)
有些小伙伴在测试的时候,想要单独调试用例,但是不清楚如何调试。本文将详细讲解~
- [如何重复执行用例?](https://www.gitlink.org.cn/zone/tester/newdetail/514)
平常在做功能测试的时候经常会遇到某个模块不稳定偶然会出现一些bug对于这种问题我们会针对此用例反复执行多次最终复现出问题来。
自动化运行用例时候也会出现偶然的bug可以针对单个用例重复执行多次。
## 九、初始化项目可能遇到的问题

View File

@@ -18,6 +18,7 @@ ENV_VARS = {
"test": {
# 示例测试环境及示例测试账号
"host": "https://testforgeplus.trustie.net",
"wiki_host": "",
"client_id": "****client_id-test****", # 获取oauth_token需要的参数
"client_secret": "****client_secret-test****", # 获取oauth_token需要的参数
"green_code": "****green_code-test****", # 万能验证码
@@ -26,7 +27,7 @@ ENV_VARS = {
"nickname": "autotest",
"user_id": 106,
"super_login": "floraachy",
"super_password": "****floraachy-test****", # 运行时需要手动更改密码
"super_password": "****floraachy-test****", # 运行时需要手动更改密码
"project_url": "/autotest/auotest",
"db_info": {
"db_host": "xx.xx.xx.xx",
@@ -46,6 +47,7 @@ ENV_VARS = {
"pre": {
# 示例测试环境及示例测试账号
"host": "http://172.20.32.202:4000",
"wiki_host": "",
"client_id": "****client_id-pre****", # 获取oauth_token需要的参数
"client_secret": "****client_secret-pre****", # 获取oauth_token需要的参数
"green_code": "****green_code-pre****", # 万能验证码
@@ -54,7 +56,7 @@ ENV_VARS = {
"nickname": "autotest",
"user_id": 115,
"super_login": "floraachy",
"super_password": "****floraachy-pre****", # 运行时需要手动更改密码
"super_password": "****floraachy-pre****", # 运行时需要手动更改密码
"project_url": "/floraachy/autotest",
"db_info": {
"db_host": "xx.xx.xx.xx",
@@ -73,15 +75,16 @@ ENV_VARS = {
},
"live": {
"host": "https://www.gitlink.org.cn",
"wiki_host": "",
"client_id": "****client_id-live****", # 获取oauth_token需要的参数
"client_secret": "****client_secret-live****", # 获取oauth_token需要的参数
"green_code": "****green_code-live****", # 万能验证码
"login": "floraachy",
"password": "****floraachy-live****", # 运行时需要手动更改密码
"password": "****floraachy-live****", # 运行时需要手动更改密码
"nickname": "🌼陈银花",
"user_id": 87611,
"super_login": "chenyh",
"super_password": "****chenyh-live****", # 运行时需要手动更改密码
"super_password": "****chenyh-live****", # 运行时需要手动更改密码
"project_url": "/floraachy/auotest",
"db_info": {
"db_host": "xx.xx.xx.xx",

View File

@@ -78,7 +78,7 @@ def pytest_terminal_summary(terminalreporter, config):
f"- 异常用例个数error: {_ERROR}\n" \
f"- 重跑的用例数(--reruns的值): {_RERUN} ({reruns_value}) 个\n"
try:
_RATE = _PASSED / (_TOTAL - _SKIPPED) * 100
_RATE = (_PASSED + _XPASSED )/ (_PASSED + _FAILED + _XPASSED + _XFAILED) * 100
test_result = f"- 用例成功率: {_RATE:.2f} %\n"
logger.success(f"{test_info}{test_result}")
except ZeroDivisionError:

View File

@@ -0,0 +1,44 @@
case_common:
allure_epic: GitLink接口
allure_feature: 开源项目模块
allure_story: 组织
case_markers:
- gitlink
- projects
- gitea
- delete_organization
- usefixtures: get_oauth_token
case_info:
-
id: gitlink_projects_delete_organization_01
title: 删除组织
severity: critical
run: True
url: /api/organizations/${org_id}.json
method: DELETE
headers:
Content-Type: application/json; charset=utf-8;
Authorization: ${token_type} ${access_token}
cookies:
request_type: params
payload:
password: ${password} # 用户登录密码
files:
assert_response:
status_code: 200
assertMessage:
message: 断言响应message=success
assert_type: ==
expect_value: success
type_jsonpath: $.message
assertStatus:
message: 断言响应status=0
assert_type: ==
expect_value: 0
type_jsonpath: $.status
assert_sql:
extract:
case_dependence:
setup:
interface: gitlink_projects_new_organization_01

View File

@@ -1,4 +1,3 @@
# 公共参数
case_common:
allure_epic: GitLink接口
allure_feature: 开源项目模块
@@ -9,9 +8,7 @@ case_common:
- gitea
- new_organization
- usefixtures: get_oauth_token
- skip: 参数image还没处理好暂时略过
# 用例数据
case_info:
-
id: gitlink_projects_new_organization_01
@@ -31,7 +28,7 @@ case_info:
description: ${generate_paragraph()} # 组织描述
location: ${generate_city(full=False)} # 组织地区
repo_admin_change_team_access: true # 项目管理员可以添加或移除团队的访问权限
image: ${} # 组织图片
image: data:image/png;base64,${get_file_content('gitlinklogo3.jpg')} # 组织图片 base64编码方式
visibility: common # 组织可见性默认值common
files:
assert_response:
@@ -41,4 +38,9 @@ case_info:
assert_type: contains
expect_value: id
assert_sql:
extract:
extract:
type_jsonpath:
org_id: $.id
case_dependence:
teardown:
interface: gitlink_projects_delete_organization_01

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 2023/11/14 9:56
# @Author : floraachy
# @File : __init__.py
# @Software: PyCharm
# @Desc:

View File

@@ -0,0 +1,26 @@
case_common:
allure_epic: 开源项目
allure_feature: 项目
allure_story: 疑修Issue
case_markers:
- gitlink
- project
- issue
case_info:
-
id: project_get_issue_detail
title: 项目内获取疑修详情接口
run: True
severity: normal
url: /api/v1/${project_url}/issues/${issue_id}
method: GET
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies: ${login_cookies}
request_type: json
payload:
files:
assert_response:
status_code: 200
assert_sql:
extract:

View File

@@ -0,0 +1,26 @@
case_common:
allure_epic: 开源项目
allure_feature: 项目
allure_story: 疑修Issue
case_markers:
- gitlink
- project
- issue
case_info:
-
id: project_get_issue_journals_detail
title: 项目内获取疑修列表接口
run: True
severity: normal
url: /api/v1/${project_url}/issues/{issue_id}/journals?category=comment&page=${page}&limit=${limit}
method: GET
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies: ${login_cookies}
request_type: json
payload:
files:
assert_response:
status_code: 200
assert_sql:
extract:

View File

@@ -0,0 +1,26 @@
case_common:
allure_epic: 开源项目
allure_feature: 项目
allure_story: 疑修Issue
case_markers:
- gitlink
- project
- issue
case_info:
-
id: project_get_issue_list
title: 项目内获取疑修列表接口
run: True
severity: normal
url: /api/v1/${project_url}/issues?participant_category=all&category=all&limit=${limit}&page=${page}
method: GET
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies: ${login_cookies}
request_type: json
payload:
files:
assert_response:
status_code: 200
assert_sql:
extract:

View File

@@ -0,0 +1,29 @@
case_common:
allure_epic: 开源项目
allure_feature: 项目
allure_story: 疑修Issue
case_markers:
- gitlink
- project
- issue
case_info:
-
id: project_new_issue_journals
title: 项目内新建疑修评论接口
run: True
severity: normal
url: /api/v1/${project_url}/issues/${issue_id}/journals
method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies: ${login_cookies}
request_type: json
payload:
parent_id: 0
note:
receivers_login: [ ]
files:
assert_response:
status_code: 200
assert_sql:
extract:

View File

@@ -0,0 +1,30 @@
case_common:
allure_epic: 开源项目
allure_feature: 项目
allure_story: 疑修Issue
case_markers:
- gitlink
- project
- issue
case_info:
-
id: project_new_issue_journals
title: 项目内新建疑修评论回复接口
run: True
severity: normal
url: /api/v1/${project_url}/issues/${issue_id}/journals
method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies: ${login_cookies}
request_type: json
payload:
parent_id:
reply_id:
note:
receivers_login: []
files:
assert_response:
status_code: 200
assert_sql:
extract:

View File

@@ -0,0 +1,54 @@
case_common:
allure_epic: GitLink接口
allure_feature: 开源项目模块
allure_story: 疑修Issue
case_markers:
- gitlink
- project
- issue
- new_issue
- usefixtures: gitlink_login
- repeat(20)
case_info:
-
id: project_new_issue_001
title: 项目内新建疑修接口
run: True
severity: normal
url: /api/v1/${project_url}/issues
method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies: ${cookies}
request_type: json
payload:
subject: ${generate_company_name(lan='zh', fix='pre')}_${generate_words}
description: ${generate_paragraph}
branch_name: master
status_id: 1
priority_id: ${random.choice([1,2,3,4,5])}
milestone_id:
issue_tag_ids:
assigner_ids:
attachment_ids: # ["768f752b-2037-4b11-93e4-ccc8d72d2a54", "f5141121-6791-49b7-9334-79ebdbffeb3e"]
- ${attachment_id}
start_date: ${generate_today_date}
due_date: {generate_time_after_week}
receivers_login: []
files:
assert_response:
status_code: 200
assertId:
message: 断言接口响应数据存在id
assert_type: contains
expect_value: id
assert_sql:
extract:
type_jsonpath:
issue_index: $.project_issues_index
issue_title: $.subject
issue_desc: $.description
issue_files: $.attachments..title
case_dependence:
setup:
interface: gitlink_upload_file_01

View File

@@ -25,7 +25,6 @@ case_info:
payload:
ref: master
filepath:
type: # 可选file或者dir
files:
assert_response:
status_code: 200

View File

@@ -18,7 +18,7 @@ case_info:
url: /api/attachments.json
method: POST
headers:
cookies: ${login_cookie}
cookies: ${cookie}
cookies:
request_type: file
payload:
@@ -28,4 +28,4 @@ case_info:
assert_sql:
extract:
type_jsonpath:
file_id: $.id
attachment_id: $.id

View File

@@ -0,0 +1,51 @@
case_common:
allure_epic: GitLink接口
allure_feature: 开源项目模块
allure_story: Wiki
case_markers:
- gitlink
- projects
- gitea
- new_wiki
- usefixtures: gitlink_login
case_info:
-
id: gitlink_projects_new_wiki_01
title: 新建wiki页面
severity: critical
run: True
url: ${wiki_host}/api/wiki/createWiki
method: POST
headers:
Content-Type: application/json; charset=utf-8;
cookies: ${cookie}
cookies:
request_type: json
payload:
owner: ${repo_owner}
repo: ${repo_identifier}
projectId: ${project_id}
pageName: 2023
title: 2023
message:
content_base64: 5qyi6L+O5p2l5YiwV2lraQ==
files:
assert_response:
status_code: 200
commit_count:
message: 断言接口返回的commit_count
expect_value: 1
assert_type: ==
type_jsonpath: $.data.commit_count
assertMessage:
message: 断言接口返回的message
expect_value: 201
assert_type: ==
type_jsonpath: $.message
assert_sql:
extract:
case_dependence:
setup:
interface:
- gitlink_projects_new_project_01

View File

@@ -5,6 +5,7 @@ case_common:
allure_story: 登录接口 # 故事可以理解为场景相当于method级的标签, 往下是 title
case_markers: # pytest框架的标记 pytest.mark.
- gitlink
- smoke
- login: 登录接口
# 用例数据

View File

@@ -9,4 +9,5 @@ addopts =
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
markers =
auto: auto generate case
test_demo: demo case
test_demo: demo case
smoke:smoke

Binary file not shown.

View File

@@ -89,7 +89,8 @@ def gitlink_login():
"""
# 请求登录接口
login_api = get_api_data(os.path.join(GITLINK_DIR, "test_login.yaml"), "gitlink_login_01")
api_work_flow(login_api, GLOBAL_VARS)
res = api_work_flow(login_api, GLOBAL_VARS)
GLOBAL_VARS.update(res)
@pytest.fixture(scope="session")
@@ -100,4 +101,5 @@ def get_oauth_token():
login_oauth_token_api = get_api_data(os.path.join(GITLINK_DIR, "login_oauth_token.yaml"),
"gitlink_login_oauth_token_01")
api_work_flow(login_oauth_token_api, GLOBAL_VARS)
res = api_work_flow(login_oauth_token_api, GLOBAL_VARS)
GLOBAL_VARS.update(res)

View File

@@ -31,7 +31,7 @@ class AssertUtils:
self.assert_data = assert_data
self.response = response
if db_info:
if assert_data and db_info:
self.db_connect = MysqlServer(**db_info)
@property

View File

@@ -177,7 +177,7 @@ def gen_case_file(filename, case_template_path, case_common, case_data, target_c
os.makedirs(target_case_path, exist_ok=True)
# 获取用例数据中的标记
case_markers = case_common.get("case_markers", []) or []
logger.trace(f"从用例中拿到的标记有:{case_markers} {type(case_markers)}")
logger.debug(f"从用例中拿到的标记有:{case_markers} {type(case_markers)}")
# 先读取用例模板中每一行的内容
with open(file=case_template_path, mode="r", encoding="utf-8") as f:
case_template = f.readlines()
@@ -187,7 +187,7 @@ def gen_case_file(filename, case_template_path, case_common, case_data, target_c
# 这里是预计往 @pytest.mark.parametrize( 这一行的上面插入标记
if content.strip().startswith('@pytest.mark.parametrize('):
# 往测试用例模板中插入自定义标记
logger.trace(f"获取到的case_markers{case_markers} {type(case_markers)}")
logger.debug(f"获取到的case_markers{case_markers} {type(case_markers)}")
for case_marker in case_markers:
# 获取符合要求格式的自定义标记名称,并插入到测试模板中
marker = is_valid_marker(case_marker)
@@ -237,6 +237,12 @@ def is_valid_marker(markers):
CUSTOM_MARKERS.append(markers)
# 返回合法有效的标记名称,用于添加到测试方法中
return markers
elif "repeat" in markers:
if re.match(r'repeat\(\d+\)', markers):
return markers
else:
logger.error(f"{markers} 格式不合法, 正确格式参考repeat(2)")
return False
else:
logger.error(f"{markers} 格式不合法, 建议仅输入数字,字母,下划线组合,且不能以数字,下划线开头")
return False

View File

@@ -4,7 +4,7 @@
# @File : data_handle.py
# @Software: PyCharm
# @Desc:
import os.path
# 标准库导入
import random # 导包不能移除否则random.choice这种就不能处理了
import json
@@ -18,6 +18,8 @@ from requests.utils import dict_from_cookiejar
# 本地应用/模块导入
from utils.data_utils.faker_handle import FakerData
from utils.data_utils.eval_data_handle import eval_data
from utils.files_utils.files_handle import file_to_base64, get_files
from config.path_config import FILES_DIR
class DataHandle:
@@ -175,10 +177,34 @@ class DataHandle:
msg = (f"\nobj --> {obj}\n"
f"函数返回值 --> {res}\n"
f"函数返回值类型 --> {type(res)}\n")
logger.warning(f"\nWarn: --------处理函数方法后尝试eval({obj})失败可能原始的字符串并不是python表达式-------{msg}")
logger.warning(
f"\nWarn: --------处理函数方法后尝试eval({obj})失败可能原始的字符串并不是python表达式-------{msg}")
return obj
def get_file_content(file_name):
"""
获取文件二进制内容
:param file_name: 文件名称
:return:
"""
file_path = os.path.join(FILES_DIR, file_name)
if os.path.exists(file_path):
# 如果文件是一个真实存在的路径,则返回文件二进制内容
return file_to_base64(file_path=file_path)
else:
# 若文件不存在,则尝试以文件扩展名随机选择一个文件
logger.warning(f"图片不存在,将获取传入文件名后缀,随机取对应类型的文件, 路径:{file_path}")
file_extension = os.path.splitext(file_name)[1]
files = get_files(target=FILES_DIR, end=file_extension)
if files:
# 返回文件二进制内容
return file_to_base64(file_path=random.choice(files))
else:
logger.warning(f"找不到该文件后缀对应的同类型文件,将返回空, 传入的文件名:{file_name}")
return None
# 声明data_handle方法这样外部就可以直接import data_handle来使用了
data_handle = DataHandle().data_handle

View File

@@ -24,6 +24,7 @@ def json_extractor(obj, expr: str = '.'):
"""
try:
result = jsonpath(obj, expr)[0] if len(jsonpath(obj, expr)) == 1 else jsonpath(obj, expr)
result = data_handle(obj=result)
logger.debug(f"\n提取对象:{obj}\n"
f"提取表达式: {expr} \n"
f"提取值类型: {type(result)}\n"

View File

@@ -5,10 +5,13 @@
# @Software: PyCharm
# @Desc: 处理文件相关操作
# 第三方库导入
from loguru import logger
# 标准库导入
import os
import zipfile
import shutil
import base64
def get_files(target, start=None, end=None):
@@ -77,7 +80,7 @@ def zip_file(in_path: str, out_path: str):
"""
# 如果传入的路径是一个目录才进行压缩操作
if os.path.isdir(in_path):
print(f"目标路径:{in_path} 是一个目录,开始进行压缩......")
logger.debug(f"目标路径:{in_path} 是一个目录,开始进行压缩......")
# 写入
zip = zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED)
for path, dirnames, filenames in os.walk(in_path):
@@ -89,9 +92,9 @@ def zip_file(in_path: str, out_path: str):
path, filename), os.path.join(
fpath, filename))
zip.close()
print(f"目标路径:{in_path} 压缩完成!, 压缩文件路径:{out_path}")
logger.debug(f"目标路径:{in_path} 压缩完成!, 压缩文件路径:{out_path}")
else:
print(f"目标路径:{in_path} 不是一个目录,请检查!")
logger.debug(f"目标路径:{in_path} 不是一个目录,请检查!")
def delete_dir_file(file_path):
@@ -101,7 +104,7 @@ def delete_dir_file(file_path):
"""
paths = os.listdir(file_path)
if paths:
print(f"目标目录: {file_path} 存在文件或目录,进行删除操作")
logger.debug(f"目标目录: {file_path} 存在文件或目录,进行删除操作")
for item in paths:
path = os.path.join(file_path, item)
# 如果目标路径是一个文件使用os.remove删除
@@ -111,7 +114,7 @@ def delete_dir_file(file_path):
if os.path.isdir(path):
os.rmdir(path)
else:
print(f"目标目录: {file_path} 不存在文件或目录,不需要删除")
logger.debug(f"目标目录: {file_path} 不存在文件或目录,不需要删除")
def copy_file(src_file_path, dest_dir_path):
@@ -161,3 +164,15 @@ def get_relative_path(file_path, directory_path):
relative_path = os.path.relpath(os.path.abspath(file_path), os.path.abspath(directory_path))
# 如果相对路径中包含文件名,则去除文件名部分并返回
return os.path.dirname(relative_path)
def file_to_base64(file_path):
"""
使用Python的标准库base64来读取文件内容并将其转换为base64编码
"""
if os.path.exists(file_path):
with open(file_path, "rb") as file:
encoded_string = base64.b64encode(file.read())
return encoded_string.decode('utf-8')
else:
logger.warning(f"{file_path} 不是一个真实有效的文件路径")

View File

@@ -22,7 +22,7 @@ class BaseRequest:
Request操作封装
"""
TIMEOUT = 5
TIMEOUT = 8
session = None

View File

@@ -30,7 +30,7 @@ class RequestPreDataHandle:
"""
def __init__(self, request_data: dict, global_var: dict):
logger.trace(f"\n======================================================\n" \
logger.debug(f"\n======================================================\n" \
"-------------Start处理用例数据前--------------------\n"
f"用例标题(title): {type(request_data.get('title', None))} || {request_data.get('title', None)}\n" \
f"用例优先级(severity): {type(request_data.get('severity', None))} || {request_data.get('severity', None)}\n" \
@@ -61,7 +61,7 @@ class RequestPreDataHandle:
self.payload_handle()
self.files_handle()
self.assert_handle()
logger.trace(f"\n======================================================\n" \
logger.debug(f"\n======================================================\n" \
"-------------End处理用例数据后--------------------\n"
f"用例标题(title): {type(self.request_data.get('title', None))} || {self.request_data.get('title', None)}\n" \
f"用例优先级(severity): {type(self.request_data.get('severity', None))} || {self.request_data.get('severity', None)}\n" \