name: lorantest-scripting description: 在用户要求编写 LoranTest 测试脚本时触发。指导如何编写 pytest 测试脚本,涵盖类结构、register 用法、Logic 调用参数(check/fetch/retry/restore/context)、数据清理、断言体系、复杂场景编排等完整工程方法论。Logic 封装规则见 logic_encapsulation skill。
LoranTest 测试脚本编写完整指南
1. 核心概念
| 概念 | 说明 |
|---|---|
| Logic | 封装了 HTTP API 的 Python 函数,放在 common/ 目录下 |
| 测试脚本 | 放在 cases/ 目录下,组合调用 Logic 函数完成测试场景 |
| register | ClassDict(属性式字典),存储测试过程中产生的 ID、数据等 |
| check | 调用 Logic 时传入的业务层响应断言 |
| fetch | 调用 Logic 时传入的响应值提取指令 |
| retry | 调用 Logic 时传入的重试次数 |
| restore | 调用 Logic 时传入的自动清理标记 |
| context | 调用 Logic 时传入的上下文数据,合并到请求体中 |
1.1 关键约束
- Logic 文件中使用
from core.logic import * - 测试脚本中使用
from common.epros_logic import * - 禁止在测试脚本中使用
from core.logic import *(会导致NameError: Api)
2. 测试脚本基本结构
2.1 标准模板
# coding: utf-8
from common.ruoyi_logic import *
class TestFeatureName001(object):
def setup_method(self):
pass
@allure.title("测试标题-中文描述")
def test_feature_name_001(self):
# Precondition operation:
# [1]按照测试目的自行构造
# Procedure:
# [1]正向操作
# Expected results:
# [1]操作成功
reg = register({
"resource_id": None,
})
self.reg = reg
# 测试步骤...
def teardown_method(self):
# 数据清理...
pass
2.2 导入声明
# coding: utf-8
from common.ruoyi_logic import *
这一行导入了:
- 所有 Logic 函数(
add_process,lst_institution_nodes, ...) - 框架工具(
register,config,allure,edit_json_data,timestamp_str, ...) - 断言工具(
check_excel,check_time,check_obj) - 文件操作(
write_register,read_register)
按需追加导入(仅在特定场景需要):
import time # time.sleep() 等待异步操作
import pytest # pytest.skip() 等
from pages import * # UI 自动化测试时
from core.mysql import * # 数据库操作时
3. 类结构与命名
3.1 类命名规范
格式:Test{功能描述}{编号}
class TestNewProcessFile001(object): # 功能 + 编号
class TestDuplicateNameVerification001(object): # 场景描述 + 编号
class TestBugPatch517202404001(object): # Bug修复 + 日期 + 编号
class TestAddProcessArchitecture001(object): # 操作 + 对象 + 编号
class TestSubmitInstitution001(object): # 操作 + 对象 + 编号
规则:
- 必须以
Test开头(pytest 发现机制) - 继承
object或不写(API 测试);继承EprosUicase(UI 测试) - 编号用三位数字
001,002, ...
3.2 测试方法命名
格式:test_{功能描述}_{编号}
def test_new_process_file_001(self):
def test_duplicate_name_verification_001(self):
def test_bug_patch517_202404_001(self):
def test_information_statistics_detail_download_001(self):
3.3 测试步骤注释模板
@allure.title("测试标题")
def test_xxx_001(self):
# Precondition operation:
# [1]系统运行正常
# Procedure:
# [1]创建架构
# [2]在架构下创建流程
# [3]发布流程
# Expected results:
# [1]成功
# [2]成功
# [3]成功
# [1]创建架构
# [1]成功
add_process_architecture(...)
# [2]在架构下创建流程
# [2]成功
add_process(...)
4. register() 用法
4.1 创建 register
register() 创建一个 ClassDict(支持属性访问的字典),用于存储测试过程中产生的数据:
reg = register({
"architecture_id": None,
"process_id": None,
"diagram_id": None,
"card_data": None,
"one_activity_node": {},
})
self.reg = reg # 挂到 self 上以便 teardown 使用
4.2 属性访问
reg.architecture_id # 读取
reg.architecture_id = 123 # 写入
reg["architecture_id"] # 也可以用字典方式
4.3 fetch 写入 register
fetch 会将响应中的值写入 register:
add_process_architecture(
name="架构A",
fetch=[reg, "architecture_id", "id"] # 将响应中 $.id 的值写入 reg.architecture_id
)
# 之后可直接使用 reg.architecture_id
4.4 rebuild_dict()
当使用 edit_json_data 修改了 register 中的嵌套数据后,必须调用 rebuild_dict() 将内部的 ClassDict 转回普通 dict:
edit_json_data(json_data=reg.one_activity_node, update={...})
reg.rebuild_dict() # 必须调用,否则嵌套数据的类型可能不正确
4.5 跨脚本持久化
write_register(__file__, reg) # 将 register 写入本地 JSON 文件
read_register(__file__, reg) # 从本地 JSON 文件恢复 register
5. Logic 调用参数详解
5.1 check(业务层断言)
格式:[JSONPath, 比较符, 期望值] 或包含多个断言的二维列表
# 单个断言
add_process(name="流程A", check=["$..data..name", "eq", "流程A"])
# 多个断言
lst_process_architecture(
sid=reg.id1,
check=[
["$..sourceNode.id", "eq", reg.id1],
["name", "eq", "架构名称"]
]
)
# exist 断言(判断 JSONPath 是否匹配到数据)
lst_latest_released_architecture(
check=[f"$.data[?(@.link.name=='{var_name}')]", "exist", True]
)
# 错误码断言(预期失败场景)
add_process(
name="重复名称", parentId=reg.parent_id,
check=[
["$.code", "eq", "A0422"],
["$.data..message", "eq", "已存在名称为[重复名称]的节点"],
]
)
禁用 check:
download_file(recordId=reg.id, check=False) # 文件下载等无标准 JSON 响应的场景
5.2 check 支持的比较符
| 符号 | 别名 | 含义 |
|---|---|---|
eq |
==, equal |
相等 |
!= |
not_equal, not_eq |
不等 |
> |
lg, larger, greater |
大于 |
< |
smaller, less |
小于 |
>= |
greater_equal |
大于等于 |
<= |
less_equal |
小于等于 |
in |
— | 值在目标集合中 |
not_in |
— | 值不在目标集合中 |
include |
— | 目标包含在响应值中(子串/子集) |
not_include |
— | 目标不包含在响应值中 |
exist |
— | JSONPath 是否匹配到数据(target 为 True/False) |
len> |
length_greater, len_lg |
长度大于 |
len< |
length_smaller, len_less |
长度小于 |
len== |
length_equal, len_eq |
长度等于 |
不支持 contains,用 include 代替。
5.3 fetch(响应值提取)
格式:[register对象, "键名", "JSONPath表达式"]
# 单个 fetch(两种写法等价)
add_process(fetch=[reg, "process_id", "id"])
add_process(fetch=[reg, "process_id", "$.data.id"])
# 多个 fetch(使用二维列表)
add_process(
fetch=[
[reg, "process_id", "id"],
[reg, "supportFileDirId", "$.data.supportFileDirId"],
]
)
# fetch 完整对象
lst_process(sid=reg.process_id, fetch=[reg, "diagram_id", "$.data.id"])
# fetch 带 JSONPath 过滤的数据
lst_process(
sid=reg.process_id,
fetch=[[reg, "one_activity_node", "$.data.nodes[?(@.zIndex==5)]"]]
)
# fetch 到 register 后使用
lst_application_details(id=reg.app_id, fetch=[reg, "app_data", "$.data"])
mod_application_name(context=reg.app_data, newName="新名字")
5.4 retry(重试)
用于异步操作(发布、下载、统计等),框架会重复调用直到 check 通过或达到最大次数:
# 等待发布结果
lst_latest_released_architecture(
check=[f"$.data[?(@.link.name=='{var_name}')]", "exist", True],
retry=60 # 最多重试 60 次
)
# 等待下载任务完成
lst_download_detailed_report(
recordId=reg.recordId,
check=["$..status", "eq", "SUCCESSFUL"],
retry=60
)
使用场景:发布后查询、异步任务状态轮询、ES 索引更新等。
5.5 restore(自动清理)
调用侧传入 restore=True,框架在测试结束后自动执行 Logic 中定义的清理操作:
add_process_architecture(
name="架构A",
fetch=[reg, "architecture_id", "id"],
restore=True # 测试结束后自动删除该架构
)
add_application(
name="应用A",
fetch=[reg, "app_id", "$.data.id"],
restore=True # 测试结束后自动删除该应用
)
注意:restore=True 依赖 Logic 函数内部定义了 restore 配置。如果 Logic 没有定义 restore,传 restore=True 无效。
5.6 context(上下文填充)
将之前查询到的完整数据对象作为请求体的基础:
# 1. 先查询详情获取完整数据
lst_application_details(id=reg.app_id, fetch=[reg, "app_data", "$.data"])
# 2. 用 context 传入完整数据,只修改需要改的字段
mod_application_name(
context=reg.app_data,
newName="新名字前缀_" + var_app_name
)
原理:框架将 context 递归合并到 req_json 中,然后再用其他显式参数覆盖特定字段。
5.7 timeout(超时设置)
publish_processes_architecture(
nodeId=reg.architecture_id,
number="A1111",
description="描述",
timeout=60 # 秒
)
5.8 recv_file(接收文件)
download_detailed_report(
recordId=reg.recordId,
recv_file="report_download.xls", # 保存到 files/ 目录
check=False
)
6. 数据生成
6.1 命名策略
核心原则:测试数据名称必须包含测试方法名,便于定位数据来源。
var_architecture = "架构_test_new_process_file_001"
var_process = "流程_test_new_process_file_001"
var_institution = "制度目录test_submit_institution_001"
var_app_name = "应用_test_use_context_001"
6.2 时间戳与随机值
timestamp_str() # 字符串时间戳,如 "1709827200"
timestamp_int() # 整数时间戳
generate_random_string(12) # 12 位随机字符串
generate_12_random_string(reg, "key") # 生成 12 位随机串并写入 reg
6.3 文件名
filename = "EPROSV5.0-功能验证列表(售后).docx" # 直接使用 files/ 目录下的文件
filename = "new_institution_file_001.txt" # 简单文件名
7. 数据清理规则(关键)
7.1 清理策略选择
| 策略 | 适用场景 | 实现方式 |
|---|---|---|
restore=True |
Logic 定义了 restore 配置 | 调用时传入 restore=True,teardown 中 pass |
| 手动 teardown | Logic 未定义 restore 或需要精确控制 | teardown_method 中手动调用 rmv_* |
| 混合策略 | 部分数据用 restore,部分手动清理 | 结合使用 |
7.2 使用 restore=True 的模式
class TestFeature001(object):
def setup_method(self):
pass
def test_feature_001(self):
reg = register({"id1": None})
self.reg = reg
add_institution_directory(
name="目录A", parentId=0,
fetch=[reg, "id1", "$.data.id"],
restore=True # 自动清理
)
add_institution_file(filename="file.txt", parentId=reg.id1)
def teardown_method(self):
pass # restore=True 会自动清理
7.3 手动 teardown 的模式
def teardown_method(self):
rmv_process_architecture_completely(
ids=[self.reg.architecture_id]
)
7.4 层级数据从下往上删除
父子关系的数据必须先删子再删父:
def teardown_method(self):
# 先删流程
rmv_process_completely(ids=[self.reg.process_id])
# 再删架构
rmv_process_architecture_completely(ids=[self.reg.architecture_id])
7.5 审批数据清理
对于已提交审批的数据,必须先完成审批流程才能删除:
def teardown_method(self):
lst_user_all_task(fetch=[reg, "task_id", "$.data[0].id"])
approval_task_all_passed(task_id=reg.task_id)
rmv_process_architecture_completely(ids=[self.reg.architecture_id])
7.6 配置恢复
修改了系统配置的测试,必须在 teardown 中恢复:
def teardown_method(self):
set_process_same_directory_verification() # 恢复为默认配置
rmv_process_architecture_completely(ids=[self.reg.architecture_id])
7.7 预期失败的操作不需要清理
如果操作预期会失败(如重复名称校验),不会产生数据,不需要 fetch 和清理:
add_process(
name="重复名称", parentId=reg.parent_id,
check=[["$.code", "eq", "A0422"]] # 预期失败,无需 fetch
)
8. setup_method 模式
8.1 简单测试(无前置条件)
def setup_method(self):
pass
8.2 复杂前置条件
def setup_method(self):
reg = register({
"architecture_id": None,
"process_id": None,
"diagram_id": None,
"one_activity_node": {},
})
self.reg = reg
# 开启必要配置
set_overview_statistics_of_view_role_of_system_admin(restore=True)
# 创建基础数据
var_architecture = "架构_test_name_001"
add_process_architecture(
name=var_architecture,
fetch=[[reg, "architecture_id", "id"]],
restore=True
)
var_process = "流程_test_name_001"
add_process(
name=var_process, parentId=reg.architecture_id,
fetch=[[reg, "process_id", "id"], [reg, "supportFileDirId", "$.data.supportFileDirId"]]
)
8.3 setup 中还是 test 中初始化 register
| 模式 | 适用场景 |
|---|---|
| setup_method 中初始化 | 前置条件复杂,需要提前构建大量数据 |
| test 方法中初始化 | 简单测试,前置条件与测试步骤紧密相关 |
9. 响应验证体系
9.1 JSONPath 基础
# 简单路径
"$.code" # 响应体的 code 字段
"$.data.id" # 嵌套路径
"$.data.sourceNode.isPublished" # 深层嵌套
# 递归查找
"$..data..name" # 递归查找 data 下的 name
"$..sourceNode.id" # 递归查找 sourceNode 下的 id
# 过滤表达式
"$.data[?(@.name=='架构A')]" # 按条件过滤
"$.data[?(@.link.name=='流程A')]" # 嵌套对象过滤
"$.data.nodes[?(@.type=='activity')]" # 按类型过滤
"$.data.nodes[?(@.zIndex==5)]" # 按数值过滤
# 索引访问
"$.data[0].id" # 第一个元素
"$.[0].name" # 根数组第一个元素
9.2 JSONPath 中特殊字符处理
@、#、' 等字符会破坏 JSONPath filter 表达式。处理方式:
# 方案 1:用 API 参数过滤后取索引
lst_institution_nodes(name=special_name, fetch=[reg, "id", "$.data[0].id"])
# 方案 2:使用 f-string 构造(当名称不含特殊字符时)
check=[f"$.data[?(@.name=='{var_name}')]", "exist", True]
9.3 exist 断言
验证某个 JSONPath 是否匹配到数据:
lst_latest_released_architecture(
check=[f"$.data[?(@.link.name=='{var_architecture}')]", "exist", True],
retry=60
)
9.4 负向断言
测试预期失败的场景,必须用具体的错误信息断言:
# ✅ 推荐 — 精确验证错误码和错误信息
add_process(
name="重复名称", parentId=reg.parent_id,
check=[
["$.code", "eq", "A0422"],
["$.data..message", "eq", "已存在名称为[重复名称]的节点"],
]
)
# ❌ 不推荐 — 无法区分是哪种错误
add_process(name="重复名称", check=[["$.code", "!=", 200]])
9.5 Excel 断言
check_excel({
"file": "report.xls", # files/ 目录下的文件名
"sheet": "活动信息化详情报表", # sheet 名称(默认 Sheet1)
"condition": [ # 过滤条件
{"column_name": "系统名称", "column_value": "应用A"},
{"column_name": "所属流程", "column_value": "流程B"},
],
"assert_type": "equal", # 断言类型
"target": 1 # 期望匹配行数
})
condition 简写格式:
"condition": [
["人员名称", "张三"], # [列名, 列值]
["人员账号", 88880001],
]
assert_type 选项:
| 类型 | 含义 |
|---|---|
equal |
等于 |
<= |
左边是右边的子集 |
=> |
右边是左边的子集 |
target 格式:
- 整数:期望匹配行数,如
1,2 - 列表:期望匹配行的列值断言:
"target": [
{"column_name": "功能角色", "column_value": "销售工程师"},
{"column_name": "年龄", "column_value": 25},
]
9.6 时间断言
check_time(
exp_timestamp=reg.some_time,
check_type="==", # ==, earlier, later
target_timestamp=reg.other_time
)
9.7 对象断言
check_obj(obj=some_list, attr="length", assert_type="eq", target=5)
10. 复杂场景编排
10.1 多步骤流程测试
@allure.title("发布流程")
def test_process_publish_001(self):
reg = register({
"architecture_id": None,
"process_id": None,
"template_id": None,
"card_data": None,
})
self.reg = reg
# 步骤 1: 创建架构
add_process_architecture(
name="架构A", number="A", nameEn="A", code="A",
fetch=[[reg, "architecture_id", "id"]],
)
# 步骤 2: 创建流程
add_process(
name="流程A", parentId=reg.architecture_id,
fetch=[[reg, "process_id", "id"]],
)
# 步骤 3: 设置审批模板
set_architecture_task(
relatedId=reg.architecture_id,
templateId=config.approve_template.process.four_reviewer_release_by_drafter.id
)
# 步骤 4: 设置架构卡片
lst_architecture_card(processId=reg.architecture_id, fetch=[reg, "card_data", "$.data.data"])
edit_json_data(json_data=reg.card_data, update={"description": "流程描述"})
set_architecture_card(data=reg.card_data, processId=reg.architecture_id)
# 步骤 5: 发布
publish_processes_architecture(
nodeId=reg.architecture_id, number="V1", description="首次发布", timeout=60
)
# 步骤 6: 验证发布状态
lst_process(sid=reg.process_id, check=["$.data.sourceNode.isPublished", "eq", True])
10.2 数据依赖链
fetch 的结果可以作为后续步骤的输入:
# 创建架构 → 获取 ID → 在架构下创建流程 → 获取画布 ID → 修改画布
add_process_architecture(name="架构", fetch=[reg, "arch_id", "id"])
add_process(name="流程", parentId=reg.arch_id, fetch=[reg, "process_id", "id"])
lst_process(sid=reg.process_id, fetch=[reg, "diagram_id", "$.data.id"])
add_default_diagram_for_process_2_role_2_activity(diagramId=reg.diagram_id)
10.3 edit_json_data 修改复杂数据
从响应中 fetch 出复杂 JSON 对象,修改后作为参数传回:
# 1. 获取活动节点数据
lst_process(
sid=reg.process_id,
fetch=[[reg, "one_activity_node", "$.data.nodes[?(@.zIndex==5)]"]]
)
# 2. 修改节点数据
edit_json_data(
json_data=reg.one_activity_node,
update={
"data": {
"executionRole": {
"roleId": "2XfVV5q3IPik",
"roleText": "角色"
},
"itSystems": [
{
"itSystem": reg.it_system_node,
"launchTime": "",
"transactionCode": "",
"url": ""
}
],
},
}
)
# 3. 必须 rebuild_dict
reg.rebuild_dict()
# 4. 将修改后的数据保存
mod_diagram_of_process(
version=1,
diagramId=reg.diagram_id,
node=reg.one_activity_node,
)
10.4 异步操作等待
对于发布、下载等异步操作,使用 retry 轮询直到完成:
# 提交下载任务
submit_download_detailed_report(
itSystemIds=[reg.app_id],
fetch=[reg, "recordId", "$.recordId"]
)
# 轮询等待下载完成
lst_download_detailed_report(
recordId=reg.recordId,
check=["$..status", "eq", "SUCCESSFUL"],
retry=60
)
# 下载文件
download_detailed_report(
recordId=reg.recordId,
recv_file="report.xls",
check=False
)
必要时也可以用 time.sleep() 等待:
mod_diagram_of_process(...)
time.sleep(3) # 等待画布数据同步
lst_process(...)
10.5 config 全局配置使用
# 使用预配置的审批模板 ID
config.approve_template.architecture.four_reviewer_release_by_drafter.id
config.approve_template.process.four_reviewer_release_by_drafter.id
config.approve_template.institution.four_reviewer_release_by_drafter.id
# 使用预配置的用户信息
config.user.designer_1.id
10.6 脚本调试闭环
脚本编写完成后,须执行调试直至通过。具体流程如下:
- 执行测试脚本;
- 若执行失败,根据日志输出定位失败原因;
- 针对失败原因修改脚本;
- 重复步骤 1-3,直至脚本执行成功。
10.7 待排查问题记录
编写过程中,若遇到当前无法解决的问题,须将问题详情记录至根目录下的 to_check_record.md 文件中,以便后续人工跟进排查。
10.8 业务合理性审查与 BUG 记录
编写脚本过程中,须对被测接口的业务逻辑合理性(包括但不限于参数校验、状态流转、权限控制等)进行审查。若发现疑似 BUG(即接口实际行为与预期业务规则不符),须将 BUG 信息追加记录至根目录下的 to_check_bug.md 文件中。
10.9 测试数据独立性要求
每个测试脚本所依赖的数据必须由脚本自身创建,禁止直接引用系统中已有的存量数据。例如,若测试场景需要对某用户进行授权操作,须在脚本中先创建一个专属于该脚本的用户,而非复用已有用户。创建数据时,命名应优先包含当前测试脚本的 case_id 字符串,以便在数据残留时能够快速定位归属脚本并进行清理。
10.10 引用数据同步性验证
适用场景:当实体 A 被实体 B 通过 ID 引用时(例如用户引用了岗位、角色引用了菜单、部门引用了上级部门),修改 A 的名称/编码等展示性字段后,需验证查询 B 时引用的 A 的字段是否已更新为最新值。
BUG 本质:系统在 B 中冗余存储(缓存)了 A 的名称,修改 A 后未同步更新 B 中的冗余字段,导致 B 展示 A 的陈旧数据。
编写模式:
@allure.title("修改岗位名称后-用户详情中引用的岗位名称应同步更新")
def test_ref_sync_after_rename(self):
reg = register({
"position_id": None,
"user_id": None,
})
self.reg = reg
ts = int(time.time())
original_name = f"岗位原名_{ts}"
new_name = f"岗位新名_{ts}"
# 步骤 1: 创建被引用实体 A(岗位)
add_position(
positionName=original_name,
positionCode=f"REF_SYNC_{ts}",
postSort=1,
fetch=[[reg, "position_id", "$.postId"]],
)
# 步骤 2: 创建引用方 B(用户),引用 A
add_user(
userName=f"ref_sync_user_{ts}",
postIds=[reg.position_id],
# ... 其他必填字段
fetch=[[reg, "user_id", "$.userId"]],
)
# 步骤 3: 修改 A 的名称
mod_position(
positionId=reg.position_id,
positionName=new_name,
positionCode=f"REF_SYNC_{ts}",
postSort=1,
check=[["$.code", "eq", 200]],
)
# 步骤 4: 查询 B 的详情,断言引用的 A 字段已更新为最新值
lst_user_detail(
userId=reg.user_id,
check=[["$.data.posts[0].postName", "eq", new_name]],
)
关键要点:
- 先建 A → 建 B(引用 A)→ 改 A → 查 B:这是固定的四步验证流程。
- 断言目标是 B 中引用 A 的展示字段:如
$.data.posts[0].postName、$.data.roles[0].roleName、$.data.dept.deptName等,而非 A 自身的详情。 - 准备两个名称:
original_name(创建时用)和new_name(修改后用),断言时期望 B 中展示new_name。 - 常见引用关系(若依系统为例):
| 被引用实体 A | 引用方 B | B 中展示 A 的字段路径(示例) |
|---|---|---|
| 岗位 | 用户 | $.data.posts[*].postName |
| 角色 | 用户 | $.data.roles[*].roleName |
| 部门 | 用户 | $.data.dept.deptName |
| 字典类型 | 字典数据 | $.data.dictType(类型名称) |
| 上级部门 | 子部门 | $.data.parentName |
- teardown 中先删 B 再删 A:因为 B 引用了 A,须先解除引用关系后再删被引用方。
def teardown_method(self):
if getattr(self.reg, "user_id", None):
rmv_user(userIds=[self.reg.user_id])
if getattr(self.reg, "position_id", None):
rmv_position(positionIds=[self.reg.position_id])
11. Allure 报告装饰器
@allure.feature("流程体系") # 功能模块(可选)
@allure.story("架构管理") # 用户故事(可选)
class TestXxx(object):
@allure.title("新建流程架构") # 测试标题(推荐必填)
def test_xxx_001(self):
...
实践规则:
@allure.title()使用中文简述测试目的,每个测试方法都应有@allure.feature()/@allure.story()按需使用
12. 文件位置与目录结构
12.1 项目根目录
LoranTest/
├── cases/ # 测试脚本(按项目/测试类型/业务模块组织)
├── common/ # Logic 函数封装(按业务模块组织)
├── config/ # 配置文件(environment.yaml, database.yaml, user.yaml)
├── core/ # 框架核心(logic引擎、工具、钩子)
├── pages/ # Page Object(Web UI 自动化)
├── files/ # 测试用静态文件
├── logs/ # 测试运行日志
├── utils/ # 通用工具
├── docs/ # 文档
├── requirements.txt
├── run_api_case.py # pytest 入口脚本
└── readme.md
12.2 测试脚本目录(cases/)
cases/
└── test_ruoyi/ # 项目名
├── test_api/ # API 测试
│ └── test_system_management/ # 一级业务模块
│ ├── test_dept_management/ # 二级业务模块:部门管理
│ │ ├── test_dept_basic_crud.py
│ │ ├── test_dept_boundary.py
│ │ ├── test_dept_validation.py
│ │ └── ...
│ ├── test_dict_management/ # 二级业务模块:字典管理
│ ├── test_post_management/ # 二级业务模块:岗位管理
│ ├── test_role_management/ # 二级业务模块:角色管理
│ └── test_user_management/ # 二级业务模块:用户管理
├── test_example/ # 框架用法示例
│ ├── test_api_with_restore_example_001.py
│ └── ...
├── test_utils/ # 测试辅助工具
│ └── test_data_generator.py
└── test_web_ui/ # Web UI 测试
├── test_common/
│ └── test_login_ruoyi.py
├── test_add_user_ui.py
└── ...
12.3 cases 与 common 的对应关系
cases和common的目录没有直接对应的关系, 因为不同的脚本中有可能会由不同的模块的logic来组成
12.4 脚本组织原则
- 具体目录按业务模块层级组织
- 每一个业务模块所对应的目录命名都需要以
test_开头,这样可以方便在任意一级目录执行pytest来运行下面的所有脚本 cases/目录下不需要__init__.py(pytest 自动发现机制)common/目录下每个包都需要__init__.py(Python 包导入机制)- 测试脚本文件一律以
test_开头(pytest 发现规则) - 同一个业务模块下,按测试维度拆分为多个文件(如
test_dept_basic_crud.py、test_dept_boundary.py、test_dept_validation.py等)
12.5 测试脚本文件命名
格式:test_{模块缩写}_{测试维度}.py
test_dept_basic_crud.py # 部门-基础增删改查
test_dept_boundary.py # 部门-边界条件
test_dept_field_validation.py # 部门-字段校验
test_role_admin_protection.py # 角色-管理员保护
test_role_data_scope.py # 角色-数据权限范围
test_post_pagination.py # 岗位-分页
13. 常见陷阱速查
| 现象 | 原因 | 修复 |
|---|---|---|
NameError: Api |
测试脚本用了 from core.logic import * |
改为 from common.epros_logic import * |
TypeError: 'bool' object is not subscriptable |
fetch 的 JSONPath 匹配不到数据 | 检查响应结构;检查名称是否含特殊字符 |
contains 比较符报错 |
不支持该比较符 | 用 include 代替 |
| teardown 删除报错"存在下级" | 删除顺序错误 | 先删子再删父 |
RuntimeError: No active exception to reraise |
check 的 JSONPath 在响应中不存在 | 确认响应结构,用 exist 而非 eq None |
| 测试后数据残留 | 未清理或 restore 未生效 | 检查 restore 配置或添加手动 teardown |
| fetch 值为 None | JSONPath 表达式错误或响应结构变化 | 打印响应确认结构,修正 JSONPath |
reg.rebuild_dict() 遗漏 |
edit_json_data 后未重建 |
在 edit_json_data 后立即调用 reg.rebuild_dict() |
| retry 超时 | 异步操作耗时超出重试次数 | 增大 retry 值或检查操作是否有异常 |
| config 属性找不到 | 配置文件中未定义 | 检查 config/ 目录下的配置文件 |
14. 编写测试脚本检查清单
编写每个测试脚本时,逐项检查:
- 文件头
# coding: utf-8和from common.epros_logic import * - 类名以
Test开头,方法名以test_开头 - 有
setup_method和teardown_method - register 中声明了所有需要存储的 ID/数据
- register 挂到了
self.reg(teardown 需要使用时) - 每个创建操作都有对应的 fetch(获取 ID)或 restore=True
- teardown 中正确清理了所有测试数据
- 层级数据从下往上删除
- 使用了
@allure.title()描述测试目的 - 测试数据名称包含测试方法名(便于追溯)
- 预期失败的场景使用了精确的错误信息断言
- 异步操作使用了 retry 或 time.sleep
-
edit_json_data后调用了reg.rebuild_dict()