Skip to content

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

参数系统详解

参数类型

  1. 必需参数 - 调用时必须提供
  2. 可选参数 - 有默认值,可以不提供
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
    # ...
end

2. 参数设计

python
# ✅ 好的参数设计
function 创建用户 (用户名, 邮箱, 密码, 角色="普通用户", 激活状态="True") do
    # 必需参数在前,可选参数在后,有合理默认值
end

# ❌ 避免的参数设计
function 创建用户 (角色="普通用户", 用户名, 邮箱, 激活状态, 密码) do
    # 参数顺序混乱,必需参数和可选参数混合
end

3. 功能单一性

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
end

4. 返回值一致性

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
end

5. 适当的注释和说明

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 "验证通过"
end

resources/api/http_utils.resource:

python
@name: "HTTP工具关键字"
@import: "../common/utils.resource"  # 可以导入其他资源文件

function 登录获取Token (用户名, 密码) do
    # 使用导入的格式化消息关键字
    登录消息 = [格式化消息], 模板: "用户登录", 变量值: ${用户名}
    [打印], 内容: ${登录消息}
    
    # 模拟登录过程
    模拟Token = "token_${用户名}_123456"
    [打印], 内容: "登录成功 - Token: ${模拟Token}"
    
    return ${模拟Token}
end

test_example.dsl:

python
@name: "自动导入示例测试"

# 直接使用resources中定义的关键字,无需@import指令
用户名 = "testuser"
验证结果 = [验证非空], 值: ${用户名}, 字段名: "用户名"

登录结果 = [登录获取Token], 用户名: ${用户名}, 密码: "password123"

[打印], 内容: "验证结果: ${验证结果}"
[打印], 内容: "登录结果: ${登录结果}"

功能特点

  • 零配置自动导入 - 无需手动配置,系统自动发现并导入resources目录
  • 智能依赖解析 - 自动处理资源文件之间的依赖关系,按正确顺序加载
  • 高性能缓存机制 - 避免重复导入同一文件
  • 多环境支持 - 在CLI、pytest等不同环境中都能正常工作
  • 向后兼容 - 不影响现有的@import功能

工作原理

  1. 自动发现机制:系统会自动检测项目根目录下的resources目录
  2. 递归扫描:递归查找所有.resource文件
  3. 依赖分析:解析文件中的@import指令,构建依赖关系图
  4. 拓扑排序:使用拓扑排序算法确定加载顺序
  5. 按序加载:按依赖关系顺序加载文件,避免依赖冲突

最佳实践

  1. 目录组织

    resources/
    ├── common/          # 通用工具(最基础)
    ├── api/             # API 相关
    ├── ui/              # UI 相关
    ├── database/        # 数据库相关
    └── business/        # 业务流程(最复杂)
  2. 避免循环依赖

    • 使用分层设计:common -> functional -> business
    • 明确依赖关系:在文件头部声明所有依赖
    • 合理规划:避免相互引用
  3. 命名规范

    • 文件命名:使用描述性名称,如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测试流程
  • 数据驱动 - 结合参数化测试使用
  • 断言系统 - 创建自定义的断言关键字

下一步

Released under the MIT License.