让模型根据函数代码提出测试用例,是 AI 辅助开发中一个很适合练习的任务:输入是代码,输出是结构化建议,最终是否执行仍由开发者决定。本篇把结构化输出、结果校验和离线测试组合起来,构建一个最小的测试用例草稿器。它只生成 JSON 草稿,不运行被测代码,也不修改项目文件。
先划清模型和程序的边界 示例输入是一段 Python 函数源码。模型负责阅读逻辑,并提出正常值、边界值和非法值等候选场景;程序负责定义结果格式、检查编号和数量、拒绝重复名称。这样做的原因是:模型适合处理自然语言和代码语义,但不适合充当最终的协议校验器。
输出契约定义为一个 cases 数组。每个用例包含名称、类型、输入、预期结果和设计理由。kind 只允许三个值:normal、boundary、invalid。expected 使用文字描述而不是让模型生成可执行断言,避免把未经审核的模型内容直接拼进测试代码。
结构化输出解决的是“返回什么形状”的问题,不等于“测试一定正确”。例如模型可能漏掉某个分支,或者误读异常条件。因此还要在模型外部检查:至少有一个用例、数量不超过上限、名称不重复,并且三种类型都覆盖。业务规则越明确,越容易写成确定性校验。
准备依赖和环境变量 安装当前 OpenAI Python SDK 与 Pydantic。模型名从环境变量读取,密钥不写入源代码:
1 2 3 python -m pip install -U openai pydantic export OPENAI_API_KEY="替换为你的真实密钥" export MODEL_NAME="替换为你可用的模型名称"
如果还没有密钥,可以先运行代码内置的 --self-test。这个测试只验证 Pydantic 对象和本地校验逻辑,不会发出网络请求,所以它不能证明模型调用成功。
编写最小测试用例草稿器 把下面代码保存为 test_case_drafter.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 import jsonimport osimport sysfrom enum import Enumfrom openai import OpenAIfrom pydantic import BaseModel, ConfigDictSAMPLE_SOURCE = '''def parse_port(value: str) -> int: port = int(value) if not 1 <= port <= 65535: raise ValueError("port out of range") return port ''' class TestKind (str , Enum): normal = "normal" boundary = "boundary" invalid = "invalid" class TestCase (BaseModel ): model_config = ConfigDict(extra="forbid" ) name: str kind: TestKind input_value: str expected: str reason: str class TestPlan (BaseModel ): model_config = ConfigDict(extra="forbid" ) cases: list [TestCase] def validate_plan (plan: TestPlan ) -> TestPlan: if not 3 <= len (plan.cases) <= 6 : raise ValueError("测试用例数量必须在 3 到 6 个之间" ) names = [case .name.strip() for case in plan.cases] if any (not name for name in names): raise ValueError("测试用例名称不能为空" ) if len (names) != len (set (names)): raise ValueError("测试用例名称不能重复" ) kinds = {case .kind for case in plan.cases} required = {TestKind.normal, TestKind.boundary, TestKind.invalid} if kinds != required: raise ValueError("必须覆盖 normal、boundary、invalid 三类" ) return plan def ask_model (source: str ) -> TestPlan: client = OpenAI(api_key=os.environ["OPENAI_API_KEY" ]) response = client.responses.parse( model=os.environ["MODEL_NAME" ], instructions=( "你是 Python 测试设计助手。请阅读输入的函数源码,生成 3 到 6 个测试用例。" "必须覆盖 normal、boundary、invalid 三类;只描述候选测试,不要执行代码," "不要编造源码中不存在的行为。input_value 和 expected 使用简短文字描述。" ), input =json.dumps({"source" : source}, ensure_ascii=False ), text_format=TestPlan, ) if response.output_parsed is None : raise RuntimeError("模型没有返回可解析的测试计划" ) return validate_plan(response.output_parsed) def self_test () -> None : fixture = TestPlan( cases=[ TestCase( name="普通端口" , kind=TestKind.normal, input_value='"8080"' , expected="返回整数 8080" , reason="覆盖常见合法输入" , ), TestCase( name="最小合法端口" , kind=TestKind.boundary, input_value='"1"' , expected="返回整数 1" , reason="覆盖下边界" , ), TestCase( name="超出范围" , kind=TestKind.invalid, input_value='"65536"' , expected="抛出 ValueError" , reason="覆盖上界之外的输入" , ), ] ) validate_plan(fixture) print ("self-test passed" ) def main () -> None : if "--self-test" in sys.argv: self_test() return plan = ask_model(SAMPLE_SOURCE) with open ("test-plan.json" , "w" , encoding="utf-8" ) as output: json.dump(plan.model_dump(mode="json" ), output, ensure_ascii=False , indent=2 ) output.write("\n" ) print ("已生成测试草稿:test-plan.json" ) if __name__ == "__main__" : main()
先运行离线检查:
1 python test_case_drafter.py --self-test
确认自测通过后,再设置两个环境变量运行真实调用:
1 python test_case_drafter.py
真实调用成功后,程序会把通过解析和校验的结果写入 test-plan.json。文件中的内容来自本次模型响应,具体用例会随模型、提示词和服务配置变化,不能预先假定。查看草稿后,可以由开发者手动把候选场景改写成项目现有测试框架能够执行的代码。
关键代码为什么这样写 TestPlan 和 TestCase 是输出契约。ConfigDict(extra="forbid") 会拒绝未声明的字段,避免模型悄悄增加下游没有处理的结构。TestKind 把分类限制成有限枚举,模型即使返回其它字符串,也会在解析阶段失败。
client.responses.parse(..., text_format=TestPlan) 使用 SDK 根据 Pydantic 类型生成结构化输出约束,并把可解析结果放在 output_parsed。代码仍然检查 None,因为拒答、输出不完整或其它异常情况都不应被当成有效计划。结构化解析通过后,validate_plan 再检查覆盖范围和重复名称;两层检查解决的是不同问题。
离线 self_test 使用固定的 TestPlan,没有伪造模型调用结果。它只验证本地协议:三种类型都存在、名称不重复、数量在范围内。实际项目可以继续增加失败样例,例如构造两个同名用例,确认校验确实会拒绝错误数据。
常见问题 为什么不让模型直接生成 pytest 代码? 代码生成可以作为下一步,但直接执行模型生成的代码会扩大风险。先生成经过审阅的场景草稿,开发者再决定断言写法、夹具和权限边界,更容易发现模型对业务规则的误解。
结构化输出通过后,为什么还要人工检查? 结构化输出主要保证字段、类型和枚举符合契约,不能证明测试覆盖了所有业务分支,也不能证明预期结果正确。覆盖率、边界条件和领域规则仍需要测试作者确认。
函数源码很长怎么办? 不要无上限地拼进一次请求。可以先截取目标函数及其必要依赖,或按模块分批生成草稿,再由程序合并并检查重复。输入裁剪应由代码完成,不能只依赖提示词提醒模型“不要遗漏”。
模型调用失败时是否应该自动重试? 可以对超时、临时网络错误等可重试异常使用有限次数和指数退避;解析失败或业务校验失败则应记录原因,必要时调整输入或提示词,而不是无限重复同一个请求。
小结 这个综合项目把代码理解任务拆成了四步:用 Pydantic 定义测试草稿契约,用 Responses API 请求结构化结果,用确定性规则检查数量、名称和类型覆盖,再由人工决定是否落地为真实测试。模型提供候选,程序守住格式和边界,开发者确认语义,这种分工比让模型直接执行生成代码更容易测试、回放和维护。
官方参考:Structured model outputs ;OpenAI Python SDK 。