测试驱动开发有两大门派,但大多数人入行时根本没意识到自己选了边。快速回顾一下:伦敦派(mockist)通过模拟协作者、断言交互来驱动设计;芝加哥派(classicist)则从内部向外构建,使用真实对象,断言值和状态。
我坚定地站在芝加哥派这边。原因如下,以及它为何与六边形架构完美契合。
![]()
模拟只属于边界。仅此而已。
![]()
我并不反对模拟。模拟在跨越你无法控制或不想在测试中触达的边界时非常好用:数据库、HTTP API、消息队列、时钟、文件系统。任何替代方案会导致缓慢、不稳定或产生无法撤销的副作用的场景。
边界之内?用真实对象。如果两个领域类需要协作,就让它们协作。把它们构建起来,调用方法,断言输出结果。这就是测试。
一旦你开始模拟自己的内部类型,你就不是在测试系统,而是在测试你对系统应该如何自我对话的假设。这是截然不同的两件事。
模拟掩盖了真正的问题
当你模拟一个协作者时,你是在手写你期望它返回的内容。那个模拟会永远快乐地返回你告诉它的任何东西,即使真实对象的契约在三次重构前就已经变了。你的测试保持绿色。生产环境崩溃。
真实对象不会放过你。如果协作者的行为变了,使用它的测试会察觉,因为它实际上在运行完整的流程。你得到一个早期、诚实的信号。模拟给你的却是舒适、虚假的信号。
好的测试不关心实现。模拟强迫你关心。
![]()
这是最让我困扰的部分。在我看来,测试的全部意义就是"给定这个输入,我期望这个输出(或这个状态变化)"。仅此而已。我不需要知道,也不关心代码如何到达那里。这种自由正是重构安全的保障。
重度模拟的测试摧毁了这一点。现在你的测试在断言类似"服务恰好调用了一次repository.findById并传入这个参数,然后调用mapper.toDto,然后……"的内容。你把实现细节烘焙进了测试。哪怕行为完全一致,只要你重组内部结构,测试就会爆红。这不是有用的信号。这是摩擦。
更糟的是:在严格模拟下,你甚至不需要正确实现功能。只要调用匹配预期,测试就通过。你可以满足契约而不遵守契约。我觉得这 genuinely 令人不安。测试不是在证明代码有效;它只是在证明代码打了正确的电话。
使用真实对象。其余用伪造。
我的默认策略是:能用到真实对象的地方都用真实对象。当我无法做到时(还是边界问题),我优先选择伪造而非模拟。
伪造是一个真实、可工作的实现,只是更简单。一个用Map而非Postgres存储的内存仓库。一个把邮件追加到列表而非真正发送的邮件发送器。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.