codecamp

Grok 契约测试与 CI mock

CI 里没有真实额度,这件事常被当成「所以接口不用测」。结果是一次字段改名、一次解析层重构,本地跑得好好的,上线才发现取答案的位置变了。问题不在于有没有额度,而在于有没有把「契约」固定下来。

这一节给一套不花钱的契约测试:七类 fixture,覆盖正常与六种异常。

一、问题不是「测不了真实接口」,而是「没定义契约」

契约测试不验证「模型答得好不好」,只验证「你的代码对响应的假设是否还成立」。这些假设包括:

  • 请求体字段名(input 而不是 messages)。
  • 取答案的位置(output_text 还是 output[].content)。
  • 工具调用的错误分类(未知工具、畸形参数)。
  • 错误响应的形态(401 / 429 怎么表现)。

只要这些假设被写进 fixture,CI 就能在没有额度的情况下守住它们。真正的模型质量,另由人工评审与小样本实测负责。

二、最小可运行示例:七类 fixture 与断言

下面的脚本不联网、不需要真实密钥。它用一份 mock 处理器模拟七种输入,逐条比对期望结果。

"""契约测试骨架(mock):七类 fixture 覆盖正常与六种异常。"""

FIXTURES = [
    {"name": "正常回答", "input": {"kind": "text"}, "expect": "text"},
    {"name": "单工具", "input": {"kind": "tool", "n": 1}, "expect": "tool1"},
    {"name": "多工具", "input": {"kind": "tool", "n": 2}, "expect": "tool2"},
    {"name": "畸形参数", "input": {"kind": "tool_bad"}, "expect": "json_error"},
    {"name": "未知工具", "input": {"kind": "tool_unknown"}, "expect": "unknown_tool"},
    {"name": "429 限流", "input": {"kind": "rate"}, "expect": "retry"},
    {"name": "无效 Key", "input": {"kind": "auth"}, "expect": "fail"},
]

def mock_handler(payload: dict) -> str:
    kind = payload["kind"]
    if kind == "text":
        return "text"
    if kind == "tool":
        return f"tool{payload['n']}"
    if kind == "tool_bad":
        return "json_error"
    if kind == "tool_unknown":
        return "unknown_tool"
    if kind == "rate":
        return "retry"
    return "fail"

def run(fixtures: list[dict]) -> list[tuple[str, bool]]:
    results = []
    for f in fixtures:
        try:
            got = mock_handler(f["input"])
            results.append((f["name"], got == f["expect"]))
        except Exception:
            results.append((f["name"], False))
    return results

def offline_check() -> None:
    results = run(FIXTURES)
    for name, ok in results:
        print(f"{name} {'ok' if ok else 'FAIL'}")
    passed = sum(1 for _, ok in results if ok)
    print(f"{passed}/{len(results)} 通过")

if __name__ == "__main__":
    offline_check()

真正落地时,把 mock_handler 换成「解析你代码的输出」的函数:给定一份固定响应,验证解析层取到的是不是期望值。fixture 存成 JSON 文件,跟着代码一起评审。

三、七类 fixture 覆盖什么

fixture 模拟的输入 守住什么
正常回答 纯文本响应 取答案的位置
单工具 一个 tool_call 工具名与参数解析
多工具 并行多个调用 是否全部处理
畸形参数 非法 JSON 解析失败分类
未知工具 未注册的工具名 拒绝而非崩溃
429 限流响应 重试策略
无效 Key 401 不重试、快速失败

三件要强调的事:

  • fixture 是契约,不是快照。不要存整段真实响应(会含敏感信息、也会过期),只存「你的代码依赖的那几个字段」。
  • 七类是下限,不是上限。你的业务若有多轮、流式、引用,各加一条;但先保证这七类绿,再谈扩展。
  • 静态校验 + mock 是本书的验证上限。官方能力、真实延迟、限流阈值都无法在 CI 里复现;这些必须靠小流量实测与线上的观测补足。

四、预期输出与异常分支

运行测试骨架,预期输出:

正常回答 ok
单工具 ok
多工具 ok
畸形参数 ok
未知工具 ok
429 限流 ok
无效 Key ok
7/7 通过

换成真实代码时,若出现异常,按下面顺序定位:

现象 优先怀疑 第一步动作
解析用例红 取答案字段变了 对照官方响应结构
工具用例红 参数结构变了 核对 arguments 与 schema
只有 CI 红本地绿 fixture 过期 用当前响应重录一份
异常用例红 错误分类改了 对齐 7.2 的三类错误

最后一行尤其重要:fixture 过期是这类测试最常见的失效方式。它不是「测试没用」,而是提醒你该更新契约了。

五、判据卡

判断项 该这么做 不该这么做
测什么 契约(字段与位置) 模型回答质量
fixture 内容 只存依赖的字段 存整段真实响应
覆盖范围 正常 + 六类异常 只测 happy path
失败含义 契约变了,去核对 直接改期望值让它绿
验证上限 mock + 小流量实测 指望 CI 覆盖一切

这张卡的重点是契约与质量分开。五条里有三条在说:CI 守结构,人守质量。

六、来源与核验

  • 官方 API 参考:https://docs.x.ai/developers/rest-api-reference(核验日期 2026-11-04)。响应结构与错误形态以该页为准。
  • 官方 Function Calling:https://docs.x.ai/developers/tools/function-calling(核验日期 2026-11-04)。tool_call 与 function_call_output 的结构来自该页,工具解析的三类错误见 7.2。
  • 七类 fixture 的划分与测试骨架为本书原创;具体断言内容需按你的实现补充,本节不提供可直接运行的线上测试。

七、动手试

  1. 把七类 fixture 落成 JSON 文件,写一个读取并逐条断言的脚本,接入你现有的 CI。
  2. 给「多工具」用例补一条断言:当模型一次返回三个调用时,你的代码是否全部执行、是否有遗漏。
  3. 设计一条 fixture 更新流程:谁负责在接口变更后重录、如何评审、多久检查一次过期。写成一个三行的小节,贴进项目 README。
Grok 错误码该不该重试
Grok 密钥轮换与合规交接
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }