Codex 的测试哲学——为什么集成测试比单元测试更重要

当你写一个 AI agent 的测试时,你会怎么测?

传统的软件测试教你"先写单元测试,再写集成测试"。但 Codex 团队把这个金字塔倒过来了 :集成测试调用 harness 超过 500 处(grep -r 'test_codex()' codex-rs/core/tests/ | wc -l),snapshot 文件 426 个(find . -name '*.snap' | wc -l),测试基础设施代码超过 6.5 万行------远超单元测试规模。

为什么?因为 AI agent 的核心价值不在于"每个函数正确",而在于"整个交互流程符合预期"。一个函数可能 100% 正确,但放在完整的 agent turn 中可能产生完全错误的行为。

本文带你走进 Codex 的测试体系,看他们如何用集成测试捕获 AI 系统的真实行为。


场景触发条件:什么情况下 Codex 团队会决定增加一种测试类型?

从代码库结构和测试目录组织可以合理推断 Codex 团队扩展测试策略的动机:

集成测试成为主力:当 agent 逻辑越来越复杂时,团队发现单元测试无法覆盖"用户输入 → 模型推理 → 工具调用 → 结果返回"的完整链条。一个函数在隔离环境下正确,不代表在完整流程中能正常工作。

Snapshot 测试引入 :当 TUI 的渲染输出频繁变化,而肉眼审查每次 PR 的 UI 差异不现实时,团队引入了 cargo insta------把"看起来对"变成"机器能验证"。

TUI PTY 测试:当 TUI 在无头环境(CI)中频繁崩溃时,团队开发了基于伪终端的测试框架------模拟真实终端的交互。

Docker 远程测试:当本地测试无法复现远程执行环境的问题时,团队构建了基于 Docker 的远程测试环境。

一个可行的判断标准:当某类 bug 反复在"集成环境"中出现,但在单元测试中无法复现时,就值得新增一种集成测试


核心基础设施:test_codex harness

Codex 测试体系的核心不是某个测试框架,而是一个自定义的测试 harnessTestCodexBuilder

它让你可以用几行代码测试完整的 AI 交互:

rust 复制代码
let codex = test_codex()
    .with_model("gpt-4")
    .with_config(...)
    .build(&server).await;

codex.submit_turn("write a hello world program").await;
let output = codex.function_call_stdout().await;
assert!(output.contains("hello"));

这个 harness 在内部做了很多事情:

  • 启动 wiremock::MockServer 作为模型 provider,模拟 OpenAI API 响应
  • 创建临时目录作为 CODEX_HOME 和工作区
  • 设置确定性的环境变量和进程 ID
  • 提供便捷方法:submit_turn()write_file()read_file_text()request_bodies()

整个代码库里有 超过 500 处调用 这个 harness。它不是"一个测试工具",而是集成测试的标准写法

harness 如何模拟模型响应

test_codex 在底层使用 wiremock 来构造假模型响应:

rust 复制代码
pub async fn start_mock_server() -> MockServer {
    let server = MockServer::builder()
        .body_print_limit(BodyPrintLimit::Limited(80_000))
        .start()
        .await;

    // 提供默认的 /models 响应,确保测试自包含
    let _ = mount_models_once(&server, ModelsResponse { models: Vec::new() }).await;
    server
}

ResponseMock 会捕获所有请求,让你可以验证 agent 发送了什么:

rust 复制代码
let mock = mount_response_once(&server, response_template).await;
// ... 运行 agent ...
let req = mock.single_request();
assert!(req.body_contains_text("write a hello world"));

这意味着测试不仅能验证"agent 输出了什么",还能验证"agent 向模型发送了什么"------这对于调试提示词工程和工具调用顺序至关重要。


关键设计决策一:集成测试优先覆盖 Agent 变更

.codex/skills/code-review-testing/SKILL.md 明确规定:

"For agent changes prefer integration tests over unit tests. Integration tests are under core/suite and use test_codex to set up a test instance of codex." "Features that change the agent logic MUST add an integration test."

这不是建议,而是强制要求 。集成测试本身必须覆盖"主要逻辑变化和用户可见的行为"(Provide a list of major logic changes and user-facing behaviors that need to be tested)。Codex 的 contributing.md 也要求 invited PR 必须包含测试:bug fix 要有"改之前挂、改之后过"的测试覆盖。

