原子正确性与单测
目录
俗话说,“40% 的时间写代码,60% 的时间 Debug。”经验表明,调试才是开发过程中真正的时间黑洞。在 AI 辅助编程日益普及的背景下,这一问题更加突出。AI 能加速代码生成,但处理复杂任务时难免出错,而这些错误更隐蔽且难调试。因此,我们比以往更需要依靠单测这套“原子级正确性保障”体系,确保代码质量和功能可靠性。
单元测试的首要作用是提升开发体验——**减少开发过程中的打断,缩短调试时间,**从而自然地提高项目的整体质量。
那么,单元测试为什么能够减少打断并缩短开发时间呢?关键在于它提供了更短的开发 - 验证循环。
1、提高开发体验的核心:更短的反馈链路
单测能够提升开发体验的核心在于它提供了更短的开发 - 验证的反馈循环。
在传统开发流程中,从修改代码到验证结果通常需要经过以下步骤:
尤其是在 UI 开发中,随着热更新技术的广泛应用,步骤 2 和 3 的耗时大大缩短,也让开发者更倾向于依赖上述验证流程。
然而,即便如此,这个过程仍可能耗费几分钟甚至更长时间,因为我们需要依赖整个应用的最终表现来验证某个函数的正确性。
而通过单元测试,这一反馈循环可以缩短至几秒钟:
这种即时反馈不仅大幅节省时间,更关键的是,它有效地保持了我们的思维连贯性(减少打断):无需在冗长的验证流程中分散注意力,而是能将精力集中在问题本身。
然而,即便即时反馈提升了开发体验,开发流程中更大的挑战往往出现在问题被发现之后——也就是 Debug 阶段。
2、缩短开发时间的核心:“原子正确性”
单测的一个重要作用是让我们确信:**这个函数肯定没问题!**我将这种能力称为“原子正确性”。
在 Debug 时,我们希望问题的定位范围越小越好。
如果没有单测,最糟糕的情况是整个应用都可能成为 Debug 范围;
而在有单测的情况下,Debug 的重点就只需放在**函数之间的组合逻辑****,**因为单测为我们提供了“原子正确性”的保障。
单测通过提供“原子正确性”,显著缩小了调试范围:由语句级提升到函数级。
每个通过单测验证的函数(绿色节点)都成为可靠的构建块,我们只需关注这些构建块之间的组合是否正确。
3、AI 下单测的意义:分治
如果你研究过 prompt,就会发现一个高频出现的 magic word:step by step。
AI 在处理长任务时更容易出错,因此我们需要将长任务拆解为小任务(或者让 AI 自动拆分),并按照以下流程执行:
小任务 -> 检查 -> 小任务 -> 检查。
在各种 copilot 的辅助下,开发者往往不需要亲自写出函数的每一行代码,而是通过注释或指令完成大部分工作。
然而,正因为代码不是完全由我们编写,一旦出错,Debug 会变得更加困难。
在这种情况下,快速失败(fast fail)尤为重要。明智的做法是让 AI 自我检测:自动生成覆盖各种边界情况的单测。
通过一连串由“原子正确性”保障的小任务,才能显著提升长任务的整体正确性。
基于 AI 构建工具应用,面临的核心难题是:**如何在非确定性模型上构建表现稳定的工具。**其解决策略与此相同——将复杂任务拆解为易控的小任务,并为每一步提供可靠的验证机制。
4、为什么依然不写单测:单测的冷启动
很多情况下,开发者并非不愿意写单测,而是因为想到需要先搭建一系列环境和配置,就直接放弃了。
我将这种现象称为单测的“冷启动”问题。
在实际项目中,**单测更常见于服务端开发中,**主要原因有以下两点:
-
**运行整个应用验证的成本更高:**服务端通常依赖外部资源,如 RPC 调用、数据库、缓存等。尤其在微服务架构下,完整、可重复地运行整个应用进行验证往往并不现实。
-
**语言内置测试工具:**大多数服务端语言自带轻量级测试框架,开发者几乎无需额外配置,便能快速上手,避免了冷启动问题。
只有彻底消除冷启动,才能让编写单测变得轻而易举、自然而然。
下面,我将分享一些具体的实践经验,帮助消除前端开发中的“冷启动”问题,让单测成为随手可用的开发利器。
5、实践分享:拿来即用的干货
5.1 怎么写单测可以获得最短反馈路径
-
紧接着被测试函数写测试;
-
开箱即用的单文件(ts)运行测试;
紧邻被测试函数编写测试
将单测直接写在靠近函数实现的位置,不仅可以节省上下文切换时间,还能让单测发挥另一层作用:函数的“注释”。
例如,在 Python 中,可以直接在函数注释中编写测试。而在 TypeScript 中,虽然没有完全类似的机制,但可以通过以下方式在当前文件中快速验证:
if (require.main === module) {
(() => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const pbStr = fs.readFileSync('test/proto/tolstoyUserProto.proto', 'utf-8')
try {
const result = pb2mockData({
pbStr,
serviceName: 'Config',
methodName: 'FormatLabels',
})
console.log('result', result)
} catch (error) {
console.log('e', error)
}
})()
}
这种方式能够让你在完成函数实现后,随手写一个测试,使用命令 npx ts-node src/…
即可快速验证。
只需关注测试本身,无需关注运行环境和配置
许多语言提供内置测试框架,例如 Go:
calculator.go
// 只需要遵循约定创建
calculator_test.go
在这种约定下,calculator_test.go
自动成为 JS 中 if (require.main === module)
的对应实现:
-
测试文件与源码文件共享上下文,测试代码与实现逻辑紧密结合;
-
无需任何额外配置,直接运行
go test
即可验证代码。
5.2 TypeScript 中推荐的测试框架:Vitest
Vitest 是一个适合 TypeScript 项目的测试框架,提供了即开即用的体验,尤其是在“单测冷启动”问题上优势明显:
1、支持直接在源码文件写测试:In-Source Testing(https://vitest.dev/guide/in-source.html)
// src/index.ts
// the implementation
export function add(...args: number[]) {
return args.reduce((a, b) => a + b, 0)
}
// in-source test suites
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('add', () => {
expect(add()).toBe(0)
expect(add(1)).toBe(1)
expect(add(1, 2, 3)).toBe(6)
})
}
如果测试用例较多,也可以像 Go 那样,在源码文件旁边创建测试文件,例如:index.test.ts
,比如:
2、TS 开箱即用,无需额外配置
与 Jest 或 Mocha 等框架相比,Vitest 避免了复杂的配置过程,不再需要为 TypeScript 测试环境单独配置编译或运行时环境。也就是之前提到的单测冷启动。
使用 Vitest,你只需要:
npx vitest xxx.test.ts
即可轻松运行测试,完全消除了“单测冷启动”的障碍。
6、总结:双赢的策略
通过解决单测的“冷启动”问题并缩短反馈链路,我们可以从根本上降低编写测试的阻力,将 写测试从短期与长期的权衡,转变为真正的双赢策略:
-
**短期:**for 开发,用更少的时间做更多的事情;
-
**长期:**for 项目,确保质量的稳定性,避免劣化。
当编写单测不再是一种负担,而是一种自发行为时,项目迭代将进入正向循环:持续的效率和质量提升;
特别是在 AI 辅助编程的背景下,迭代速度大幅加快,正反馈的作用更为突出。通过单测保障每一步的正确性,开发者不仅能够驾驭复杂任务,还能进一步释放生产力,实现真正的快速迭代和高质量交付。
-End-
原创作者|付志远
本文收藏来自互联网,仅用于学习研究,著作权归原作者所有,如有侵权请联系删除,阅读原文。
9ong@TsingChan markdown 2025
部分引用格式为收藏注解,比如本句就是注解,非作者原文。