你的架构图早就是谎言了
架构文档在你保存的那一刻就开始腐烂。以下是如何利用代码生成图表、ADR 和自动化架构测试来保持文档的诚实。
我在 wiki 里见过的每一张架构图都是错的。不是那种惊天动地的错,而是悄无声息、日积月累的错。标着 "Auth" 的服务六个月前就被拆成了三个微服务。标着 "sync call" 的箭头现在已通过队列变成了异步调用。标着 "PostgreSQL" 的数据库在一次紧急故障处理中被迁移到了别的系统,却没人更新那个方框。…
19 posts
架构文档在你保存的那一刻就开始腐烂。以下是如何利用代码生成图表、ADR 和自动化架构测试来保持文档的诚实。
我在 wiki 里见过的每一张架构图都是错的。不是那种惊天动地的错,而是悄无声息、日积月累的错。标着 "Auth" 的服务六个月前就被拆成了三个微服务。标着 "sync call" 的箭头现在已通过队列变成了异步调用。标着 "PostgreSQL" 的数据库在一次紧急故障处理中被迁移到了别的系统,却没人更新那个方框。…
超时或崩溃并不意味着你的 API 请求已经丢失。本文介绍幂等键(idempotency key)如何让重试变得安全,以及真正防止重复请求的数据库存储模式。
你的服务在处理 请求时中途崩溃。客户端看到超时,于是重试。结果出现了两笔扣款。客户很生气。数据库是一致的。但业务逻辑不是。 这不是边缘情况。这是分布式系统的默认行为。网络丢包。容器在请求中途被 OOM 杀掉。负载均衡器对已经到达后端的请求返回 502。如果你的 API…
内存中的互斥锁在服务器重启后随即消失。本文介绍带有 fencing token 和 TTL 的分布式租约如何防止崩溃后的重复执行,以及它们在哪里仍然会失效。
你的 无法挺过 。它也无法挺过 OOM、部署滚动更新或节点重启。进程退出的瞬间,锁就消失了。如果这个锁正在保护一个定时任务、数据迁移或主节点选举,那么你现在会有两个进程都坚信自己是唯一在运行的那个。 这不是你的互斥锁有 bug。这是一个范畴错误。进程本地的锁无法保护集群范围的资源。…
大多数熔断器库都会启动后台线程来探测恢复。你根本不需要它们。本文介绍一种请求驱动的设计,在消除所有后台开销的同时,不牺牲正确性。
我审查过的每一个生产环境熔断器,最终都会拉起一条后台线程。它可能是 Go 的 goroutine、Java 的 ,或是 Rust 的 tokio task。干的事情永远一样:每隔几秒唤醒一次,检查下游服务是否已经恢复,然后把状态从 OPEN 切回 CLOSED。…
Crash-only 软件将每一次失败视为崩溃,将每一次启动视为恢复。对于 Web 服务来说,这意味着删除你的关闭逻辑,并设计出能在 kill -9 下存活的状态。
你的 Web 服务有一个关闭处理器。它会刷写缓冲区、关闭连接、写入检查点。也许你曾经测试过一次。在生产环境中,它可能只在每年一次的有计划部署时才会运行。其余时间,你的服务死于 OOM kill、节点驱逐、断电,或是超时后被 SIGKILL 的部署。 Crash-only…
mutation testing 发现了一个 survivor,但你根本不知道这个 mutation 做了什么。这里有一个分步方法,让你不用先理解 mutant 也能写出正确的测试。
你的 mutation testing 报告里全是 survivor,其中至少有一个你完全看不懂。 工具说它把第 47 行的 换成了 ,或者把整个条件块替换成了 ,又或者变异了一个你根本不知道正在被测试的字符串字面量。你 diff 看了三遍。你还是不明白这个 mutant…
为什么在整个代码库强制执行单一变异分数是错误的,以及如何根据实际风险设置按模块划分的阈值。
在整个代码库强制执行单一变异分数,是让团队厌恶测试的绝佳方式。 用 PIT 或 Stryker 跑一个典型仓库,你会看到同样的模式:认证模块得 40%,字符串工具类冲到 95%,ORM 层则在 60 多分徘徊。本能反应是设一个全局门槛,比如 70%,堵住所有低于它的 PR。两个冲刺之后,就会有人在 CI…
代码覆盖率告诉你很安全。变异测试告诉你,你的测试大多只是摆设。以下是存活变异体如何暴露这一鸿沟,以及如何弥合它。
你的测试全过了。覆盖率报告显示87%。但你的变异得分是40%,一半的变异体还活着。 这个40%不代表你的代码有毛病。它代表你的测试有毛病。覆盖率衡量的是测试运行期间哪些行被执行了。变异测试衡量的是,如果那些行开始做错事,你的测试会不会发现。40%的变异得分意味着,60%本可以被引入代码的 bug 会大摇大摆地通过…
cargo-mutants 能找出那些只是假装在验证代码的测试。本文介绍变异测试在 Rust 中的工作原理、它能捕捉什么问题,以及编译时间成本是否值得。
你有 100% 的行覆盖率。每个分支都执行到了。每个函数都被调用了。然后有人在定价逻辑里把一个 改成了 ,跑了一遍测试,全部通过。 这不是理论问题。它真实发生在你的测试执行了代码,却没有真正验证行为的时候。覆盖率衡量的是哪些行被执行了,而不是哪些输出被检查了。变异测试通过故意引入小的…
大多数团队不会每次提交都跑完整的变异测试套件。以下是工程团队如何在不破坏构建流水线的情况下,真正把变异测试集成进CI的做法。
如果你的变异测试套件需要跑四个小时,恭喜你。你证实了大家早就怀疑的一件事:你的测试套件存在漏洞。 你不可能每次 push 都到 CI 里跑这个。没有哪个团队会这么干。问题不在于你能否承受每次提交花四小时,而在于你能否承受带着“测试通过但实际上什么也没验证”的代码上线。…
Mock 数据库测试验证的是 SQL 语法,而不是数据行能否在崩溃、并发写入或 schema 不匹配时存活。下面介绍如何真实地测试持久化。
如果你在测试中 Mock 数据库,你实际上只是在验证仓库层调用了正确的方法。你并没有测试数据能否在崩溃后存活、唯一约束是否真的会阻止重复数据、或者事务失败时是否会回滚。 这个区别很重要。Mock 的 返回你预设的值。真实的…
为每个 Redux action 写 mock 会让你的测试变成 changelog 校验器。下面介绍如何用真实的状态流转来测试 store。
如果你写过这样的测试:验证 被调用时传入的 payload 结构完全匹配,那么你的测试会在每次有人重命名常量时崩溃。 这不是在测试你的状态逻辑,而是在测试你的手指有没有敲对字符串。 Redux 测试教程通常以 Jest mock 开头:spy ,断言 action creator 被调用了,断言 type…
Property-based testing 中默认的 100 个 example 是一种社会妥协,而非统计策略。以下是如何选择与你的信心需求和 CI 预算相匹配的运行次数。
如果你用默认的 100 个 example 来运行 property-based tests,那你两头不讨好。你的 CI 比实际需要更慢,而你仍然没有抓到真正重要的 bug。 这个数字并不神奇。包括 Hypothesis 在内的大多数库默认设为 100,只是因为它是一个看起来保险的整数。但「感觉保险」不是测试策略。…
基于示例的测试只覆盖你想得到的输入。property-based testing 生成随机数据,检查 invariants,并将失败 shrink 到最小反例。
你写了一个 函数。你用 和 测了它。测试通过。你发布了。 用户传入了一个单元素切片。你的函数把它漏掉了。他们提了 issue。你盯着测试文件,想不通这么明显的问题自己是怎么漏掉的。 你漏掉它,是因为 example-based testing 只能抓住你提前预料到的 bug。测试套件里的每一个…
代码覆盖率指标制造了一种虚假的安全感。以下是单元测试为何漏掉那些让你夜不能寐的 bug,以及你应该测什么。
你有 90% 的代码覆盖率,却仍在凌晨两点被告警吵醒。 单元测试通过了。CI 是绿的。bug 还是溜进了生产环境。覆盖率没有撒谎,但它也没有说出真相。它衡量的是哪些行被执行了,而不是哪些行为真正被验证了。…
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 传输的…
严格模式能捕获你写出来的 null,却无法捕获来自 API、DOM 查询和 JSON.parse 在运行时抵达的 null。类型系统止步之处,正是你的防御开始之时。
你在 里启用了 ,修掉了每一个红色波浪线,满怀信心地把代码发布到生产环境,以为 和 已经是过去式。 随后后端响应变了结构,一次 DOM 查询返回了空值, 在 TypeScript 曾信誓旦旦保证安全的代码里抛出了 。到底发生了什么? TypeScript…