聊聊测试自信
在长期构建可被他人依赖的软件系统的过程中,我逐渐意识到,测试并不是一个单纯的质量保证工具,而是一种在维护者与用户之间建立信任边界的工程手段。
当一个系统缺乏这种边界时,复杂度并不会消失,而是会转移到维护者的判断、用户的使用成本,或双方的协作摩擦中。
对维护者来说,测试决定了系统是否还能被持续演进。 对用户来说,测试决定了系统是否值得被依赖。
而所谓“测试自信”,并不是来自某个测试指标,而是来自这两种视角是否通过测试被清晰地对齐。
测试并不等价于覆盖率
在工程实践中,测试常常被简化为一些指标:覆盖率、CI 通过率、回归数量。 这些指标对维护者而言确实有直接价值,它们降低了修改代码时的不确定性。
但这些指标本身,并不能回答用户真正关心的问题:
当系统被放入真实环境、真实负载和真实依赖中,它的行为是否是可预期的?
系统一旦被他人使用,其使用方式几乎一定会偏离设计者的预期。 测试的价值,并不在于穷举所有路径,而在于通过一组可执行的用例,把系统的预期行为和边界条件显式固定下来。
当这些边界是清晰的:
- 维护者知道哪些行为不能被破坏。
- 用户知道哪些行为可以被依赖。
从这个角度看,测试并不是代码的附属物,而是连接维护者与用户的一层结构。
测试是在对语义做长期约束
在 Databend 的实践中,我们大量依赖现有数据库系统中已经被验证过的测试用例,并对关键基准进行长期跟踪,以此来固定实现对既有语义和标准的承诺。
一个重要的事实是: 系统复杂度的主要来源,并不是实现难度,而是语义在时间维度上的稳定性。
例如:
- 某些异常条件下应返回结果还是错误。
- 不同执行路径是否满足相同的不变量。
- 一些并不优雅、但被大量使用的行为是否需要长期兼容。
如果这些语义没有通过测试被固定下来,对维护者而言,它们会变成一种隐性负担。 每一次修改,都需要依赖记忆和经验来判断“这会不会破坏什么”。
而对用户来说,语义漂移带来的风险更加直接。 同样的用法,在不同版本中可能产生不同结果。
因此,测试在这里承担的角色,并不是“检查代码有没有 bug”,而是:
- 为维护者提供清晰的演进边界。
- 为用户提供稳定的行为预期。
这也是为什么像 MySQL、SQLite 这样的系统,会长期将测试视为高价值资产——因为测试本身承载的是对用户的长期承诺。
测试是在协调维护者判断与用户现实
在 Apache OpenDAL 的 CI 中,囊括了数百种不同的测试,提供对多平台和不同服务、binding 下的全面测试。 事实上,单一贡献者很难有精力自主筹备完整测试环境所需的一切依赖和服务。
如果说 Databend 的测试主要解决的是系统内外部语义的稳定性,那么在 Apache OpenDAL,测试更多是在解决维护者判断与用户现实之间的落差。
在对象存储生态里,很多服务都会声称自己是“S3 compatible”。 对维护者来说,只要实现了规范,就很容易形成一种假设: 系统在工程上“应该是可用的”。
但用户并不会在规范层面使用系统,他们只会在真实账户、真实服务、真实失败模式中使用它。 而大量问题,正是在这种假设与现实的落差中出现的。
因此,在 OpenDAL 中,我们始终坚持:
- 尽可能使用真实服务进行测试。
- 不把文档和兼容性声明当作工程事实。
- 将失败用例长期保留,作为回归测试的一部分。
这类测试的价值在于:
- 它约束了维护者对外部世界的过度乐观判断。
- 也为用户提供了一个明确的信号: 哪些行为是已经在现实环境中被验证过、可以依赖的。
在这里,测试并不是为了“证明实现正确”,而是为了让维护者的判断与用户的现实使用保持一致。
测试自信:让双方对“失败”有一致理解
在这些实践中,我逐渐形成了一个比较稳定的判断:
测试自信,并不是系统“很少出问题”,而是当问题出现时,维护者和用户对问题的性质有一致的理解。
当某种行为被测试覆盖时:
- 对用户而言,失败意味着 bug。
- 对维护者而言,修复是明确的工程责任。
而当某种行为从未被测试:
- 对用户而言,它就不应被视为系统承诺。
- 对维护者而言,也不应被默认为必须长期兼容的行为。
测试让这种区分变得清晰、可执行、可回归,也显著降低了双方在预期上的摩擦成本。
结语
从工程角度看,测试并不是只为维护者服务的工具,也不是只为用户提供保障的机制,而是让双方在同一套行为边界上达成共识的结构。
它把隐含假设变成显式约束,把个人经验变成可以延续的工程边界。
对我而言,无论系统处在哪一层,测试最终都服务于同一件事:
让系统在被依赖时是可预期的, 让维护在长期演进中是可持续的。