JSON Schema 是实现全量字段校验最核心、最标准的工具。
校验接口返回响应的全部字段(更进一步的断言)
字段名
字段类型
字段值
定义 JSON 语法校验格式
比对接口实际响应数据是否符合 JSON 校验格式
pip install jsonschema
JSON Schema Validator - Newtonsoft
1.导包 import jsonschema
2.定义 jsonschema 格式 数据校验规则
3.调用 jsonschema.validate(instance = "json 数据" , schema = "jsonschema 规则")
校验通过:返回 None
校验失败:
| 关键字 | 描述 |
|---|---|
type |
表示待校验元素的类型 |
properties |
定义待校验的 JSON 对象中,各个 key-value 对中 value 的限制条件 |
required |
定义待校验的对象中,必须存在的 key |
const |
JSON 元素必须等于指定的内容 |
pattern |
使用正则表达式约束字符串类型数据 |
作用:约束数据类型
integer —— 整数
string —— 字符串
object —— 对象
array —— 数组
number —— 整数/小数
null —— 空值
boolean —— 布尔值
语法
{
“type” : ”数据类型”
}
说明:是 type 关键字的辅助,用 type 的值为 object 的场景
作用:指定对象中每个字段的检验规则,可以嵌套使用
作用:校验对象中必须存在的字段。字段必须是关键字,且唯一
语法:
{
“required” : [ “字段1” , ”字段2” , ...]
}
作用:校验的字段名是一个固定值
{
"字段" : { "const" : 具体值 }
}
作用:指定正则表达式,对字符串进行模糊匹配
基础正则示例:
1 包含字符串:hello
2 以字符串开头 ^:^hello 如:hello,world
3 以字符串结尾 $:hello$ 如:中国,hello
4 匹配[]内任意1个字符[]:[0 - 9]匹配任意一个数字 [a - z]匹配任意一个小写字母 [cjfew9823]匹配任意一个
5 匹配指定次数{}:[0 - 9]{11}匹配11个数字
语法:
{
“字段”:{“pattern” : “正则表达式”}
}
import pytest
import jsonschema
from jsonschema import validate
# 定义完整的JSON Schema
user_schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "用户注册信息校验Schema",
"type": "object",
"properties": {
"userType": {
"type": "string",
"const": "registered",
"description": "用户类型必须为固定值'registered'"
},
"userId": {
"type": "string",
"pattern": "^USER_[A-Z0-9]{8}$",
"description": "用户ID格式:USER_后跟8位大写字母数字"
},
"username": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]{3,15}$",
"description": "用户名:字母开头,4-16位字母数字下划线"
},
"password": {
"type": "string",
"pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,20}$",
"description": "密码复杂度校验"
},
"email": {
"type": "string",
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
},
"phone": {
"type": "string",
"pattern": "^1[3-9]\\d{9}$"
},
"age": {
"type": "integer",
"minimum": 18,
"maximum": 100
},
"salary": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": True
},
"isActive": {
"type": "boolean"
},
"registrationDate": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$"
},
"userLevel": {
"type": "integer",
"const": 1
},
"profile": {
"type": "object",
"properties": {
"nickname": {"type": "string", "minLength": 1, "maxLength": 20},
"avatar": {"type": "string", "pattern": "^https?://.*\\.(jpg|jpeg|png|gif)$"}
},
"required": ["nickname"],
"additionalProperties": False
}
},
"required": [
"userType", "userId", "username", "password", "email",
"age", "isActive", "registrationDate", "userLevel"
],
"additionalProperties": False
}
def validate_user_data(user_data):
"""全量字段校验函数"""
try:
validate(instance=user_data, schema=user_schema)
return True, "校验通过"
except jsonschema.ValidationError as e:
return False, f"校验失败: {e.message}"
# 测试数据
valid_user_data = {
"userType": "registered",
"userId": "USER_ABCD1234",
"username": "john_doe123",
"password": "Passw0rd!",
"email": "john.doe@example.com",
"phone": "13800138000",
"age": 25,
"salary": 15000.50,
"isActive": True,
"registrationDate": "2024-01-15T10:30:00Z",
"userLevel": 1,
"profile": {
"nickname": "John",
"avatar": "https://example.com/avatar.jpg"
}
}
# 使用pytest的参数化测试
@user1ize("test_name,test_data,expected_pass", [
# 有效测试用例
("完全有效数据", valid_user_data, True),
# 必填字段测试
("缺少必填字段username", {**valid_user_data, "username": None}, False),
("缺少必填字段email", {k: v for k, v in valid_user_data.items() if k != "email"}, False),
# 数据类型测试
("age字段类型错误-字符串", {**valid_user_data, "age": "25"}, False),
("age字段类型错误-浮点数", {**valid_user_data, "age": 25.5}, False),
("isActive字段类型错误", {**valid_user_data, "isActive": "true"}, False),
# const约束测试
("违反userType的const约束", {**valid_user_data, "userType": "guest"}, False),
("违反userLevel的const约束", {**valid_user_data, "userLevel": 2}, False),
# pattern格式测试
("userId格式错误", {**valid_user_data, "userId": "user_123"}, False),
("email格式错误", {**valid_user_data, "email": "invalid-email"}, False),
("phone格式错误", {**valid_user_data, "phone": "1234567890"}, False),
("username格式错误-太短", {**valid_user_data, "username": "ab"}, False),
("username格式错误-特殊字符开头", {**valid_user_data, "username": "1john"}, False),
# 数值范围测试
("age小于最小值", {**valid_user_data, "age": 16}, False),
("age大于最大值", {**valid_user_data, "age": 101}, False),
("salary非正数", {**valid_user_data, "salary": 0}, False),
# 嵌套对象测试
("profile缺少必填nickname", {**valid_user_data, "profile": {"avatar": "https://example.com/img.jpg"}}, False),
("profile中avatar格式错误", {**valid_user_data, "profile": {"nickname": "John", "avatar": "invalid-url"}}, False),
# 额外字段测试
("包含额外字段", {**valid_user_data, "extraField": "value"}, False),
])
def test_user_data_validation(test_name, test_data, expected_pass):
"""参数化测试用例 - 测试各种验证场景"""
is_valid, message = validate_user_data(test_data)
if expected_pass:
assert is_valid == True, f"{test_name} 应该通过验证,但实际失败: {message}"
else:
assert is_valid == False, f"{test_name} 应该失败验证,但实际通过了"
# 特殊的边界值测试
def test_edge_cases():
"""边界值测试"""
# 边界年龄测试
edge_age_cases = [
(17, False), # 小于最小值
(18, True), # 等于最小值
(100, True), # 等于最大值
(101, False) # 大于最大值
]
for age, expected in edge_age_cases:
test_data = {**valid_user_data, "age": age}
is_valid, message = validate_user_data(test_data)
assert is_valid == expected, f"年龄边界测试失败: age={age}, 期望={expected}, 实际={is_valid}"
# 测试详细的错误信息
def test_validation_error_messages():
"""测试错误信息的具体内容"""
# 测试const约束的错误信息
invalid_data = {**valid_user_data, "userType": "invalid"}
is_valid, message = validate_user_data(invalid_data)
assert not is_valid
assert "const" in message or "invalid" in message.lower()
# 测试pattern的错误信息
invalid_data = {**valid_user_data, "email": "invalid"}
is_valid, message = validate_user_data(invalid_data)
assert not is_valid
assert "pattern" in message or "invalid" in message.lower()
# 使用fixture来准备测试数据
@pytest.fixture
def base_user_data():
"""提供基础的有效用户数据fixture"""
return valid_user_data.copy()
def test_with_fixture(base_user_data):
"""使用fixture的测试示例"""
# 修改fixture数据但不影响其他测试
base_user_data["username"] = "new_user_123"
is_valid, message = validate_user_data(base_user_data)
assert is_valid == True
# 运行测试的命令行示例
if __name__ == "__main__":
# 可以直接使用 pytest 命令运行
# 或者在代码中运行
pytest.main([__file__, "-v"])
[tool:pytest]
addopts = -v --tb=short
testpaths = .
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 运行所有测试
pytest
# 运行特定文件并显示详细信息
pytest test_user_validation.py -v
# 运行包含特定标记的测试
pytest -k "test_edge_cases" -v
# 生成测试报告
pytest --html=report.html
我最近跑步的最后冲刺阶段,脑子里竟然会蹦出来” 全量校验字段 “这六个字,大抵是魔性洗脑导致的吧。