如何让一个报表 SQL builder 变得值得信赖
大家好。我是想成为超SSS级开发者的李昇宰。这篇文章记录的是,如何让一个用来生成广告报表的 SQL builder 变得值得信赖。
我负责的服务里有一个用 SQL 生成报表的 builder。当用户用自然语言提出请求,比如"给我看上个月各活动的转化数",AI 智能体会解析这句话,把维度、指标、时间段、过滤条件抽取成参数。builder 接过这些参数,按子句逐段拼装出要在 Athena 上运行的 SQL,再把执行结果返回成报表。一句自然语言变成 SQL,随即变成广告主看到的数字。
问题在于,这类代码就算错了也看不出来。只要不是 SQL 语法崩掉,查询照样跑得好好的,表格也填得漂漂亮亮。哪怕营收被重复统计、翻了一倍,也没人察觉,只有数字在悄悄出错。
所以我开始琢磨,怎样才能用测试让这个 builder 变得可信。这篇文章就是这段琢磨的记录。
在我看来,测试大体分两类:单元测试和集成测试。如果问这两者哪个更重要,我会回答两者都重要,因为它们的目的各不相同。
单元测试是对契约的验证。它确认"给定这个输入就返回这个输出"的契约,也就是规格。它不牵动真实基础设施,成本低、速度快,但那套环境和真实环境不同。生产环境里会调用基础设施,模块之间会协同,还要经网络向外走。单元测试却不是这样。而且单元测试大多注定会通过——毕竟一开始就是为了确认规格,才把成功用例和失败用例分开、用 mock 造出来的。所以单元测试的作用在于验证规格,以及在改动代码时捕捉变更的扩散。
相比之下,集成测试尽管程度有别,却力求尽可能贴近真实的生产环境。它能测出真实行为,但也因此更重,某些情况下还会产生费用。
真实代码要走向生产,集成测试必不可少。而且这两者必须始终一起开发,测试代码才真正物有所值。
真要动手测时才发现有个问题。S3、Athena 这类 AWS 基础设施都有,可偏偏在它们之上没有可供测试用的数据。
于是我从 mock 数据入手。造一小份假数据放进去,先用 pandas 把这些值算好核对一遍,再把同样的输入喂给 builder,把它生成的 SQL 的结果和我算好的对照。这种不用 builder 去核验 builder 自己吐出的数字、而是拿一份独立计算来对照的做法,叫作测试预言机。两条路径完全独立,逐个单元格比对,只要有一格对不上就判失败。
这一阶段我也查看了数据的一致性,但主要想确认的是规格:SQL 会不会崩、builder 有没有按条件正确分支、有没有选对表。我想验证的是 builder 逻辑吐出的 SQL 的形状。
等真实数据落地的那一刻,只要再造一个新的测试预言机就好。所幸基于真实数据它也验证得很好,但很快又撞上了另一个问题。需求总在变,bug 总会被发现。每逢此时就要重构代码、重新整备测试,这个过程实在太重复、太枯燥了。
设计测试时,我看重三件事。第一,无论何时运行都能复现。第二,改了什么、为什么改,都能追溯。第三,这两件事不靠人手,而是自动运转。
第一条原则,复现。把用于验证的数据集固定下来,在它之上每次重跑全部用例。每当动到 builder 逻辑,就对同一份数据抛出同一批用例,确认从前对的现在是否仍然对。这算是一种回归测试。验证脚本也在仓库里永久保存,光凭文件名就能把当时那一轮原样重跑一遍。
第二条原则,追溯。每次跑测试都留下验证文档。一份是叙事:目标、数据来源、算式、每一轮的结果、失败时的原因与修复过程。另一份是测试集:迄今跑过的 SQL 全文与各自结果、复现方法。两份都不删除,只做 append。
第三条原则,自动化。到这一步,不可能每次都由人手工来做。于是我造了SQL builder 专用的测试智能体与技能。智能体铺开用例,技能每次都按同一顺序走完验证流程。复现和追溯不再靠人的意志,而成了工具的默认动作。
写测试这件事,似乎总比实现更难。一份写得好的测试,光看它就能推断出原本的代码,它本身就是可执行的规格。
如果非要给开发者的境界分个高下,我想,能写出有意义的测试代码的开发者,才正是前面说的那种超SSS级开发者吧。
正因如此,最近我常常在想:该如何测试,又该如何验证。
谢谢阅读。