DSL内自定义关键字
DSL内自定义关键字是在测试文件中直接定义的可复用测试步骤,使用简洁的DSL语法,让测试人员无需编程基础就能创建强大的自定义功能。
什么是DSL内自定义关键字
DSL内自定义关键字是在.dsl测试文件中使用function关键字定义的可重用测试步骤,它们具有以下特点:
- 🎯 零编程门槛 - 使用自然语言风格的DSL语法
- 🔄 即时可用 - 在同一文件中定义和使用
- 📖 高可读性 - 测试逻辑清晰直观
- 🛠️ 灵活组合 - 可以调用内置关键字和其他自定义关键字
基本语法
定义语法
python
function 关键字名称 (参数1, 参数2="默认值") do
# 关键字实现逻辑
[内置关键字], 参数: ${参数1}
return ${结果}
end调用语法
python
# 基本调用
结果 = [关键字名称], 参数1: 值1, 参数2: 值2
# 使用默认值
结果 = [关键字名称], 参数1: 值1 # 参数2使用默认值快速入门示例
简单的问候关键字
python
@name: "DSL自定义关键字入门"
# 定义问候关键字
function 问候 (姓名, 前缀="你好") do
消息 = "${前缀}, ${姓名}!"
[打印], 内容: ${消息}
return ${消息}
end
# 使用自定义关键字
结果1 = [问候], 姓名: "张三"
结果2 = [问候], 姓名: "李四", 前缀: "欢迎"
[数据比较], 实际值: ${结果1}, 预期值: "你好, 张三!"
[数据比较], 实际值: ${结果2}, 预期值: "欢迎, 李四!"计算器关键字
python
@name: "计算器关键字示例"
function 计算器 (操作, 数字1, 数字2=0, 精度=2) do
if ${操作} == "加法" do
结果 = ${数字1} + ${数字2}
elif ${操作} == "减法" do
结果 = ${数字1} - ${数字2}
elif ${操作} == "乘法" do
结果 = ${数字1} * ${数字2}
elif ${操作} == "除法" do
if ${数字2} == 0 do
[打印], 内容: "错误:除数不能为0"
return "错误"
end
结果 = ${数字1} / ${数字2}
else
[打印], 内容: "不支持的操作: ${操作}"
return "不支持"
end
# 格式化结果精度
格式化结果 = round(${结果}, ${精度})
[打印], 内容: "${数字1} ${操作} ${数字2} = ${格式化结果}"
return ${格式化结果}
end
# 测试计算器功能
加法结果 = [计算器], 操作: "加法", 数字1: 10, 数字2: 5
除法结果 = [计算器], 操作: "除法", 数字1: 10, 数字2: 3, 精度: 4
[数据比较], 实际值: ${加法结果}, 预期值: 15
[数据比较], 实际值: ${除法结果}, 预期值: 3.3333参数系统详解
参数类型
- 必需参数 - 调用时必须提供
- 可选参数 - 有默认值,可以不提供
python
function 示例关键字 (必需参数, 可选参数1="默认值1", 可选参数2=100) do
[打印], 内容: "必需: ${必需参数}"
[打印], 内容: "可选1: ${可选参数1}"
[打印], 内容: "可选2: ${可选参数2}"
end
# 各种调用方式
[示例关键字], 必需参数: "测试"
[示例关键字], 必需参数: "测试", 可选参数1: "自定义值"
[示例关键字], 必需参数: "测试", 可选参数2: 200
[示例关键字], 必需参数: "测试", 可选参数1: "自定义", 可选参数2: 300参数验证
python
function 验证邮箱 (邮箱地址) do
# 检查是否为空
if "${邮箱地址}" == "" do
[打印], 内容: "邮箱地址不能为空"
return False
end
# 检查格式
if "@" not in "${邮箱地址}" do
[打印], 内容: "邮箱格式无效:缺少@符号"
return False
end
if "." not in "${邮箱地址}" do
[打印], 内容: "邮箱格式无效:缺少域名"
return False
end
[打印], 内容: "邮箱验证通过: ${邮箱地址}"
return True
end
# 测试邮箱验证
[验证邮箱], 邮箱地址: "test@example.com" # 应该通过
[验证邮箱], 邮箱地址: "invalid-email" # 应该失败返回值和变量作用域
返回值使用
python
function 生成用户信息 (姓名, 年龄=25, 部门="技术部") do
用户ID = "USER_" + ${姓名} + "_" + str(${年龄})
用户信息 = {
"id": ${用户ID},
"name": ${姓名},
"age": ${年龄},
"department": ${部门},
"created_at": "2024-01-01"
}
[打印], 内容: "生成用户信息: ${用户信息["name"]}"
return ${用户信息}
end
# 使用返回值
用户1 = [生成用户信息], 姓名: "张三", 年龄: 30
用户2 = [生成用户信息], 姓名: "李四", 部门: "产品部"
[打印], 内容: "用户1 ID: ${用户1["id"]}"
[打印], 内容: "用户2 部门: ${用户2["department"]}"变量作用域
python
# 全局变量
计数器 = 0
function 增加计数 (增量=1) do
# 局部变量
旧值 = ${计数器}
新值 = ${旧值} + ${增量}
# 更新全局变量(通过赋值)
计数器 = ${新值}
[打印], 内容: "计数器从 ${旧值} 增加到 ${新值}"
return ${新值}
end
# 测试作用域
[增加计数], 增量: 5
[增加计数], 增量: 3
[打印], 内容: "最终计数器值: ${计数器}"流程控制
条件分支
python
function 评估成绩 (分数, 科目="数学") do
if ${分数} >= 90 do
等级 = "优秀"
评语 = "表现出色"
elif ${分数} >= 80 do
等级 = "良好"
评语 = "表现不错"
elif ${分数} >= 70 do
等级 = "中等"
评语 = "需要努力"
elif ${分数} >= 60 do
等级 = "及格"
评语 = "刚好通过"
else
等级 = "不及格"
评语 = "需要重修"
end
结果 = {
"subject": ${科目},
"score": ${分数},
"grade": ${等级},
"comment": ${评语}
}
[打印], 内容: "${科目}成绩: ${分数}分 - ${等级} (${评语})"
return ${结果}
end
# 测试成绩评估
成绩1 = [评估成绩], 分数: 95, 科目: "语文"
成绩2 = [评估成绩], 分数: 75, 科目: "英语"
成绩3 = [评估成绩], 分数: 55, 科目: "物理"循环处理
python
function 批量处理数据 (数据列表, 操作="打印") do
处理结果 = []
成功计数 = 0
失败计数 = 0
for item in ${数据列表} do
[打印], 内容: "正在处理: ${item}"
if ${操作} == "打印" do
处理状态 = "成功"
处理值 = "已打印: ${item}"
elif ${操作} == "转大写" do
处理状态 = "成功"
处理值 = [字符串操作], 操作: "upper", 字符串: ${item}
elif ${操作} == "计算长度" do
处理状态 = "成功"
项目字符串 = [转换为字符串], 值: ${item}
处理值 = [获取长度], 对象: ${项目字符串}
else
处理状态 = "失败"
处理值 = "不支持的操作: ${操作}"
end
if ${处理状态} == "成功" do
成功计数 = ${成功计数} + 1
else
失败计数 = ${失败计数} + 1
end
结果项 = {
"input": ${item},
"output": ${处理值},
"status": ${处理状态}
}
处理结果 = 处理结果 + [${结果项}]
end
数据列表长度 = [获取长度], 对象: ${数据列表}
总结 = {
"total": ${数据列表长度},
"success": ${成功计数},
"failed": ${失败计数},
"results": ${处理结果}
}
[打印], 内容: "批量处理完成 - 总数: ${总结["total"]}, 成功: ${成功计数}, 失败: ${失败计数}"
return ${总结}
end
# 测试批量处理
测试数据 = ["hello", "world", "pytest", "dsl"]
结果1 = [批量处理数据], 数据列表: ${测试数据}, 操作: "转大写"
结果2 = [批量处理数据], 数据列表: ${测试数据}, 操作: "计算长度"实际应用场景
场景1:用户注册流程
python
@name: "用户注册流程测试"
function 验证用户输入 (用户名, 密码, 邮箱) do
错误列表 = []
# 验证用户名
用户名长度 = [获取长度], 对象: ${用户名}
if ${用户名长度} < 3 do
错误列表 = 错误列表 + ["用户名长度不能少于3位"]
end
# 验证密码
密码长度 = [获取长度], 对象: ${密码}
if ${密码长度} < 6 do
错误列表 = 错误列表 + ["密码长度不能少于6位"]
end
# 验证邮箱
if "@" not in ${邮箱} do
错误列表 = 错误列表 + ["邮箱格式无效"]
end
错误列表长度 = [获取长度], 对象: ${错误列表}
if ${错误列表长度} == 0 do
[打印], 内容: "用户输入验证通过"
return {"valid": True, "errors": []}
else
[打印], 内容: "用户输入验证失败: ${错误列表}"
return {"valid": False, "errors": ${错误列表}}
end
end
function 注册用户 (用户名, 密码, 邮箱) do
# 验证输入
验证结果 = [验证用户输入], 用户名: ${用户名}, 密码: ${密码}, 邮箱: ${邮箱}
if not ${验证结果["valid"]} do
return {"success": False, "message": "输入验证失败", "errors": ${验证结果["errors"]}}
end
# 模拟注册过程
[打印], 内容: "正在注册用户: ${用户名}"
用户ID = "USER_" + ${用户名} + "_" + str(time.time())
用户信息 = {
"id": ${用户ID},
"username": ${用户名},
"email": ${邮箱},
"status": "active",
"created_at": "2024-01-01"
}
[打印], 内容: "用户注册成功: ${用户ID}"
return {"success": True, "user": ${用户信息}}
end
# 测试用户注册
注册结果1 = [注册用户], 用户名: "testuser", 密码: "password123", 邮箱: "test@example.com"
注册结果2 = [注册用户], 用户名: "ab", 密码: "123", 邮箱: "invalid-email"
[断言], 条件: "${注册结果1["success"]} == True"
[断言], 条件: "${注册结果2["success"]} == False"场景2:数据处理和转换
python
@name: "数据处理示例"
function 清理文本数据 (文本, 选项) do
处理后文本 = ${文本}
# 去除首尾空格
if ${选项.trim} do
处理后文本 = [字符串操作], 操作: "strip", 字符串: ${处理后文本}
end
# 转小写
if ${选项.lowercase} do
处理后文本 = [字符串操作], 操作: "lower", 字符串: ${处理后文本}
end
[打印], 内容: "文本处理: '${文本}' -> '${处理后文本}'"
return ${处理后文本}
end
function 批量清理数据 (数据列表, 清理选项) do
清理结果 = []
for item in ${数据列表} do
清理后 = [清理文本数据], 文本: ${item}, 选项: ${清理选项}
清理结果 = 清理结果 + [${清理后}]
end
数据列表长度 = [获取长度], 对象: ${数据列表}
[打印], 内容: "批量清理完成,处理了 ${数据列表长度} 条数据"
return ${清理结果}
end
# 测试数据清理
原始数据 = [
" Hello World! ",
" TEST@#$%Data ",
" Another Example!!! "
]
# 不同的清理选项
选项1 = {"trim": True, "lowercase": True}
选项2 = {"trim": True, "lowercase": True, "remove_special": True}
结果1 = [批量清理数据], 数据列表: ${原始数据}, 清理选项: ${选项1}
结果2 = [批量清理数据], 数据列表: ${原始数据}, 清理选项: ${选项2}
[打印], 内容: "清理结果1: ${结果1}"
[打印], 内容: "清理结果2: ${结果2}"错误处理和调试
错误处理模式
python
function 安全执行 (操作类型, 数据, 重试次数=3) do
当前尝试 = 1
for i in range(1, ${重试次数} + 1) do
当前尝试 = ${i}
[打印], 内容: "第 ${当前尝试} 次尝试执行: ${操作类型}"
# 模拟操作执行
if ${操作类型} == "保存文件" do
# 模拟成功/失败(基于尝试次数)
if ${当前尝试} >= 2 do
[打印], 内容: "文件保存成功"
return {"status": "success", "attempt": ${当前尝试}, "data": "文件已保存"}
else
[打印], 内容: "文件保存失败,准备重试..."
end
elif ${操作类型} == "网络请求" do
if ${当前尝试} >= 3 do
[打印], 内容: "网络请求成功"
return {"status": "success", "attempt": ${当前尝试}, "data": "请求响应数据"}
else
[打印], 内容: "网络请求失败,准备重试..."
end
else
[打印], 内容: "不支持的操作类型: ${操作类型}"
return {"status": "error", "message": "不支持的操作类型"}
end
# 重试间隔
if ${当前尝试} < ${重试次数} do
[等待], 秒数: 1
end
end
[打印], 内容: "所有重试都失败了"
return {"status": "failed", "attempts": ${重试次数}}
end
# 测试错误处理
结果1 = [安全执行], 操作类型: "保存文件", 数据: "测试数据"
结果2 = [安全执行], 操作类型: "网络请求", 数据: "请求数据", 重试次数: 2调试信息输出
python
function 调试关键字 (功能名称, 输入数据, 详细模式="False") do
[打印], 内容: "=== 调试信息开始 ==="
[打印], 内容: "功能: ${功能名称}"
[打印], 内容: "输入数据类型: ${type(${输入数据})}"
if ${详细模式} do
[打印], 内容: "详细输入数据: ${输入数据}"
[打印], 内容: "当前时间: 2024-01-01 12:00:00"
[打印], 内容: "调试模式: 开启"
end
# 执行实际功能
if ${功能名称} == "数据验证" do
if "${输入数据}" != "" do
处理结果 = "验证通过"
else
处理结果 = "验证失败:数据为空"
end
elif ${功能名称} == "数据转换" do
处理结果 = "转换后的" + str(${输入数据})
else
处理结果 = "未知功能: ${功能名称}"
end
[打印], 内容: "处理结果: ${处理结果}"
[打印], 内容: "=== 调试信息结束 ==="
return ${处理结果}
end
# 测试调试功能
[调试关键字], 功能名称: "数据验证", 输入数据: "测试数据"
[调试关键字], 功能名称: "数据转换", 输入数据: "原始数据", 详细模式: True最佳实践
1. 命名规范
python
# ✅ 好的命名 - 清晰描述功能
function 验证用户登录状态 (用户ID, 预期状态="已登录") do
# ...
end
function 发送邮件通知 (收件人, 主题, 内容) do
# ...
end
# ❌ 避免的命名 - 含义不明
function func1 (param1, param2) do
# ...
end
function 处理 (数据) do
# ...
end2. 参数设计
python
# ✅ 好的参数设计
function 创建用户 (用户名, 邮箱, 密码, 角色="普通用户", 激活状态="True") do
# 必需参数在前,可选参数在后,有合理默认值
end
# ❌ 避免的参数设计
function 创建用户 (角色="普通用户", 用户名, 邮箱, 激活状态, 密码) do
# 参数顺序混乱,必需参数和可选参数混合
end3. 功能单一性
python
# ✅ 好的设计 - 单一职责
function 验证邮箱格式 (邮箱地址) do
# 只负责验证邮箱格式
if "@" not in ${邮箱地址} do
return False
end
return True
end
function 发送验证邮件 (邮箱地址, 验证码) do
# 只负责发送邮件
[打印], 内容: "向 ${邮箱地址} 发送验证码: ${验证码}"
return True
end
# ❌ 避免的设计 - 职责混杂
function 验证并发送邮件 (邮箱地址, 验证码) do
# 既验证又发送,职责不清晰
if "@" not in ${邮箱地址} do
return False
end
[打印], 内容: "发送邮件..."
return True
end4. 返回值一致性
python
# ✅ 统一的返回值结构
function 处理订单 (订单ID, 操作) do
if ${操作} == "取消" do
return {"success": True, "message": "订单已取消", "order_id": ${订单ID}}
elif ${操作} == "确认" do
return {"success": True, "message": "订单已确认", "order_id": ${订单ID}}
else
return {"success": False, "message": "不支持的操作", "order_id": ${订单ID}}
end
end5. 适当的注释和说明
python
function 复杂业务逻辑 (输入数据) do
# 使用有意义的变量名作为"注释"
数据验证阶段 = "开始验证输入数据"
[打印], 内容: ${数据验证阶段}
# 第一步:数据清理
清理后数据 = [字符串操作], 操作: "strip", 字符串: ${输入数据}
# 第二步:格式转换
标准格式数据 = [字符串操作], 操作: "upper", 字符串: ${清理后数据}
# 第三步:业务处理
处理结果 = "处理完成: " + ${标准格式数据}
完成阶段 = "业务逻辑处理完成"
[打印], 内容: ${完成阶段}
return ${处理结果}
end自动导入resources目录
零配置自动导入
pytest-dsl 支持自动发现和导入项目根目录下的 resources 目录中的所有 .resource 文件,无需手动使用 @import 指令。这个功能让项目中的自定义关键字可以自动可用,大大简化了测试文件的编写。
项目结构
your_project/
├── resources/ # 自动导入的资源目录
│ ├── common/ # 通用工具关键字
│ │ └── utils.resource # 基础工具函数
│ ├── api/ # API 测试关键字
│ │ └── http_utils.resource # HTTP 工具
│ └── business/ # 业务流程关键字
│ └── workflows.resource # 业务流程
├── tests/ # 测试文件目录
│ ├── *.dsl # DSL 测试文件
│ └── *.py # Python 测试文件
└── config/ # 配置文件目录(可选)
└── *.yaml使用示例
resources/common/utils.resource:
python
@name: "通用工具关键字"
function 格式化消息 (模板, 变量值) do
格式化结果 = "${模板}: ${变量值}"
[打印], 内容: "格式化消息 - ${格式化结果}"
return ${格式化结果}
end
function 验证非空 (值, 字段名="字段") do
if ${值} == "" do
[打印], 内容: "验证失败: ${字段名}不能为空"
return "验证失败"
end
[打印], 内容: "验证通过: ${字段名}值为'${值}'"
return "验证通过"
endresources/api/http_utils.resource:
python
@name: "HTTP工具关键字"
@import: "../common/utils.resource" # 可以导入其他资源文件
function 登录获取Token (用户名, 密码) do
# 使用导入的格式化消息关键字
登录消息 = [格式化消息], 模板: "用户登录", 变量值: ${用户名}
[打印], 内容: ${登录消息}
# 模拟登录过程
模拟Token = "token_${用户名}_123456"
[打印], 内容: "登录成功 - Token: ${模拟Token}"
return ${模拟Token}
endtest_example.dsl:
python
@name: "自动导入示例测试"
# 直接使用resources中定义的关键字,无需@import指令
用户名 = "testuser"
验证结果 = [验证非空], 值: ${用户名}, 字段名: "用户名"
登录结果 = [登录获取Token], 用户名: ${用户名}, 密码: "password123"
[打印], 内容: "验证结果: ${验证结果}"
[打印], 内容: "登录结果: ${登录结果}"功能特点
- 零配置自动导入 - 无需手动配置,系统自动发现并导入
resources目录 - 智能依赖解析 - 自动处理资源文件之间的依赖关系,按正确顺序加载
- 高性能缓存机制 - 避免重复导入同一文件
- 多环境支持 - 在CLI、pytest等不同环境中都能正常工作
- 向后兼容 - 不影响现有的
@import功能
工作原理
- 自动发现机制:系统会自动检测项目根目录下的
resources目录 - 递归扫描:递归查找所有
.resource文件 - 依赖分析:解析文件中的
@import指令,构建依赖关系图 - 拓扑排序:使用拓扑排序算法确定加载顺序
- 按序加载:按依赖关系顺序加载文件,避免依赖冲突
最佳实践
目录组织
resources/ ├── common/ # 通用工具(最基础) ├── api/ # API 相关 ├── ui/ # UI 相关 ├── database/ # 数据库相关 └── business/ # 业务流程(最复杂)避免循环依赖
- 使用分层设计:common -> functional -> business
- 明确依赖关系:在文件头部声明所有依赖
- 合理规划:避免相互引用
命名规范
- 文件命名:使用描述性名称,如
user_management.resource - 关键字命名:使用清晰的中文名称
- 参数命名:使用有意义的参数名
- 文件命名:使用描述性名称,如
运行方式
bash
# CLI工具自动导入
python -m pytest_dsl.cli run test_example.dsl
# pytest集成自动导入
pytest test_example.py -v
# 查看所有可用关键字(包括自动导入的)
python -m pytest_dsl.cli list-keywords --format json调试信息
运行时会显示详细的自动导入过程:
发现resources目录: /path/to/project/resources
在resources目录中发现 3 个资源文件
已注册自定义关键字: 格式化消息 来自文件: .../utils.resource
已注册自定义关键字: 登录获取Token 来自文件: .../http_utils.resource
...
共 43 个关键字
内置: 29 个
项目自定义: 14 个与其他功能的结合
DSL内自定义关键字可以与pytest-dsl的其他功能完美结合:
- 自动导入 - 在
resources目录中组织和管理关键字,自动导入无需配置 - 资源文件 - 在资源文件中定义通用关键字,在测试文件中定义特定关键字
- Hook机制 - 通过Hook动态注册关键字和获取变量配置
- 变量系统 - 使用Hook提供的环境变量和配置
- HTTP测试 - 封装复杂的API测试流程
- 数据驱动 - 结合参数化测试使用
- 断言系统 - 创建自定义的断言关键字
下一步
- 学习 Python代码自定义关键字 了解更强大的代码方式
- 查看 资源文件 了解如何组织和共享关键字
- 阅读 HTTP API测试 了解API测试中的关键字应用