超SSS级开发者会写测试

如何让一个报表 SQL builder 变得值得信赖

李昇宰 · NHN AD
平台服务实验室 · 2026 年 7 月 · 约 8 分钟

写在前面

大家好。我是想成为超SSS级开发者的李昇宰。这篇文章记录的是,如何让一个用来生成广告报表的 SQL builder 变得值得信赖。

自然语言如何变成报表

我负责的服务里有一个用 SQL 生成报表的 builder。当用户用自然语言提出请求,比如"给我看上个月各活动的转化数",AI 智能体会解析这句话,把维度、指标、时间段、过滤条件抽取成参数。builder 接过这些参数,按子句逐段拼装出要在 Athena 上运行的 SQL,再把执行结果返回成报表。一句自然语言变成 SQL,随即变成广告主看到的数字。

自然语言问题 维度·指标·时间段·过滤 AI 智能体 参数抽取·归一化 SQL builder 按子句拼装 按子句分别生成再拼装 SELECT GROUP BY WHERE HAVING ORDER BY LIMIT Athena 执行·缓存 报表 返回数字
自然语言请求经 AI 智能体拼装成 SQL,在 Athena 中执行后变成报表

问题在于,这类代码就算错了也看不出来。只要不是 SQL 语法崩掉,查询照样跑得好好的,表格也填得漂漂亮亮。哪怕营收被重复统计、翻了一倍,也没人察觉,只有数字在悄悄出错。

报表结果 查询成功 ✓ 项目 营收 实际营收 ¥1,000,000 报表营收 ¥2,000,000 重复统计 ×2 SQL 语法没问题——只有值在悄悄出错 报表营收为什么 跟实际对不上? 广告主
SQL 执行成功,营收却翻了一倍——出错的不是语法,而是悄悄错掉的值

所以我开始琢磨,怎样才能用测试让这个 builder 变得可信。这篇文章就是这段琢磨的记录。

关于测试的思考

在我看来,测试大体分两类:单元测试和集成测试。如果问这两者哪个更重要,我会回答两者都重要,因为它们的目的各不相同。

单元测试是对契约的验证。它确认"给定这个输入就返回这个输出"的契约,也就是规格。它不牵动真实基础设施,成本低、速度快,但那套环境和真实环境不同。生产环境里会调用基础设施,模块之间会协同,还要经网络向外走。单元测试却不是这样。而且单元测试大多注定会通过——毕竟一开始就是为了确认规格,才把成功用例和失败用例分开、用 mock 造出来的。所以单元测试的作用在于验证规格,以及在改动代码时捕捉变更的扩散。

相比之下,集成测试尽管程度有别,却力求尽可能贴近真实的生产环境。它能测出真实行为,但也因此更重,某些情况下还会产生费用。

真实代码要走向生产,集成测试必不可少。而且这两者必须始终一起开发,测试代码才真正物有所值。

单元测试 契约 · 规格验证 · 用 mock 分开成功·失败 · 无需基础设施、成本低 · 跑得快 捕捉变更的扩散 集成测试 贴近生产 · 调用基础设施 · 模块之间协同 · 经网络向外走 走向生产的必要条件 两者始终一起开发,测试才物有所值
单元测试廉价地确认契约,集成测试确认生产中的协同 · 两者相辅相成

没有数据怎么测

真要动手测时才发现有个问题。S3、Athena 这类 AWS 基础设施都有,可偏偏在它们之上没有可供测试用的数据。

于是我从 mock 数据入手。造一小份假数据放进去,先用 pandas 把这些值算好核对一遍,再把同样的输入喂给 builder,把它生成的 SQL 的结果和我算好的对照。这种不用 builder 去核验 builder 自己吐出的数字、而是拿一份独立计算来对照的做法,叫作测试预言机。两条路径完全独立,逐个单元格比对,只要有一格对不上就判失败。

builder 的 SQL 真实 Athena 执行 原始数据 pandas 独立重算 结果 A 结果 B 逐单元格比对 0 处不一致 = 通过
必须另有一份知道正确答案的独立计算,验证才成立 · 两条路径连每一个单元格都相同才算通过

这一阶段我也查看了数据的一致性,但主要想确认的是规格:SQL 会不会崩、builder 有没有按条件正确分支、有没有选对表。我想验证的是 builder 逻辑吐出的 SQL 的形状。

等真实数据落地的那一刻,只要再造一个新的测试预言机就好。所幸基于真实数据它也验证得很好,但很快又撞上了另一个问题。需求总在变,bug 总会被发现。每逢此时就要重构代码、重新整备测试,这个过程实在太重复、太枯燥了。

怎么可能每次都靠人做

设计测试时,我看重三件事。第一,无论何时运行都能复现。第二,改了什么、为什么改,都能追溯。第三,这两件事不靠人手,而是自动运转。

01 复现 固定数据集 回归测试 脚本永久化 何时运行结果都一样 02 追溯 叙事文档 测试集文档 append only 留下改了什么、为什么改 03 自动化 测试智能体 技能 重复同一套流程 无需人手自行运转
复现 · 追溯 · 自动化——测试设计中抓住的三件事

第一条原则,复现。把用于验证的数据集固定下来,在它之上每次重跑全部用例。每当动到 builder 逻辑,就对同一份数据抛出同一批用例,确认从前对的现在是否仍然对。这算是一种回归测试。验证脚本也在仓库里永久保存,光凭文件名就能把当时那一轮原样重跑一遍。

第二条原则,追溯。每次跑测试都留下验证文档。一份是叙事:目标、数据来源、算式、每一轮的结果、失败时的原因与修复过程。另一份是测试集:迄今跑过的 SQL 全文与各自结果、复现方法。两份都不删除,只做 append。

验证文档 · 叙事 目标 · 数据来源 · 算式 每轮结果与判定 失败原因 · 自行修复过程 append only SQL 文档 · 测试集 跑过的 SQL 全文 各自结果 复现方法 append only R1 R2 R3 R9 轮次越积越多,文档也一同累积
叙事与测试集不删除、只累积 · 光凭文件名就能重跑当时那一轮

第三条原则,自动化。到这一步,不可能每次都由人手工来做。于是我造了SQL builder 专用的测试智能体与技能。智能体铺开用例,技能每次都按同一顺序走完验证流程。复现和追溯不再靠人的意志,而成了工具的默认动作。

① 预言机重算 S3 原始数据 → pandas 独立重算 verify_core.py · 不依赖代码 ② 产出物执行 sql_builder → Athena 执行 结果复用缓存 ③ 逐单元格比对 键 1:1 · 三轴 值 · CASE 解析 · 列顺序 ④ 回归套件 每次 QA 追加专用轮次 重跑整套套件 ⑤ append 文档 验证.md 叙事 · SQL.md 测试集 · QA 追踪 每次改动 sql_builder 都重跑整套套件 · 套件随每次 QA 增长
验证技能结构——把独立预言机与 builder 执行逐单元格比对,每次 QA 追加专用轮次、以回归重跑整套,并把结果 append 到文档

写在最后

写测试这件事,似乎总比实现更难。一份写得好的测试,光看它就能推断出原本的代码,它本身就是可执行的规格

如果非要给开发者的境界分个高下,我想,能写出有意义的测试代码的开发者,才正是前面说的那种超SSS级开发者吧。

正因如此,最近我常常在想:该如何测试,又该如何验证。

谢谢阅读。

参考