deep-dives

19 posts

你的架构图早就是谎言了

架构文档在你保存的那一刻就开始腐烂。以下是如何利用代码生成图表、ADR 和自动化架构测试来保持文档的诚实。

我在 wiki 里见过的每一张架构图都是错的。不是那种惊天动地的错,而是悄无声息、日积月累的错。标着 "Auth" 的服务六个月前就被拆成了三个微服务。标着 "sync call" 的箭头现在已通过队列变成了异步调用。标着 "PostgreSQL" 的数据库在一次紧急故障处理中被迁移到了别的系统,却没人更新那个方框。…

你的重试循环假设第一次请求失败了。大概率并没有。

超时或崩溃并不意味着你的 API 请求已经丢失。本文介绍幂等键(idempotency key)如何让重试变得安全,以及真正防止重复请求的数据库存储模式。

你的服务在处理 请求时中途崩溃。客户端看到超时,于是重试。结果出现了两笔扣款。客户很生气。数据库是一致的。但业务逻辑不是。 这不是边缘情况。这是分布式系统的默认行为。网络丢包。容器在请求中途被 OOM 杀掉。负载均衡器对已经到达后端的请求返回 502。如果你的 API…

比进程更长寿的锁:分布式租约的真实工作原理

内存中的互斥锁在服务器重启后随即消失。本文介绍带有 fencing token 和 TTL 的分布式租约如何防止崩溃后的重复执行,以及它们在哪里仍然会失效。

你的 无法挺过 。它也无法挺过 OOM、部署滚动更新或节点重启。进程退出的瞬间,锁就消失了。如果这个锁正在保护一个定时任务、数据迁移或主节点选举,那么你现在会有两个进程都坚信自己是唯一在运行的那个。 这不是你的互斥锁有 bug。这是一个范畴错误。进程本地的锁无法保护集群范围的资源。…

无 goroutine、无定时器、无后台开销的熔断器

大多数熔断器库都会启动后台线程来探测恢复。你根本不需要它们。本文介绍一种请求驱动的设计,在消除所有后台开销的同时,不牺牲正确性。

我审查过的每一个生产环境熔断器,最终都会拉起一条后台线程。它可能是 Go 的 goroutine、Java 的 ,或是 Rust 的 tokio task。干的事情永远一样:每隔几秒唤醒一次,检查下游服务是否已经恢复,然后把状态从 OPEN 切回 CLOSED。…

你的 Web 服务有一条优雅关闭路径。这就是 Bug。

Crash-only 软件将每一次失败视为崩溃,将每一次启动视为恢复。对于 Web 服务来说,这意味着删除你的关闭逻辑,并设计出能在 kill -9 下存活的状态。

你的 Web 服务有一个关闭处理器。它会刷写缓冲区、关闭连接、写入检查点。也许你曾经测试过一次。在生产环境中,它可能只在每年一次的有计划部署时才会运行。其余时间,你的服务死于 OOM kill、节点驱逐、断电,或是超时后被 SIGKILL 的部署。 Crash-only…

看不懂突变改了什么,怎么写测试杀死它?

mutation testing 发现了一个 survivor,但你根本不知道这个 mutation 做了什么。这里有一个分步方法,让你不用先理解 mutant 也能写出正确的测试。

你的 mutation testing 报告里全是 survivor,其中至少有一个你完全看不懂。 工具说它把第 47 行的 换成了 ,或者把整个条件块替换成了 ,又或者变异了一个你根本不知道正在被测试的字符串字面量。你 diff 看了三遍。你还是不明白这个 mutant…

认证代码需要 90% 的变异覆盖率。你的字符串工具函数不需要。

为什么在整个代码库强制执行单一变异分数是错误的,以及如何根据实际风险设置按模块划分的阈值。

在整个代码库强制执行单一变异分数,是让团队厌恶测试的绝佳方式。 用 PIT 或 Stryker 跑一个典型仓库,你会看到同样的模式:认证模块得 40%,字符串工具类冲到 95%,ORM 层则在 60 多分徘徊。本能反应是设一个全局门槛,比如 70%,堵住所有低于它的 PR。两个冲刺之后,就会有人在 CI…

你的测试全过了。变异得分只有40%。以下是你该从存活变异体中读到的东西。

代码覆盖率告诉你很安全。变异测试告诉你,你的测试大多只是摆设。以下是存活变异体如何暴露这一鸿沟,以及如何弥合它。

你的测试全过了。覆盖率报告显示87%。但你的变异得分是40%,一半的变异体还活着。 这个40%不代表你的代码有毛病。它代表你的测试有毛病。覆盖率衡量的是测试运行期间哪些行被执行了。变异测试衡量的是,如果那些行开始做错事,你的测试会不会发现。40%的变异得分意味着,60%本可以被引入代码的 bug 会大摇大摆地通过…

Rust 的变异测试确实能用,但你的编译时间会恨你

cargo-mutants 能找出那些只是假装在验证代码的测试。本文介绍变异测试在 Rust 中的工作原理、它能捕捉什么问题,以及编译时间成本是否值得。

你有 100% 的行覆盖率。每个分支都执行到了。每个函数都被调用了。然后有人在定价逻辑里把一个 改成了 ,跑了一遍测试,全部通过。 这不是理论问题。它真实发生在你的测试执行了代码,却没有真正验证行为的时候。覆盖率衡量的是哪些行被执行了,而不是哪些输出被检查了。变异测试通过故意引入小的…

变异测试需要跑4小时。团队到底怎么在CI里用它?

