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%,只针对关键路径

覆盖率驱动的测试设计,流程是这样的:

  1. 先写一批基础的定向测试用例。
  2. 跑一遍,收集覆盖率数据。
  3. 看哪些代码没被覆盖到。
  4. 针对没覆盖到的代码,补充测试用例。
  5. 重复2-4,直到覆盖率达标。

听起来很简单?但实际操作中,我遇到过一个问题:覆盖率很高,但bug还是漏了

为什么?因为覆盖率只告诉你「代码被执行了」,但不告诉你「执行的结果对不对」。比如一个加法函数,你测了1+1=2,覆盖率100%。但你没测1+2=3,结果代码写的是1+2=4。覆盖率还是100%,但功能是错的。

所以我的原则是: 覆盖率是「必要但不充分」的条件。先保证功能正确,再用覆盖率去查漏补缺。不要为了凑覆盖率而写一堆无意义的用例。我见过有人为了达到100%分支覆盖,写了一个「if(1==0)」的假分支用例。这种用例除了让报告好看,没有任何价值。

4.5 四种方法的融合使用

你可能会问:这四种方法,我到底该用哪一种?

我的答案是:全用。但不是同时用,而是分阶段用。

  • 第一阶段:基于需求。先把需求里的功能点全部覆盖掉。这是底线。
  • 第二阶段:基于模型。如果系统有复杂的状态机或数据流,建个模型,补全路径。
  • 第三阶段:随机测试。跑一批随机用例,看看有没有「意外惊喜」。
  • 第四阶段:覆盖率驱动。跑覆盖率工具,查漏补缺,直到达标。

这个流程我用了很多年,效果不错。当然,具体项目可以灵活调整。比如一个简单的开关逻辑,可能不需要建模型。但一个复杂的域控制器,这四个阶段一个都不能少。

好,这一章就到这里。下一章咱们聊聊测试执行和自动化,那又是一个大坑。到时候我给你们讲讲我当年是怎么被自动化框架坑到加班的。