3. 模型在环(MIL)基础
MIL(Model-in-the-Loop)测试,说白了就是我们在纯仿真环境里,对着算法模型做验证。这一步要是没做好,后面HIL台架一跑,问题全暴露出来,返工成本就高了。我个人习惯,在MIL阶段把测试用例设计扎实,后面能省一半的调试时间。
这一章,我们重点聊三件事:怎么从需求文档里提炼出测试点,怎么用等价类和边界值把输入空间覆盖好,以及怎么看覆盖率报告才知道“测够了没”。
3.1 基于需求的测试用例设计
很多工程师拿到需求文档,第一反应是“直接写用例”。我建议你先别急。需求里经常藏着模糊的描述,比如“在适当条件下激活”。什么叫适当?你得把它翻译成可测量的条件。
我一般分三步走:
- 拆解功能需求:把一句话需求拆成“输入→处理→输出”的原子逻辑。
- 识别触发条件:明确什么信号变化、什么阈值到达,才会激活功能。
- 定义预期结果:输出信号的值范围、变化速率、持续时间,必须量化。
举个例子:需求写“当车速超过120km/h时,点亮超速报警灯”。
- 输入:车速信号(单位km/h)
- 触发条件:车速 > 120 km/h
- 预期结果:报警灯状态 = 1(点亮)
- 额外检查:车速回落到115km/h时,灯是否熄灭?(这里涉及迟滞)
你看,一个简单的需求,拆开后就发现需要测不止一条用例。我在项目中遇到过,需求里只写了“超过阈值报警”,结果实车测试时发现车速在阈值附近来回跳,灯一闪一闪的。后来才补了迟滞逻辑的测试。嗯,这就是需求不明确的坑。
3.2 等价类与边界值分析
等价类划分,说白了就是把输入空间分成几个“等效”的区间。你只要在每个区间里选一个代表值测试,就能覆盖整个区间。边界值分析则是盯着区间的边缘——因为软件最容易在边界处出问题。
我习惯把这两者结合起来用。还是用超速报警的例子:
| 输入范围 | 等价类 | 代表值 | 边界值 |
|---|---|---|---|
| 0 ~ 120 km/h | 有效等价类(不报警) | 60 km/h | 0, 119, 120 |
| 120 ~ 250 km/h | 有效等价类(报警) | 180 km/h | 120, 121, 250 |
| < 0 km/h | 无效等价类 | -10 km/h | -1 |
| > 250 km/h | 无效等价类 | 300 km/h | 251 |
我的经验:边界值不要只测“等于边界”这一个点。我建议测三个点:边界值-1、边界值、边界值+1。比如120这个边界,测119、120、121。这样能抓到“大于等于”和“大于”写错的情况。
为什么会这样?因为需求里写“超过120”时,开发可能理解成“>=120”,也可能理解成“>120”。你测了119和121,就能区分这两种实现。我曾经就因为没测119,漏掉了一个“>=”写成了“>”的bug,到HIL阶段才抓出来,多花了两天排查时间。
3.3 MIL测试覆盖率分析
用例写完了,跑一遍模型,覆盖率报告出来了。但很多人只看一个“100%”就觉得完事了。其实不是这样。MIL阶段的覆盖率,我主要关注三种:
- 需求覆盖率:每条需求是否至少有一条测试用例对应?
- 模型结构覆盖率:模型的每个分支、每个状态是否都被执行过?
- MC/DC覆盖率:每个条件的真值和假值是否都独立影响了判断结果?
我个人认为,MC/DC覆盖率是MIL阶段最有价值的指标。它能发现“某个条件其实从来没起作用”的情况。比如一个逻辑表达式里,某个输入信号始终为真,那它其实是个冗余条件。
注意:覆盖率100%不代表功能正确。它只说明你测过的代码都跑了,但没测到的逻辑可能还藏着bug。我见过一个项目,模型覆盖率98%,但剩下的2%恰好是故障处理分支。结果实车遇到那个故障时,系统直接崩溃了。
下面这张图,是我自己总结的MIL测试用例设计流程,你可以参考一下:
执行完测试后,覆盖率报告会告诉你哪些分支没跑到。这时候别急着补用例,先问自己:那个分支是正常逻辑还是异常处理?如果是异常处理,你得构造对应的故障注入条件。如果是正常逻辑,那说明你的等价类划分漏了某个区间。
一个小技巧:我习惯在写用例之前,先列一个“覆盖率目标清单”。比如:
- 所有if-else分支至少执行一次
- 所有状态机的状态至少进入一次
- 所有查找表的索引范围边界都测到
这样跑完覆盖率报告后,直接对照清单打勾,效率高很多。
最后说一句,MIL测试用例设计不是一次性工作。模型迭代了,需求变更了,你得回头更新用例。我见过团队把MIL用例写完后就不管了,模型改了三版,用例还是老的。那覆盖率报告再好看,也说明不了问题。