大多数团队不会每次提交都跑完整的变异测试套件。以下是工程团队如何在不破坏构建流水线的情况下,真正把变异测试集成进CI的做法。

如果你的变异测试套件需要跑四个小时,恭喜你。你证实了大家早就怀疑的一件事:你的测试套件存在漏洞。 你不可能每次 push 都到 CI 里跑这个。没有哪个团队会这么干。问题不在于你能否承受每次提交花四小时,而在于你能否承受带着“测试通过但实际上什么也没验证”的代码上线。…

单元测试全绿,但你的数据照样消失

Mock 数据库测试验证的是 SQL 语法,而不是数据行能否在崩溃、并发写入或 schema 不匹配时存活。下面介绍如何真实地测试持久化。

如果你在测试中 Mock 数据库,你实际上只是在验证仓库层调用了正确的方法。你并没有测试数据能否在崩溃后存活、唯一约束是否真的会阻止重复数据、或者事务失败时是否会回滚。 这个区别很重要。Mock 的 返回你预设的值。真实的…

不被 mock action 淹没地测试 Redux

为每个 Redux action 写 mock 会让你的测试变成 changelog 校验器。下面介绍如何用真实的状态流转来测试 store。

如果你写过这样的测试:验证 被调用时传入的 payload 结构完全匹配,那么你的测试会在每次有人重命名常量时崩溃。 这不是在测试你的状态逻辑,而是在测试你的手指有没有敲对字符串。 Redux 测试教程通常以 Jest mock 开头:spy ,断言 action creator 被调用了,断言 type…

100 次测试运行是个谎言:如何真正确定你的 Property-Based Tests 规模

Property-based testing 中默认的 100 个 example 是一种社会妥协,而非统计策略。以下是如何选择与你的信心需求和 CI 预算相匹配的运行次数。

如果你用默认的 100 个 example 来运行 property-based tests,那你两头不讨好。你的 CI 比实际需要更慢,而你仍然没有抓到真正重要的 bug。 这个数字并不神奇。包括 Hypothesis 在内的大多数库默认设为 100,只是因为它是一个看起来保险的整数。但「感觉保险」不是测试策略。…

Rust 中的 property-based tests 能找到单元测试漏掉的 bug

基于示例的测试只覆盖你想得到的输入。property-based testing 生成随机数据,检查 invariants,并将失败 shrink 到最小反例。

你写了一个 函数。你用 和 测了它。测试通过。你发布了。 用户传入了一个单元素切片。你的函数把它漏掉了。他们提了 issue。你盯着测试文件,想不通这么明显的问题自己是怎么漏掉的。 你漏掉它,是因为 example-based testing 只能抓住你提前预料到的 bug。测试套件里的每一个…

你的单元测试通过了,但生产代码仍然是坏的

代码覆盖率指标制造了一种虚假的安全感。以下是单元测试为何漏掉那些让你夜不能寐的 bug,以及你应该测什么。

你有 90% 的代码覆盖率,却仍在凌晨两点被告警吵醒。 单元测试通过了。CI 是绿的。bug 还是溜进了生产环境。覆盖率没有撒谎,但它也没有说出真相。它衡量的是哪些行被执行了,而不是哪些行为真正被验证了。…

Rust 的 Runtime Contracts 可以在 Release 构建中零开销,但编译器不会替你实现

Rust 会自动剥离 debug assertions,但真正的 design-by-contract 需要的远不止 debug_assert!。本文介绍如何在 Release 二进制文件中实现零成本的 runtime contracts,并让它们彻底消失。

Rust 可以在开发阶段强制执行 runtime contracts,并在 release 构建中将其完全抹除。前提是这门语言并没有把 contracts 当作一等公民。你会拿到所有积木,但得自己把它们拼起来。 是最显而易见的起点。它在 debug 构建中执行,在 release 构建中编译为空。这对简单的…

零个、一个还是十二个:生产函数到底需要多少个断言

开发者要么像撒彩纸一样到处抛撒断言,要么完全避之不及。这个决策框架能帮你区分有用的 invariants 和生产环境崩溃的触发器。

大多数 production codebases 都会分成两派。A 派把 当成装饰性调料,每隔一行就撒一点,直到函数读起来像个偏执律师写的法律合同。B 派把断言当作只在开发阶段使用的辅助轮,构建时全部剥离,然后祈祷代码能在生产环境跑起来,因为测试曾经通过过一次。…

你的验证层比业务逻辑还庞大

手动验证会让 codebase 膨胀不堪,却依然遗漏边界情况。下面介绍如何通过声明式 schemas 强制执行 runtime contracts,同时让它们不碍你的事。

每次 API 收到请求,你都要验证。每次函数收到来自外部系统的参数,你都要检查。如果手动完成这些工作,单个端点积累的验证代码就可能超过业务逻辑本身。 这是 runtime contracts 的隐性成本。你需要它们,因为 type system 会撒谎:通过 HTTP 传输的…

TypeScript strictNullChecks 是编译时守卫,而非运行时盾牌

严格模式能捕获你写出来的 null,却无法捕获来自 API、DOM 查询和 JSON.parse 在运行时抵达的 null。类型系统止步之处,正是你的防御开始之时。

你在 里启用了 ,修掉了每一个红色波浪线,满怀信心地把代码发布到生产环境,以为 和 已经是过去式。 随后后端响应变了结构,一次 DOM 查询返回了空值, 在 TypeScript 曾信誓旦旦保证安全的代码里抛出了 。到底发生了什么? TypeScript…