1. 从一次线上事故说起:我为什么重新审视断言
先说件真事。大概两年前,我接手了一个用C++写的网关服务,核心逻辑里有段配置解析代码,用的是最朴素的指针判空方式。某个加班的深夜,线上流量高峰,有一批异常配置流进来,那段代码直接访问了空指针,进程秒崩,报警响成一片。当时第一反应是“这代码怎么这么糙”,后来翻日志才发现,其实早在一个月前的灰度环境里,同样的配置就触发过类似问题,但因为灰度环境数据量小、调用链短,没等程序崩溃就被其他模块的容错机制“兜住”了,于是这个隐患一直潜伏到线上才爆发。
事后复盘时,同事甩过来一句话:“这种位置当初要是加了assert,灰度环境第一时间就能炸出来,根本轮不到线上。”
那句话我记到现在。assert(断言)这个东西,听起来像是编程入门课里一个不起眼的小知识点,很多人在教学代码里见过它、写过它,但真正在工作里用得好的,其实不多。它不像设计模式那样听起来高深,也不像框架源码那样值得大书特书,但在关键的交接点、前置条件校验点、不可能为假的分支里,断言往往是成本最低、发现错误最快的工具。
这篇文章我想认认真真聊一次断言:从C/C++里最原始的assert宏讲起,聊到测试框架里各种花式断言,再聊到和很多后端工程师日常工作挂钩的JMeter断言。我尽量不写成教材式说教,而是把我们实际踩过的坑、总结出来的用法习惯、以及我自己的思考过程,一并摊开来讲。
不管你是刚学编程的在校生,还是已经写了三五年业务代码的工程师,只要你需要在程序里表达“这里不可能是错的,如果错了就是你代码有bug”,断言就是最适合你的那套表达方式。
2. 断言的本质:把“不可能”翻译成机器能检查的规则
2.1 断言到底在做什么
我看过很多介绍断言语法的文章,上来就列几个示例代码,然后说“这就是断言”。语法当然要讲,但我更想先从语义层面聊聊,因为搞不清楚断言究竟解决什么问题,语法记得再牢也是白搭。
用一句话概括:断言是在程序里插入一条“此刻这个条件必须为真”的检查点,如果条件为假,程序立刻停下来告诉你哪里错了。
听起来很像if语句对吗?确实像。但两者有本质区别。if语句属于正常业务逻辑,条件为真走一条路,条件为假走另一条路,两条路都是程序“预期内”的行为;而断言表达的是“程序运行到这个位置时,某个条件无论如何都必须是真”,如果它是假,说明程序已经处于一个“不应该出现”的状态,后面继续执行只会放大错误。
更直白一点,if 是在处理“可能发生”的事情,断言是在拦截“不可能发生”的事情。
举个例子。你写了一个函数,参数是个指针,函数内部要解引用它。正常的防御性写法是:
if (ptr == NULL) { return -1; }这是用if兜底,告诉调用者“传空了,我处理不了,先返回个错误码”。而断言的写法是:
assert(ptr != NULL);这是在说:“ptr都不为空的,你传个空指针过来,那一定是调用者的逻辑出bug了。”
两者不冲突,甚至可以共存。但你要清楚,if 是协作机制,断言是纠错机制。if让你在异常场景下继续运作,assert让你在错误场景下立刻曝光问题。
2.2 一个小类比:断言的“哨声”到底响给谁听
我做过一段时间足球裁判的朋友,给我讲过一句话特别有触动:裁判的哨子不是给犯规的人听的,是给所有场上队员听的——告诉大家“这个动作越界了,比赛要停下来重新开始”。
断言就是程序世界里的裁判哨。你并不指望哨声去“修复”什么,你指望的是哨声一响,所有人立刻知道越界行为发生在哪里、发生在哪个环节,然后大家停下来修问题。如果一支球队场上没有人吹哨,犯规动作会被一次次放过,积累到最后往往演变成大冲突或者大比分失利。放到程序里,那些该有断言却空缺的位置,就像没有裁判盯着的比赛区域,错误一点点积累,直到某一天在完全意想不到的地方彻底炸开。
所以我一直觉得,断言不是用来“保护代码不出错”的,它是用来“让错误尽早暴露”的。你写的每一行代码都有出错的可能,断言能做的,是在错误刚冒头的时候,用最响亮、最刺耳的方式告诉你:“就这儿,出事了。”
想通这一点,你对断言的用法就会有一个质的改变。你不再关心“断言能不能帮我把异常拦住”,而是关心“哪些位置一旦状态不对,我必须第一时间知道”。
2.3 断言和异常处理的分工
还有一个绕不开的问题:有了异常处理,还要断言干什么?
很多中高级开发者在聊到这个点的时候,其实多少有些含糊。我的理解是这样的——异常处理和断言解决的是两类不同性质的错误。
异常处理面向的是“预期内的异常情况”。比如网络超时、文件不存在、用户输入不合法,这些都是你在写代码时就知道“有可能会发生”的状况,你需要给它们设计退路。这类问题用 try-catch / 错误码 / 返回空值去处理,都合理。
断言面向的是“预期之外的程序缺陷”。比如某个算法步骤后数组长度竟然变成了负数、某个函数被调用时参数状态竟然不满足前置条件、某个枚举值竟然走出了代码里处理过的所有分支——这些情况在正确的代码逻辑下是不可能发生的,一旦发生,说明程序本身有bug。
把异常处理用在程序缺陷上,结果往往是掩盖问题:系统吞掉异常继续跑,跑着跑着出现更隐蔽的数据损坏,到最后排查成本成倍增长。把断言用在预期内的异常上,结果则是误杀正常流程:明明用户输入不规范是常态,你却用断言直接终止程序,线上全得炸。
一句话总结我的习惯:能用异常处理表达的场景,就别用断言;能用断言拦截的程序缺陷,别轻易放过。
3. C语言里的 assert:最原始也最经典的那个宏
3.1 assert 的出身和语法细节
C语言标准库里的 assert 宏定义在<assert.h>头文件中,用法极其简单:
#include <assert.h> assert(表达式);表达式的求值结果如果为真(非0),程序继续往下走;如果为假(0),程序立即终止,并向标准错误输出一条包含文件名、行号、以及未通过的表达式的错误信息。
注意这里有个容易忽略的细节:它在标准输出还是标准错误输出这个问题上,有人会记混。assert 失败时的信息是写到stderr的,不是 stdout。如果你在脚本里只重定向了标准输出,会看不到断言失败的信息,这个小坑我踩过不止一次。
另一个细节是 assert 宏受NDEBUG宏控制。在编译时如果定义了 NDEBUG,assert 宏会展开为空,也就是所有断言检查都会被移除,程序里等于没有任何断言。这是标准化行为,GCC、Clang、MSVC 都遵循这个约定。
这就带来了一个非常经典的争议:既然发布版本里断言会被移除,那断言到底还有什么用?我稍后专门谈这个问题。
3.2 一个实际可跑的示例
很多教程喜欢用类似“int a = 5; assert(a == 5);”这种毫无营养的例子,看了等于没看。我们来看一个真实工作里常见的场景——链表节点移除:
#include <stdio.h> #include <stdlib.h> #include <assert.h> typedef struct Node { int data; struct Node *next; } Node; void remove_next(Node *current) { // 前置条件:current 本身不能为空 assert(current != NULL); // 前置条件:current->next 不能为空,否则“移除下一个节点”没有意义 assert(current->next != NULL); Node *to_delete = current->next; current->next = to_delete->next; free(to_delete); } int main() { Node n1 = {1, NULL}; Node n2 = {2, NULL}; n1.next = &n2; remove_next(&n1); // 正常流程 remove_next(&n1); // 此时 n1.next 已经是 NULL,断言会失败 return 0; }运行第二个 remove_next 时,程序会在 assert(current->next != NULL) 处终止,并打印类似这样的信息:
a.out: test.c:16: remove_next: Assertion `current->next != NULL' failed.注意看这行信息:它精确地告诉你哪个文件、哪一行、哪个函数、哪个表达式出了问题。这种信息密度是很多日志系统都达不到的,因为断言失败意味着调用栈当前状态下,那个条件已经不可能为真——这本身就是最重要的调试线索。
3.3 为什么 C 的 assert 默认只在调试版生效
如果你刚接触这段内容,你大概率会有一个疑问:既然断言这么有用,为什么标准库要设计成发布版本默认禁用?
关键在于性能。有些断言写在一个被调用几百万次的函数内部,每次判断都会带来一次表达式求值的开销,哪怕只是一次变量比较,累积起来也相当可观。再加上很多断言是辅助开发阶段排查问题用的,到了发布阶段,开发者认为自己已经通过测试验证了程序的正确性,此时再为“理论上不该失败”的检查付出性能代价,不划算。
所以 NDEBUG 的设计思路是:开发阶段全面、高频地检查程序内在状态,让缺陷在测试期间暴露;发布版本则移除检查,换取极致的运行效率。
这就引出了一个特别重要的实践规则:不要用 assert 去做程序发布后依然必须保证的功能性检查。比如不该用 assert 判断 malloc 是否失败,因为即便到了发布版,内存分配失败依然可能发生,你需要的是真正的错误处理代码。我曾经在一个项目里见过有人把文件句柄的有效性判断写在 assert 里,结果发布版本一上线,断言被移除了,文件句柄失效时程序直接继续往下执行,把一个本来可以快速恢复的故障变成了崩溃。
4. C++ 里的断言:从“条件检查”到“测试契约”
4.1 C 风格 assert 在 C++ 中的延续与问题
C++ 兼容 C 的 assert 宏,#include <cassert>之后可以直接用。但实际写下来你会发现,C 风格断言在 C++ 项目里有几个天然别扭的地方。
第一个问题是,断言失败时打印的信息是纯文本表达式,不会把相关变量的值一起输出。你只知道assert(index >= 0 && index < size)失败了,但 index 当前是多少、size 又是多少,全靠你自己想办法去查。
第二个问题是,assert 宏在 C++ 里遇到含模板和重载的复杂表达式时,报错信息往往非常难定位。因为宏展开本质上是“直接替换到代码里”,一旦这个表达式在某种类型下失败,编译器那一大串模板实例化错误能把人看吐。
第三个问题,也是更本质的问题——C++ 的工程实践早就超越了“程序不崩溃”这个目标,进入了“程序行为符合契约”的阶段。而 C 的 assert 只能检查“程序没崩”,很难表达复杂的对象状态、容器内容、关系约束这类高层次的正确性。
所以在 C++ 的严肃项目里,我用得更多的其实是下文要讲的测试断言和某些改进版的运行时断言。
4.2 C++ 中三种常见的断言用法
如果你现在要在一个 C++ 工程里使用断言,我建议你根据场景区分三种玩法。
第一种是继续用传统的assert宏做运行时前置条件检查,适合底层小函数、模块内部接口的保卫战。比如一个队列类的 Pop 方法,前置条件是“队列非空”,你用 assert 检查成本很低,而且这个条件在正确逻辑下真的不可能为假,符合断言的使用边界。
第二种是 C++11 引入的static_assert,它把断言从“运行时”搬到了“编译时”。我特别喜欢在模板代码和接口设计里用它,举个例子:
static_assert(sizeof(int) == 4, "这个平台不是我们支持的平台"); static_assert(std::is_integral_v<T>, "这个模板只接受整数类型");编译期断言的价值在于:错误发现时间点比运行时更早——程序还没跑起来,编译阶段就直接把问题暴露出来。这符合我们今天聊的主线:错误暴露得越早,修复成本越低。
第三种是在单元测试框架里的断言。
4.3 单元测试框架里的断言:真正的日常主力
如果你在用 Google Test、Catch2、doctest 这类 C++ 测试框架,你的绝大多数“断言”其实不是assert宏,而是框架提供的EXPECT_*和ASSERT_*系列。
以 Google Test 为例:
ASSERT_EQ(a, b):失败则终止当前测试用例;EXPECT_EQ(a, b):失败只记录,测试继续跑;EXPECT_THROW(statement, exception_type):检查是否抛指定异常;EXPECT_FLOAT_EQ(a, b):浮点数近似比较,避免直接等号比较带来的精度问题。
这里我想强调一个经常被忽略的原则:单元测试里的断言,本质上是在把函数/模块的行为契约转换成机器可执行的检查规则。你每写一个断言,就是在声明“我这个东西在什么样的情况下,应该有什么样的行为”。当这些契约堆积起来,你拥有的就不仅仅是一堆测试代码,而是一种回归保护网——任何代码改动了既有行为,测试断言就会立刻拉响警报。
我见过很多团队对测试断言的理解停留在“验证结果对不对”这个层面,导致断言数量少且写得非常泛。但真正高价值的测试断言,往往细到“在某个边界条件下,返回值的具体范围”“在某个非法输入下,是否抛了指定类型的异常”“在连续调用两次后,对象内部状态是否符合预期”。这些断言拼起来,才是代码的完整行为契约。
4.4 一个让我记忆深刻的断言拯救案例
去年我参与维护一个音频处理库,里面有个函数负责把 PCM 缓冲区从采样率 A 重采样到采样率 B。这个函数被上层业务频繁调用,一直相安无事。后来一次算法重构,有同事把输入缓冲区长度计算逻辑改了,压缩格式下长度计算少乘了一个声道数,缓冲区比实际需要小了一半。
这种 bug 最坑的地方在于——如果只是“缓冲区小一半”,一般的最坏结果是内存越界读,偶尔崩溃,偶尔读到错数据,极难稳定复现。但那套代码里恰好加了一个断言:
assert(input_samples_needed <= input_buffer_size);重构后的第一次单元测试就跑挂了,断言失败信息清清楚楚地指向长度计算那行代码。整个定位过程不超过五分钟。
后来我在代码评审里说:这段代码要是没有这个断言,这个 bug 可能要在客户现场跑几周才暴露,而且暴露时的现象大概率是“声音偶尔卡顿”“偶尔杂音”,排查难度完全是地狱级。断言省下的时间,可能是成百上千人时。
5. JMeter 里的断言:性能测试中的结果校验与质量守门
5.1 为什么性能测试需要断言
我看过太多人一提到 JMeter,第一反应就是“调并发数、看聚合报告、关心响应时间和吞吐量”。但有一个东西经常被忽略,那就是断言组件。
道理其实很简单:性能测试跑完,报告里显示“平均响应时间 200ms、错误率 0.5%”,看起来很漂亮。但如果大量请求返回的明明是错误内容——比如接口返回了一个错误的提示信息、或者返回了缓存中的旧数据——聚合报告里压根显示不出来,因为 HTTP 状态码可能依然是 200。
断言在 JMeter 里的核心作用,就是把“响应到底对不对”这个校验工作自动化。没有断言,你只能事后抽几条请求手工看响应体;有了断言,你可以在请求发出去的同时自动校验响应是否符合预期,并且把不符合预期的请求标记为错误,统计进错误率里。
从本质上讲,JMeter 断言和 C++ 里的 assert 是同一个思路:在关键节点上插入检查点,一旦发现程序行为偏离预期,就立刻让错误暴露。只不过这里检查的对象从“变量的值”变成了“HTTP 响应”,检查的时机从“代码执行的某个位置”变成了“请求返回后的那一刻”。
5.2 JMeter 里几种主流断言的用法对比
JMeter 自带了很多断言组件,我把平时项目里真正用得上的几种按适用场景排一下。
响应断言(Response Assertion)是最常用的一种。它的原理是让你配置一些“匹配规则”,然后检查响应内容(响应文本、响应代码、响应头、URL 样本等)是否满足规则。规则支持“包含”“匹配”“相等”“字符串”等模式,并且可以用正则表达式。
比如我只关心接口返回码和某个业务状态码:
响应体:{"code": 0, "message": "success", "data": [...]} 断言规则:包含 "code": 0只要返回体里没有"code": 0,断言失败,该请求被标记为错误。
JSON 断言(JSON Assertion)是在响应为 JSON 格式时更精细的选择。它直接用 JSONPath 语法去检查某个字段的值是否符合预期,比如$.data.count的结果是否大于 0。相比响应断言里的文本匹配,JSON 断言对结构更敏感、更准确,不会因为响应体里其他地方恰好出现了相同字符串而产生误判。
断言持续时间(Duration Assertion)用来检查响应时间是否超过阈值,比如超过 1000ms 就标记为失败。这个在性能测试里非常实用,因为聚合报告的平均值会被少数慢请求拉高,但你很难快速筛出到底是哪条链路慢。持续时间断言可以在请求级直接把这个“慢”标记出来。
断言结果监听器(Assertion Results Listener)不是断言组件本身,但它是查看断言失败详情的关键。我在排查测试失败时,基本上只看两个监听器:一个是“断言结果”,专门列断言失败的信息;另一个是“查看结果树”,用于对比请求和响应详情。
我把它们整理成一个速查表,方便日常翻阅:
| 断言类型 | 适用场景 | 验证内容 | 使用建议 |
|---|---|---|---|
| 响应断言 | 通用接口 | 响应文本/状态码/响应头 | 适合快速做粗粒度校验,配置最简单 |
| JSON 断言 | REST API | JSONPath 表达式 | 字段路径精准校验,比文本匹配更可靠 |
| 断言持续时间 | 性能测试 | 响应时间阈值 | 适合验证接口的延迟上限 |
| XPath 断言 | XML 接口 | XPath 表达式 | 老系统 XML 报文用得多 |
| HTML 断言 | 网页测试 | HTML 元素匹配 | 现在用得少了,爬虫场景会碰 |
5.3 一个完整的 JMeter 断言配置案例
下面我用一个实际接口来完整演示断言的配置流程。假设被测接口是:
POST /api/v1/order/query 请求体:{"userId": 12345, "orderStatus": "PAID"} 预期响应:{"code":0, "data": {"orderCount": 3, "orders":[...]}}第一步,在线程组里添加 HTTP 请求默认值,配置协议为 HTTPS、服务器地址填被测环境域名。
第二步,添加 HTTP 请求,方法选 POST,路径填/api/v1/order/query,Body Data 里粘贴上面的请求体,并添加 HTTP 头管理器,设置 Content-Type 为 application/json。
第三步,重点来了。添加 JSON 断言(选择 HTTP 请求节点,右键 -> 添加 -> 断言 -> JSON 断言),配置:
- JSON Path 表达式:
$.code - 预期值:
0 - 勾选“JSON Path 断言”中的“Expect null?”,不勾选
这样配置的意思是:响应里的code字段必须严格等于 0,只要不等于 0,断言就算失败。
再加一个响应断言,做一层兜底校验——检查响应体里是否包含"orderCount"这个字段,防止返回体整体结构异常(比如返回了一个 HTML 错误页)时代码字段恰好被错误内容命中。
第四步,添加“断言结果”监听器,再添加一个“聚合报告”。跑一轮测试,观察断言结果列表。
我自己的经验是,配置断言的时候,尽量让断言粒度匹配接口的真实语义。如果接口明确有业务码字段,用 JSON 断言校验业务码;如果没有明确业务码,再用文本包含来校验核心字段。两级叠加,既防“结构错了但状态码正常”的误判,也防“业务码对了但内容不对”的漏判。
5.4 我在 JMeter 断言上用过的蠢方法和最终习惯
早期我做接口压测,断言配置得特别糙。直接加一个响应断言,文本写死一个中文提示语“操作成功”,然后就去跑压测了。跑完看报告,错误率 0%,还挺开心。后来恢复网络流量回放时才发现,被测接口在某种错误场景下返回的是另一个文案“系统繁忙,请稍后再试”,状态码也是 200,那个断言压根没测出问题。
自那以后我给自己立了三条规矩:
第一,断言不能只看 HTTP 状态码,必须校验业务层面的内容。HTTP 200 不等于业务成功,这个观念不转变,断言写了也白写。
第二,能精准定位字段就绝不用模糊匹配。文本包含最大的问题是“包含即通过”,它不关心你的业务字段到底是几。比如你要校验一个金额是不是 100 元,用文本匹配“100”可能在金额为 1000 时也通过,因为字符串里包含“100”,这种误判非常隐蔽。
第三,断言结果必须单独盯。我在实际项目里会把“断言结果”监听器放在所有线程组的外层公共位置,确保所有请求的断言失败信息都能汇总到同一个监听器里,方便第一时间定位。这个习惯帮我在很多次压测里快速找到了“看起来没问题、其实早已大面积出错”的接口。
6. 断言的边界:什么场景别用断言
6.1 别用断言做输入校验的“主力”
在很多团队里,我见到一种错误用法:API 网关或者业务入口处,用断言去校验用户传参。比如:
assert(userId > 0);这个写法在开发环境没问题,因为断言能帮你快速发现哪些调用链传了非法参数。但它一旦上了发布版,NDEBUG 一定义,这个检查直接就消失了。用户传过来的非法参数会继续往深层代码里传,最后可能引发更严重的状态污染。
正确的做法是:用户输入通过显式校验去处理,返回明确的错误码或抛业务异常;断言只放在“你已经确认逻辑正确、但想拦截内部实现缺陷”的地方。
一句话:外部输入用防御式校验,内部契约用断言。
6.2 别把断言当异常处理用
这个问题我在前面其实已经深入聊过。这里只补充一个实际例子:我曾经在一个并发模块里看到有人这么写:
assert(lock_acquired == true);本意是想检查锁是否成功获取。可锁获取失败在极端情况下(比如死锁临界、系统资源不足)是一种运行时错误,不是程序逻辑缺陷。正确的写法应该是:
if (!lock_acquired) { // 走错误处理分支 }把“可能发生的错误”当成“不可能发生的缺陷”去断言,等于把运行时错误的处理路径直接砍掉了,这是在给自己埋雷。
6.3 断言失败信息本身的“信息量”要够
这个细节可能很多人没意识到:断言写得好不好,很大程度上取决于失败时输出的信息是否足够定位问题。
C 的 assert 宏只能输出表达式原文,信息量有限。所以在 C++ 项目里,我更倾向于写带上下文的断言辅助函数或者使用带自定义消息的测试断言。比如 Google Test 的ASSERT_TRUE(condition) << "index=" << index << ", size=" << size;,失败时把关键变量值一并打印出来,定位效率翻倍。
JMeter 的断言也是同理。响应断言失败时,默认信息往往只告诉你“包含匹配失败”,但具体响应内容是什么,你得到“查看结果树”里去看。我在项目里的习惯是,断言组件里能加自定义失败消息的地方就加,不能加的地方就靠监听器兜底。一句话:断言只是标记失败还不够,最好还能告诉我们失败现场长什么样。
7. 常见问题与排查实录:断言用起来之后踩过的坑
7.1 、发布版断言被移除后出问题的排查思路
前面提到 NDEBUG 会导致 assert 失效。这里分享一个真实案例:某个服务在测试环境一切正常,上了生产开始偶发数据错乱。查了很久发现,有一处关键的状态完整性判断被写在了 assert 里,发布版编译时定义了 NDEBUG,这个检查在线上根本没执行。
排查这类问题,我的思路是三点:
- 检查编译选项,确认 NDEBUG 是否在发布版中打开;
- 全局搜索代码里所有 assert 调用,逐一判断“如果它在发布版失效,会不会有安全隐患”;
- 对确实需要的运行时检查,用显式错误处理逻辑替代断言,而不是依赖断言在发布版生效。
7.2 、JMeter 断言和聚合报告里的“错误率”对不上
很多人遇到这种情况:断言结果监听器里显示了失败,但聚合报告的错误率却是 0%。原因在于聚合报告统计的是采样结果的状态码,而断言失败在 JMeter 内部被标记为“断言失败”,它不一定会改变采样结果的 HTTP 状态标记。你可以理解为,在 JMeter 里“响应正常”和“断言通过”是两个维度的事。
解决方法是,把断言结果监听器里的失败信息作为主要判断依据,或者配合“通过后置处理器/断言监听器将断言失败标记为错误”的逻辑(例如在断言失败时通过__setProperty设置全局错误标识)来做整个测试计划级别的结果统计。这是一个很常见的配置盲区,知道就很容易绕过去。
7.3 、断言本身写错导致的误报
还有一种很坑的情况:断言本身的表达式就写错了,导致一直报“断言失败”,但业务其实是正常的。
比如在 JMeter 里,JSON 断言路径写错了:$.data.count和$.data.orderCount差一个字段名,断言就永远取不到值,一律判定失败。你在那里排查半天接口问题,最后发现是断言写错了,心态会非常崩溃。
我的做法是,每配置好一个新断言,先构造一个预期成功的请求和一个预期失败的请求,分别跑一遍,确认断言行为符合预期,再纳入正式测试计划。这个做法的价值,和 C++ 里面“先写会失败的测试再写实现”的红绿测试理念是一脉相承的。
7.4 、断言密集导致性能下降的问题
断言不是零成本的。某个底层函数被调用几百万次,里面加一个断言,即便是一个简单的整数比较,也会带来可观测的性能开销。如果你是在写 C++ 高性能模块,且断言所在的函数属于热路径,我建议采用分层策略:
- 开发版开启全量断言;
- 性能测试时开启部分核心断言,关闭次要断言;
- 发布版按需关闭(通过编译开关或条件编译)。
JMeter 场景里也有类似问题。当压测并发很高时,大量请求的断言结果都会被收集到监听器,内存和网络传输的压力都会上来。我的习惯是,压测阶段跑全量断言做正确性校验,正式压测时只保留核心业务断言,去掉过于琐碎的文本匹配,避免断言本身成为压测瓶颈。
8. 写在最后:断言是我眼中“性价比最高”的代码习惯之一
我写这篇文章的时候,脑子里一直浮现着很多年前刚工作时的自己。那时候写代码,脑子里只有“功能实现”,从来没有“状态检查”和“契约约束”的意识。后来见识了太多线上事故,才真正理解一个道理:程序员对自己代码最大的负责,不是嘴上说“我测过了没问题”,而是在每一处可能出错的地方,主动埋下能第一时间侦测到问题的哨兵。
断言就是这样的哨兵。
它的语法极简、上手极快、运行时也能移除、测试阶段价值极高。它不要求你掌握什么高深理论,只需要你多问自己一句:“如果走到这里状态不对,我希望程序怎么做?”是沉默地继续错下去,还是响亮地停下来?
我现在写代码的习惯是,在每个函数的入口处问自己“这个函数的前置条件是什么”,在每个分支结束时问自己“走到这里,有哪些状态一定成立”。这些问题的答案,往往就是一句 assert、一条 EXPECT_TRUE、一个 JSON 断言。
节点不多,成本极低,但效果出奇地好。希望你也能在下一个项目里,试试把断言当成一种习惯,而不是一个应付考试的知识点。