4、测试用例设计进阶:基于需求的测试设计、基于模型的测试设计、随机测试与定向测试、覆盖率驱动的测试设计
好,咱们进入第四章。这一章我打算聊点进阶的东西。你可能会问:前面不是已经讲了测试用例怎么写吗?怎么又来一个进阶?
嗯,这么说吧。前面讲的是「怎么把测试写出来」,这一章讲的是「怎么把测试写得更好、更全、更聪明」。说白了,就是从一个测试执行者,向测试设计者的转变。
我个人习惯把测试设计分成四个维度:基于需求、基于模型、随机与定向、覆盖率驱动。咱们一个一个来拆。
4.1 基于需求的测试设计
这是最基础,也是最容易被忽视的。很多工程师拿到需求文档,扫一眼就开始写用例。结果呢?漏测了。
我遇到过最典型的一个案例:一个BMS的过压保护功能,需求里写的是「当电池电压超过4.2V时,系统在100ms内切断充电回路」。测试工程师写了三个用例:4.21V、4.25V、4.3V。看起来没问题对吧?
但实际测试时,发现4.19V的时候系统也切断了。为什么?因为ADC采样有误差,加上迟滞设计没做好。需求里没写「低于4.2V不动作」,但这是隐含的边界条件。
所以,基于需求的测试设计,核心是三点:
- 正向测试:需求里写了什么,我就测什么。比如「CAN报文周期为10ms」,我就测10ms±允许误差。
- 反向测试:需求里没写的,或者禁止的,我也要测。比如「不允许在休眠状态下发送CAN报文」,我就故意在休眠时发一条,看它会不会被抑制。
- 边界测试:这是最容易漏的。比如「工作温度范围-40°C~85°C」,你不仅要测-40和85,还要测-41和86,看看系统怎么失效。
我的经验: 拿到需求后,先花10分钟做一件事——把需求里的每个「动词」和「数值」圈出来。动词对应功能,数值对应边界。圈完之后,你的测试用例框架就有了。
4.2 基于模型的测试设计
这个听起来有点玄乎,其实说白了就是:把系统的行为画成图,然后从图里自动或半自动地生成测试用例。
我在做ADAS项目时用过这个方法。一个ACC(自适应巡航)功能,状态机有七八个状态:关闭、待机、激活、跟车、定速、取消、故障。每个状态之间的转换条件有几十个。用手写用例?写到崩溃。
后来我们用Simulink Stateflow建了一个模型,把所有的状态和转换条件都画进去。然后跑一个工具(比如Reqtify或者TPT),自动生成测试用例。生成的用例覆盖了所有状态转换路径,包括一些我根本没想到的「不可能路径」——比如从「故障」直接跳到「激活」。
基于模型的测试设计,有几个好处:
- 可视化:模型比文字更直观。你一眼就能看出哪个状态没被覆盖。
- 自动化:模型建好了,用例生成是半自动的,省力。
- 可追溯:每个用例都能追溯到模型里的某个路径,方便评审。
一个小技巧: 模型不要建得太细。太细了,模型本身就成了一个需要测试的东西。我一般只建到「状态机」和「关键数据流」这个粒度。再细的,比如内部算法,交给单元测试去处理。
4.3 随机测试与定向测试
这两个是一对冤家。定向测试,就是你知道要测什么,写一个精确的输入,看输出对不对。随机测试,就是让输入随机变化,看系统会不会崩。
你可能会问:随机测试靠谱吗?
嗯,靠谱,但要看场景。我在做网关路由测试时,定向测试只能覆盖我想到的报文组合。但实际车上,报文是乱序的、有延迟的、甚至丢包的。随机测试能帮我发现那些「我根本想不到」的时序问题。
我的做法是:
- 定向测试:用于验证核心功能。比如「发送ID为0x123的报文,期待ID为0x456的报文被转发」。这种用例,每个都要写,每个都要跑。
- 随机测试:用于压力测试和鲁棒性测试。比如「随机生成10000条CAN报文,ID随机、数据随机、周期随机,看网关会不会死机或丢包」。
但随机测试有个坑:不可复现。你发现了一个bug,但下次跑同样的随机种子,可能就复现不了了。所以我建议:
警告: 随机测试一定要记录随机种子(seed)。这样即使bug复现不了,你也能用同一个seed重新生成同样的输入序列。我曾经因为没记seed,一个偶发bug查了整整两周。后来发现是随机时序导致的一个竞态条件。记了seed之后,三次就复现了。
4.4 覆盖率驱动的测试设计
这个是我个人认为最「高级」的测试设计方法。它不关心你写了多少用例,它关心的是:你的测试到底覆盖了多少代码、多少分支、多少条件?
在MIL阶段,我们通常关注以下几种覆盖率:
| 覆盖率类型 | 含义 | 我的建议 |
|---|---|---|
| 语句覆盖 | 每行代码是否至少被执行一次 | 基础要求,必须100% |
| 分支覆盖 | 每个if-else的真假分支是否都被走过 | 建议100%,至少95% |
| 条件覆盖 | 每个布尔子条件的真假是否都被覆盖 | 复杂逻辑时使用 |
| MC/DC | 每个条件独立影响结果 | 安全关键功能(ASIL C/D)强制要求 |
| 路径覆盖 | 所有可能的执行路径 | 理论上不可能100%,只针对关键路径 |
覆盖率驱动的测试设计,流程是这样的:
- 先写一批基础的定向测试用例。
- 跑一遍,收集覆盖率数据。
- 看哪些代码没被覆盖到。
- 针对没覆盖到的代码,补充测试用例。
- 重复2-4,直到覆盖率达标。
听起来很简单?但实际操作中,我遇到过一个问题:覆盖率很高,但bug还是漏了。
为什么?因为覆盖率只告诉你「代码被执行了」,但不告诉你「执行的结果对不对」。比如一个加法函数,你测了1+1=2,覆盖率100%。但你没测1+2=3,结果代码写的是1+2=4。覆盖率还是100%,但功能是错的。
所以我的原则是: 覆盖率是「必要但不充分」的条件。先保证功能正确,再用覆盖率去查漏补缺。不要为了凑覆盖率而写一堆无意义的用例。我见过有人为了达到100%分支覆盖,写了一个「if(1==0)」的假分支用例。这种用例除了让报告好看,没有任何价值。
4.5 四种方法的融合使用
你可能会问:这四种方法,我到底该用哪一种?
我的答案是:全用。但不是同时用,而是分阶段用。
- 第一阶段:基于需求。先把需求里的功能点全部覆盖掉。这是底线。
- 第二阶段:基于模型。如果系统有复杂的状态机或数据流,建个模型,补全路径。
- 第三阶段:随机测试。跑一批随机用例,看看有没有「意外惊喜」。
- 第四阶段:覆盖率驱动。跑覆盖率工具,查漏补缺,直到达标。
这个流程我用了很多年,效果不错。当然,具体项目可以灵活调整。比如一个简单的开关逻辑,可能不需要建模型。但一个复杂的域控制器,这四个阶段一个都不能少。
好,这一章就到这里。下一章咱们聊聊测试执行和自动化,那又是一个大坑。到时候我给你们讲讲我当年是怎么被自动化框架坑到加班的。