为什么集成测试对 AI agent 更重要?因为 agent 的行为是涌现的(emergent):

  • 提示词工程的影响无法通过单元测试捕获
  • 工具调用的顺序和组合只有在完整流程中才能验证
  • 上下文窗口的管理(压缩、截断)是全局行为

单元测试验证"组件正确",集成测试验证"系统正确"。从 Codex 的实践来看,对于 AI agent,系统正确比组件正确更重要。


关键设计决策二:用 Snapshot 锁定 UI 与上下文形态

Codex 仓库里有 426 个 .snap 文件 ,用于 insta snapshot 测试。

Snapshot 测试解决了一个传统测试无法解决的问题:TUI 渲染输出的验证

当你修改了 TUI 的颜色、布局或文本时,传统的断言无法表达"渲染输出应该看起来怎样"。Snapshot 测试的做法是:

  1. 运行测试,捕获渲染输出的文本表示
  2. 新生成的输出与已批准的 snapshot 比较
  3. 如果不同,生成 .snap.new 文件
  4. 人工审查 .snap.new,确认变化符合预期
  5. 执行 cargo insta accept 批准新的 snapshot

一个真实的 .snap 文件长这样:

yaml 复制代码
---
source: tui/src/history_cell.rs
expression: rendered
---
╭───────────────────────────────────────╮
│ >_ OpenAI Codex (vtest)               │
│                                       │
│ model:       gpt-5   /model to change │
│ directory:   /tmp/project             │
│ permissions: YOLO mode                │
╰───────────────────────────────────────╯

这不仅是测试,更是代码审查的一部分。reviewer 不需要运行代码就能看到 UI 变化的效果------因为变化被捕获成了可 diff 的文本文件。

Codex 还扩展了 snapshot 的概念,实现了上下文 snapshot:把模型可见的上下文结构(消息序列、工具调用、输出)格式化为标准化的文本,验证"模型看到了什么"。例如,它可以验证在一次 agent turn 中,系统消息、用户输入、工具调用结果是否按预期顺序出现在上下文中------这对于调试上下文压缩和注入逻辑至关重要。


关键设计决策三:PTY 测试验证真实终端交互

TUI(文本用户界面)测试是最难做的测试之一。你需要:

  • 在没有真实终端的 CI 环境中运行
  • 验证光标位置、颜色、布局
  • 处理终端的交互式输入(键盘事件、鼠标事件)

Codex 的解决方案是 PTY(伪终端)测试

rust 复制代码
let mut pty = spawn_pty_process("codex", &["--some-flag"]).await;
// 发送输入
pty.write("hello\n").await;
// 读取输出并验证
let output = pty.read_until("prompt").await;
assert!(output.contains("expected text"));

