testoracle
测试预言库:用业务不变量规则做断言,避免硬编码预期值。
github 地址:https://github.com/tynam-yang/testoracle
很多自动化测试脚本大量硬编码返回值(assert resp["total"] == 10),业务微小变更就造成脚本批量失效。
testoracle 基于 Test Oracle(测试预言) 理论------测试不一定需要预先知道精确输出值,只需要判定输出是否符合业务约束、不变量规则。本库校验 total >= 0、id 不重复 这类业务规则,而非写死精确返回值。
- 适用于:接口返回、UI 提取数据、JSON 报文、报表数据
- 形态:Python 库 + pytest 插件 + CLI
- 轻量:无 Web 界面、无数据库、不发起 HTTP 请求、只负责校验数据
安装
bash
pip install testoracle
依赖:jsonpath-ng、pyyaml、pytest、allure-pytest。
快速开始
1. 编写规则(yaml)
直接用谓词字段声明约束;同一条规则声明多个谓词时
自动组合为复合规则(AND 语义):
yaml
rules:
- name: "分页total不能为负数"
path: "$.total"
min: 0
- name: "列表ID不能重复"
path: "$.data[*].id"
unique: true
- name: "返回数据不能携带密码明文"
path: "$.data[*].password"
forbidden: true
- name: "状态只能为0/1/2"
path: "$.data[*].status"
allowed_values: [0, 1, 2]
# 复合规则:min + not_null 都要通过
- name: "价格大于0,且不可为空"
path: "$.data[*].price"
min: 0
not_null: true
1b. 显式 type 式(备选)
也可以用 type 字段显式指定规则类型:
yaml
rules:
- name: "分页total不能为负数"
type: "range"
path: "$.total"
min: 0
- name: "列表id不能重复"
type: "unique"
path: "$.data[*].id"
- name: "不能返回明文password字段"
type: "forbidden_field"
path: "$.data[*].password"
两种风格可混用,也可在同一文件里共存。谓词字段含义见下表。
2. 作为 Python 库调用
支持三种规则来源:YAML 文件 、JSON 文件 、字典内联。
python
from testoracle import OracleEngine
data = {"total": 20, "data": [{"id": 1, "status": 0}, {"id": 2, "status": 1}]}
# 方式一:从 YAML 文件加载规则
engine = OracleEngine.from_yaml("demo_rules.yaml")
# 方式二:从 JSON 文件加载规则
engine = OracleEngine.from_json("demo_rules.json")
# 方式三:直接传入规则字典(无需文件)
engine = OracleEngine.from_rules({
"rules": [
{"name": "total不能为负", "path": "$.total", "min": 0},
{"name": "id不能重复", "path": "$.data[*].id", "unique": True},
{"name": "状态合法", "path": "$.data[*].status", "allowed_values": [0, 1, 2]},
]
})
# 校验数据
result = engine.validate(data)
print(result.is_passed)
print(result.summary())
for item in result.rule_results:
print(item)
JSON 规则文件示例(demo_rules.json):
json
{
"rules": [
{"name": "total不能为负", "path": "$.total", "min": 0},
{"name": "id不能重复", "path": "$.data[*].id", "unique": true},
{"name": "状态合法", "path": "$.data[*].status", "allowed_values": [0, 1, 2]}
]
}
3. 在 pytest 中使用
python
from testoracle.pytest_plugin import assert_oracle
def test_user_list():
resp = requests.get("/api/user/list")
# 方式一:YAML 规则文件
assert_oracle(resp.json(), rules_file="demo_rules.yaml")
# 方式二:JSON 规则文件
assert_oracle(resp.json(), rules_json="demo_rules.json")
# 方式三:内联规则字典
assert_oracle(resp.json(), rules={
"rules": [
{"name": "total非负", "path": "$.total", "min": 0},
{"name": "id唯一", "path": "$.data[*].id", "unique": True},
]
})
# 方式四:直接传入 engine 实例
engine = OracleEngine.from_yaml("demo_rules.yaml")
assert_oracle(resp.json(), engine=engine)
校验失败时会展示:规则名称、jsonpath 路径、实际值、失败原因。
若安装了 allure-pytest,每条规则结果会自动嵌入 allure 步骤。
4. CLI 命令行
bash
# 对一份 json 样本执行规则校验
testoracle run --data sample.json --rules demo_rules.yaml --output result.json
校验存在失败时退出码为 1,可在 CI 中阻断流水线。
支持的规则
两种声明风格:type 式 (显式指定类型)与谓词推断式 (不写 type,按字段推断,可组合)。
规则总览
| 约束 | type 值 |
谓词字段 | 说明 |
|---|---|---|---|
| 数值范围 | range |
min, max |
数值落在 [min, max] |
| 值等于/浮点近似 | range |
equals, epsilon |
精确等于或浮点近似相等 |
| 非空 | not_null |
not_null: true |
非 None / 空串 / 空集合 |
| 唯一性 | unique |
unique: true |
数组元素两两不重复 |
| 禁止字段 | forbidden_field |
forbidden: true |
路径匹配到任意值即失败 |
| 枚举 | enum |
allowed_values |
值在允许集合内 |
| 长度 | length |
min_length/max_length/length_equals |
字符串/集合长度校验 |
| 类型 | type |
type: "number" 等 |
值的 Python 类型校验 |
| 正则 | regex |
regex: "phone" 等 |
值匹配正则表达式 |
| 条件 | conditional |
if + then |
当 A 满足时,B 必须满足 |
| 字段存在 | field_exist |
must_exist |
字段 key 必须存在于 dict 中 |
| 字段不存在 | field_not_exist |
must_not_exist |
字段 key 一定不能存在 |
| 时间 | time |
time_format/future/past/before/after |
日期/时间校验 |
| 跨字段对比 | compare |
left_path + right_path/right_expr |
跨字段值对比或表达式计算 |
| 单调递增/递减 | monotonic |
monotonic/monotonic_step |
列表值单调校验,可选步长 |
| 子 schema | item_schema |
item_rules |
数组内对象嵌套子规则校验 |
| 或表达式 | any_of |
any_of |
多个子规则满足其一即可 |
| 分组统计 | count_by_group |
min_count/max_count/group_by |
分组计数约束 |
| 非法字符检测 | text_sanitize |
sanitize_categories/sanitize_custom_patterns |
XSS/SQL 注入等危险模式检测 |
| 嵌套深度限制 | depth_limit |
max_depth |
对象嵌套层级不能过深 |
| 内容包含 | contains |
contains |
值必须包含指定内容 |
| 内容排除 | not_contains |
not_contains |
值不能包含指定内容 |
| 至少包含其一 | --- | contains_any |
必须至少包含多项中的一项 |
| 必须全部包含 | --- | contains_all |
必须同时包含所有指定内容 |
| 字段互斥 | mutual_exclusive |
mutual_exclusive |
多个字段不能同时有值 |
| 聚合统计 | aggregate |
aggregate |
求和/平均值/计数等聚合校验 |
| 引用完整性 | referential_integrity |
source_path + target_path |
跨数组引用值必须存在 |
| 复合 | ---(自动) | 多个谓词并存 | 多约束 AND,全部通过才算通过 |
条件规则支持的操作符
| 操作符 | 说明 |
|---|---|
equals |
等于 |
not_equals |
不等于 |
in |
值在列表中 |
not_in |
值不在列表中 |
contains |
字符串/列表包含 |
not_contains |
不包含 |
starts_with |
字符串前缀匹配 |
ends_with |
字符串后缀匹配 |
matches |
正则匹配 |
exists |
路径存在值 |
not_exists |
路径不存在值 |
greater_than |
大于 |
less_than |
小于 |
greater_or_equal |
大于等于 |
less_or_equal |
小于等于 |
is_type |
Python 类型判断 |
类型规则支持的类型
int、float、number(int 或 float,排除 bool)、str/string、bool/boolean、list/array、dict/object、null/none、any。
正则规则内置模式
| 内置规则名 | 说明 |
|---|---|
phone |
中国大陆手机号 |
email |
邮箱地址 |
id_card |
18 位中国大陆身份证号 |
url |
HTTP/HTTPS URL |
ipv4 |
IPv4 地址 |
chinese |
纯中文字符 |
postal_code |
中国 6 位邮政编码 |
license_plate |
中国车牌号 |
也支持自定义正则:pattern: "^[a-zA-Z0-9]+$"。
时间运算表达式
支持的时间关键字:today、now、today_start、today_end。
支持运算表达式:today - 30(30天前)、today + 7d、now - 1h、now + 30m、today - 2w、today + 1y。
单位:d(天)、h(小时)、m(分钟)、w(周)、y(年)。
聚合类型
sum(求和)、avg(平均)、count(计数)、min(最小)、max(最大)。
说明
- 谓词推断式中
min/max一律按数值范围 处理;若需长度 校验请用min_length/max_length/length_equals或type: length。 equals谓词表示"值等于",支持epsilon浮点近似相等。- 一条谓词式规则只含单个谓词时退化为普通规则;含多个谓词时自动包装为
composite。 - 谓词推断式中的
type字段值如果是 Python 类型名(如number、list),会被当作类型谓词处理;如果是规则类型名(如range、type),会被当作显式 type 式处理。 - 谓词推断式中的
regex字段值如果是内置规则名(如phone、email),会使用内置正则;否则会被当作自定义正则表达式。 - 所有规则通过 jsonpath 表达式(基于
jsonpath-ng)定位数据。
编写示例
数值范围与浮点近似相等
yaml
rules:
- name: "分页total不能为负数"
path: "$.total"
min: 0
# 浮点数近似相等
- name: "计算结果应近似等于3.14"
path: "$.result"
equals: 3.14
epsilon: 0.01
# 精确等于
- name: "状态码必须为200"
path: "$.code"
equals: 200
条件规则(if-then)
yaml
rules:
- name: "已退款订单必须有退款金额"
if:
path: "$.status"
operator: "equals"
value: "refunded"
then:
path: "$.refund_amount"
min: 0.01
not_null: true
字段存在性
yaml
rules:
- name: "成功响应必须包含token字段"
path: "$"
must_exist: "access_token"
- name: "普通用户不能返回内部secret字段"
path: "$"
must_not_exist: "inner_secret"
时间校验
yaml
rules:
- name: "创建时间不能早于30天前"
type: "time"
path: "$.created_at"
format: "%Y-%m-%d"
after: "today - 30"
- name: "过期时间必须在未来"
path: "$.expire_at"
time_format: "%Y-%m-%d %H:%M:%S"
future: true
- name: "生日不能晚于今天"
path: "$.birthday"
time_format: "%Y-%m-%d"
max: "today"
past: true
跨字段对比与计算
yaml
rules:
- name: "退款金额不能超过订单金额"
type: "compare"
left_path: "$.refund_amount"
operator: "less_or_equal"
right_path: "$.order_amount"
# 表达式计算对比
- name: "总价必须等于单价乘以数量"
type: "compare"
left_path: "$.total"
operator: "equals"
right_expr: "$.price * $.quantity"
# 谓词推断式
- name: "折扣价必须低于原价"
left_path: "$.sale_price"
compare_operator: "less_than"
right_path: "$.original_price"
单调递增/递减
yaml
rules:
- name: "列表id必须单调递增"
type: "monotonic"
path: "$.data[*].id"
direction: "asc"
- name: "创建时间倒序返回"
type: "monotonic"
path: "$.data[*].create_ts"
direction: "desc"
strict: true
# 带步长校验
- name: "ID必须连续递增(步长为1)"
type: "monotonic"
path: "$.data[*].id"
direction: "asc"
step: 1
# 谓词推断式
- name: "序号必须递增"
path: "$.data[*].seq"
monotonic: "asc"
数组内对象子 schema 校验
yaml
rules:
- name: "每一条订单子项都要满足基础约束"
type: "item_schema"
path: "$.order_items[*]"
item_rules:
- min: 0
path: "$.amount"
- not_null: true
path: "$.goods_id"
或表达式(any_of)
yaml
rules:
- name: "状态可以是0、1或2"
type: "any_of"
path: "$.data[*].status"
any_of:
- path: "$.status"
equals: 0
- path: "$.status"
equals: 1
- path: "$.status"
equals: 2
分组统计
yaml
rules:
# 按 status 分组,已完成的记录至少 2 条
- name: "已完成订单至少2条"
type: "count_by_group"
path: "$.data[*]"
filter: {"status": 2}
min_count: 2
# 使用 group_by 自动推断父数组
- name: "已完成订单至少2条"
group_by: "$.data[*].status"
filter: {"status": 2}
min_count: 2
# 最大数量限制
- name: "失败订单最多1条"
path: "$.data[*]"
filter: {"status": "failed"}
max_count: 1
非法字符检测
yaml
rules:
- name: "用户输入不能包含危险字符"
type: "text_sanitize"
path: "$.content"
categories: ["xss", "sql_injection"]
# 带自定义模式
- name: "订单描述不能有特殊标记"
type: "text_sanitize"
path: "$.description"
categories: ["all"]
custom_patterns:
- name: "自定义危险词"
pattern: "SELECT\\s+.*FROM\\s+password"
# 谓词推断式
- name: "昵称不能包含脚本标签"
path: "$.nickname"
sanitize_categories: ["xss"]
嵌套深度限制
yaml
rules:
- name: "返回对象嵌套不能超过5层"
type: "depth_limit"
path: "$.data"
max_depth: 5
# 谓词推断式
- name: "data 嵌套不能超过3层"
path: "$.data"
max_depth: 3
内容包含与排除
yaml
rules:
- name: "商品名称必须包含'无线'"
type: "contains"
path: "$.data[*].name"
value: "无线"
- name: "描述不能包含'测试'"
type: "not_contains"
path: "$.data[*].description"
value: "测试"
# 谓词推断式
- name: "标签必须包含'热卖'"
path: "$.data[*].tags"
contains: "热卖"
# 至少包含一个
- name: "商品属性必须包含'新品'或'促销'"
path: "$.data[*].tags"
contains_any: ["新品", "促销"]
# 必须全部包含
- name: "地址必须同时包含'省'和'市'"
path: "$.data[*].address"
contains_all: ["省", "市"]
字段互斥
yaml
rules:
- name: "支付方式和优惠券不能同时有值"
type: "mutual_exclusive"
path: "$"
fields: ["$.pay_type", "$.coupon_type"]
# 谓词推断式
- name: "三种配送方式只能选一种"
path: "$"
mutual_exclusive: ["$.express_delivery", "$.pickup", "$.store_delivery"]
聚合统计
yaml
rules:
# 求和等于另一字段
- name: "明细金额总和等于总金额"
type: "aggregate"
path: "$.items[*].amount"
agg: "sum"
compare_path: "$.total_amount"
operator: "equals"
# 求和与常量比较
- name: "订单明细总和不能超过10000"
path: "$.items[*].amount"
agg: "sum"
max: 10000
# 平均值范围
- name: "评分平均分在3-5之间"
path: "$.scores[*].value"
agg: "avg"
min: 3
max: 5
# 计数校验
- name: "子项数量必须至少2条"
path: "$.items[*]"
agg: "count"
min: 2
# 谓词推断式
- name: "明细总和等于total"
path: "$.items[*].amount"
aggregate: "sum"
compare_path: "$.total"
跨数组引用完整性
校验一个数组中的引用值是否全部存在于另一个数组中,类似数据库的外键约束。
| 字段 | 说明 |
|---|---|
source_path |
引用方路径,提取需要校验的引用值(如商品中的 category_id) |
target_path |
被引用方路径,提取合法值集合(如分类列表中的 id) |
校验逻辑:source_path 提取到的每一个值,都必须在 target_path 提取到的值集合中存在,否则失败。
yaml
rules:
# 商品引用的分类ID,必须全部存在于分类列表的ID中
- name: "商品分类ID必须合法"
type: "referential_integrity"
source_path: "$.items[*].category_id" # 引用方:每个商品的 category_id
target_path: "$.categories[*].id" # 被引用方:分类列表中所有合法的 id
# 谓词推断式
- name: "下单用户必须存在"
source_path: "$.orders[*].user_id" # 引用方:每个订单的 user_id
target_path: "$.users[*].id" # 被引用方:用户列表中所有合法的 id
上述示例中,如果 $.items[*].category_id 提取到 [1, 2, 99],而 $.categories[*].id 提取到 [1, 2, 3],则 99 未在目标集合中,校验失败。
失败结果结构
json
{
"is_passed": false,
"summary": "总计 6 条规则,通过 2,失败 4",
"passed_count": 2,
"failed_count": 4,
"rule_results": [
{
"rule_name": "分页total不能为负数",
"rule_type": "range",
"passed": false,
"path": "$.total",
"actual": -5,
"message": "值 -5 小于最小值 0"
}
]
}
CI 集成示例(GitLab CI)
yaml
test:oracle:
script:
- testoracle run --data sample.json --rules demo_rules.yaml --output result.json
artifacts:
when: always
paths:
- result.json
非 0 退出码会阻断流水线;result.json 作为产物保留供排查。
自定义规则(扩展点)
python
from testoracle.rules import register_rule
from testoracle.rules.base import BaseRule, RuleResult
class MyRule(BaseRule):
rule_type = "my_rule"
def validate(self, data):
vals = self.extract_values(data)
# ... 自定义逻辑
return self._pass(vals)
register_rule("my_rule", MyRule)
注册后即可在 yaml 中使用 type: "my_rule"。
项目结构
testoracle/
├── testoracle/
│ ├── core/ # 规则解析、执行引擎、数据提取、报告
│ │ ├── rule_engine.py # OracleEngine + ValidationReport
│ │ ├── rule_parser.py # YAML/JSON 规则解析器
│ │ ├── extractor.py # JSONPath 数据提取
│ │ └── exceptions.py # 自定义异常
│ ├── rules/ # 内置规则实现与注册表
│ │ ├── __init__.py # 规则注册表 + 谓词推断
│ │ ├── base.py # BaseRule 抽象基类
│ │ ├── _comparators.py # 共享比较操作符工具
│ │ ├── _subrule_mixin.py # 子规则编译混合类
│ │ ├── range_rule.py # 数值范围 + equals + epsilon
│ │ ├── not_null_rule.py # 非空校验
│ │ ├── unique_rule.py # 唯一性校验
│ │ ├── enum_rule.py # 枚举校验
│ │ ├── length_rule.py # 长度校验
│ │ ├── type_rule.py # 类型校验
│ │ ├── regex_rule.py # 正则校验(含内置模式)
│ │ ├── composite_rule.py # 复合规则(AND)
│ │ ├── conditional_rule.py # 条件规则(if-then)
│ │ ├── field_exist_rule.py # 字段存在性(含 forbidden 模式)
│ │ ├── time_rule.py # 时间校验(含日期运算)
│ │ ├── compare_rule.py # 跨字段对比与表达式计算
│ │ ├── monotonic_rule.py # 单调递增/递减
│ │ ├── item_schema_rule.py # 数组子 schema 校验
│ │ ├── any_of_rule.py # 或表达式(OR)
│ │ ├── count_by_group_rule.py # 分组统计
│ │ ├── text_sanitize_rule.py # 非法字符检测
│ │ ├── depth_limit_rule.py # 嵌套深度限制
│ │ ├── contains_rule.py # 内容包含与排除
│ │ ├── mutual_exclusive_rule.py # 字段互斥
│ │ ├── aggregate_rule.py # 聚合统计
│ │ └── referential_integrity_rule.py # 跨数组引用完整性
│ ├── pytest_plugin.py # pytest 插件入口
│ └── cli.py # 命令行入口
├── examples/ # 演示规则、库示例、pytest 示例、样本数据
├── tests/ # 项目自身单元测试(400+ 用例)
└── pyproject.toml
设计原则
- 不写死预期值,只声明业务不变量
- 只校验数据,不发起请求、不抓流量、不做用例管理平台
- 规则解析 → 数据提取 → 规则校验 → 结果报告,职责单一
- 两种声明风格可混用:显式
type式与谓词推断式
License
MIT