pgtestdb 模板克隆让测试快如闪电

Pgtestdb's template cloning approach to testing is fast

pgtestdb 模板克隆让测试快如闪电

昨天被 Cup o’ Go 提醒后,我重新审视了 Peter Downs 开发的 pgtestdb 这个 Go/Postgres 测试包。它利用 Postgres 内置的 template databases 功能,通过克隆模板数据库来替代从零迁移,速度极快。我将 pgtestdb 集成到 River 的测试套件中进行对比,发现虽然单次克隆耗时约 100 毫秒,与基于 schema 的方法相近,但整体测试套件运行时间却慢了 3.5 倍。这是因为 River 现有的 schema 方案实现了巧妙的资源复用:当测试用例完成时,空闲的 schema 会被清理并重复使用,将单次设置时间压缩至 10-20 毫秒。对于拥有上万测试用例的大型应用,这种复用策略比单纯依赖快速克隆更为关键。

100 毫秒来引导测试数据库已经很快了,但如果你正在构建一个拥有 10,000 个测试的完整应用程序,理想情况下你希望测试设置速度快 10 倍。
  1. TexanFeller

    长期以来,我在对 Postgres 进行测试时一直采用类似的方法,尤其是配合 Testcontainers 等工具使用时。多年来,这为我节省了海量的等待测试运行的时间,而且实现起来极其简单,即便是手动操作也是如此。

    1. 基于我们生产环境 Postgres 的版本设置一个容器镜像,并预应用当前的生产迁移脚本。

    2. 配置 Testcontainers 以便在测试之间复用容器,至少在同一文件内的测试可以复用。

    3. 大约 25 行初始化代码,在第一次测试时将本地分支的迁移脚本应用到模板数据库,并将测试数据复制进去。

    4. 当模板数据库已存在时,测试设置只需删除数据库,然后从模板数据库重新复制一份。

    我发现那些真正执行操作的测试能捕获多得多的 bug,而且比那些大量使用 mock 或类“测试实现”的代码更容易搭建。只要稍加注意,性能损耗微乎其微,完全物超所值。

  2. peterldowns

    只是想感谢 Brandur 查看 pgtestdb 并如此彻底地对其进行基准测试。我很高兴它的表现如此出色,接下来我会投入一些算力和脑力,尝试实现一种机制:对成功的数据库进行清理、复用和池化,而不总是将其销毁。

    下面的一些讨论中存在一些混淆——

    pgtestdb 只是一个原始工具,用于“快速为我的测试提供一个干净的数据库”。既然你的 Postgres 运行在 ramdisk 上,我认为没有比这更快的方法能生成一个干净且已完全迁移的数据库了——而且你的迁移脚本只运行一次,无论你有多少个测试进程并发运行,或者这些进程中有多少个测试在并行执行。

    你实际上可以将它与测试事务结合使用,你可以对数据库做任何你想做的事!碰巧的是(根据我的经验),它的速度足够快,可以“为每个测试单独提供一个数据库”,即便测试数量相当大。

    AI 带来的一个真正酷的好处是,它使得像这样的实验成为可能,而过去这些实验因耗时过长而难以进行。再次感谢 Brandur。

  3. rgbrgb

    在过去几年里,我在测试中一直采用一种“脏数据库”的方法:针对单个基于 Postgres 的后端运行并行集成测试,测试之间不进行任何清理。每个测试都使用唯一的 ID 访问同一个数据库。限制在于你不能断言精确的数量,你需要断言特定的 ID 是否存在。在这种设置下,数据库只需初始化一次,所有测试即可并行运行。集成点是我在生产环境中看到故障最多的地方,所以我喜欢用真实的数据库进行测试。并行化让测试变得飞快,顺便这种方案有时还能捕获那些只有在并发生产请求中才会出现的竞态交互(即潜在的 Heisenbug)。许多测试并行访问 API 最终能更好地模拟生产行为。

  4. tux3

    如果你的测试不需要 SET LOCAL 或不同的隔离级别,根据我的经验,最快的方法无疑是每个进程拥有一个模板数据库(也许先迁移一次,然后将其作为模板来创建副本),然后将你的测试包裹在一个事务中,在结束时回滚。

    Postgres 的回滚几乎是瞬间完成的,因为所有的清理工作都留给了稍后的 vacuum 处理。Postgres 支持“嵌套事务”(savepoints),所以在大多数情况下你无需修改代码。

    对于那些无法被事务包裹的小部分测试,你可以在每个进程中串行运行它们,并在中间通过 DELETE 或 TRUNCATE CASCADE 进行清理。DELETE 稍快一些,但你需要自己处理外键问题。

    依赖事务回滚时出错的概率会更高。但在速度方面,我不知道有比这更快的方法了。

  5. pmontra

    我接手的一个 Rails 项目的原始开发者决定用 db/seed.rb 填充测试数据库,并将每个测试都绑定到 seed 的内容上。测试数量很多,所以重写它们并非立即可行的选项。

    我用标准方式编写了新测试,但我仍然面临一个问题:即使只运行单个测试,也要花费大量时间在填充数据库上。我写了几段脚本,在测试结束时(如果尚不存在转储文件)将数据库转储出来,并在测试开始时重新加载。这样快多了。不过,当我切换分支时,我仍然需要清空测试数据库并重新填充,因为我并没有按分支维护转储列表。

    也许我可以创建一个包含数据的模板。或者,最终还是得把每一个旧测试都重写一遍。

同日更多故事

2026-08-01