关键技术点:

  • 在测试中 spawn 真实的 Codex CLI 进程
  • 模拟终端光标位置查询(ESC[6n),返回假位置(ESC[1;1R
  • 这样 TUI 可以在没有真实终端的环境下初始化

这不是 mock,而是真实进程在模拟环境中的运行。它验证了从 CLI 参数解析到 TUI 渲染的完整链条。


关键设计决策四:Docker 远程测试覆盖真实执行环境

Codex 的 exec-server 可以在远程环境中执行命令(如 Docker 容器)。本地测试无法覆盖这种场景,因为:

  • 远程执行涉及 WebSocket 通信
  • 沙箱行为在容器中和在本地不同
  • 权限和网络配置在远程环境中有差异

Codex 的解决方案是 scripts/test-remote-env.sh。核心逻辑如下:

bash 复制代码
# 构建 codex 二进制
cargo build -p codex-cli --bin codex

# 启动特权容器(bubblewrap 需要 mount propagation)
docker run -d \
  --name "${container_name}" \
  --privileged \
  --security-opt seccomp=unconfined \
  ubuntu:24.04 sleep infinity

# 在容器中启动 exec-server,暴露 WebSocket
docker exec "${container_name}" sh -lc \
  "nohup ${remote_codex_path} exec-server --listen ws://0.0.0.0:31987 > /tmp/exec-server.stdout 2>&1 &"

# 测试通过环境变量连接
export CODEX_TEST_REMOTE_EXEC_SERVER_URL="ws://${container_ip}:31987"

这个脚本不仅在 CI 中运行,也可以在本地开发时通过 source scripts/test-remote-env.sh 运行。它让开发者可以在与生产环境接近的条件下测试远程执行功能。


关键设计决策五:主动管理 Flaky 测试生命周期

集成测试比单元测试更容易出现 flaky(不稳定)的情况,因为:

  • 异步操作的时序不确定
  • 外部依赖(网络、文件系统)有噪声
  • 多线程竞争条件

Codex 有三层 flaky test 管理策略:

第一层:标记和隔离

  • #[ignore = "flaky"] 标记已知不稳定的测试
  • nextest.toml 中用 test-groups 序列化有资源竞争的测试组:
toml 复制代码
[test-groups.app_server_integration]
max-threads = 1

[[profile.default.overrides]]
filter = 'package(codex-app-server) & kind(test)'
test-group = 'app_server_integration'

第二层:超长超时容忍

  • rust-ci-full.yml 的 tests job 使用 45 分钟超时,给大型集成测试足够的时间(其他 job 为 30 分钟)
  • nextest.toml 中的 slow-timeout 会在测试卡住时自动终止:连续 15 秒无进展则警告,警告 2 次后(累计 30 秒)强制终止

第三层:PR Babysitter 诊断

  • babysit-pr skill 会自动分析 CI 失败
  • 区分"分支相关失败"(代码问题,需要修复)和"flaky 失败"(环境问题,需要重试)
  • 对 flaky 失败通过 python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr auto --retry-failed-now 重跑失败 job

这不是"接受 flaky 测试存在",而是主动管理 flaky 测试的生命周期


核心洞察:测试是行为契约,不是覆盖率数字

Codex 的测试实践体现出的哲学是:测试不是为了证明代码没有 bug,而是为了锁定行为契约

.snap 文件锁定的是"UI 应该长什么样"。test_codex harness 锁定的是"agent 在面对这个输入时应该做什么"。Docker 远程测试锁定的是"远程执行在这个环境下应该成功"。

这些测试的价值不在于覆盖率百分比,而在于它们捕获了无法被单元测试表达的系统行为

对于 AI 原生系统,这个洞察尤为重要:

  • 模型行为是非确定性的,但系统行为应该是有边界的
  • 你无法测试"模型会输出什么",但可以测试"无论模型输出什么,系统都会正确路由"
  • 你无法测试"上下文压缩后模型是否理解",但可以测试"压缩后的上下文结构是正确的"

可学习点

  1. 集成测试优先:对于 agent/AI 系统,验证完整交互流程比验证单个函数更重要
  2. 投资测试基础设施test_codex harness 让写集成测试和写单元测试一样简单,这是集成测试能被广泛采用的关键
  3. Snapshot 测试捕获 UI 行为:把"看起来对"变成"机器能验证"
  4. 真实环境测试:用 PTY 测试 TUI,用 Docker 测试远程执行------不要依赖 mock
  5. 主动管理 flaky 测试:标记、重试、自动诊断,而不是忽视
  6. 关键判断标准:当某类 bug 反复在"集成环境"中出现,但在单元测试中无法复现时,就需要新增一种集成测试
相关推荐
hunteritself1 小时前
GPT Image2 + Seedance 2.0:3 小时从剧本到 AI 互动影游,深度实测复盘
前端·数据库·人工智能·深度学习·transformer
jedi-knight1 小时前
Vibe SRM:用自然语言设计固体火箭发动机,AI做到了
人工智能·经验分享·agi
java1234_小锋1 小时前
Spring AI 2.0 开发Java Agent智能体 - 对话与提示词工程(Prompt)
java·人工智能·spring
伊玛目的门徒1 小时前
用 npm 安装 Claude Code CLI 并对接 DeepSeek API 经验分享
人工智能·npm·大模型·ai编程·deepseek·claude code
xiaoduo AI1 小时前
智能客服机器人能实时监控会话风险规避服务纠纷吗?能规范服务话术守住门店口碑吗?
大数据·人工智能·机器人
逻辑君1 小时前
认知神经科学研究报告【20260033】
人工智能·机器学习
l1t1 小时前
DeepSeek总结的Delta 成长记:写入、Unity Catalog 和时间旅行
数据库·人工智能·unity
飞Link1 小时前
从 Promptbreeder 到 EvoPrompt:深度解析进化 AI (eAI) 的提示词自动优化策略
大数据·人工智能