第四章 麻醉机软件单元测试策略
好,咱们进入单元测试这个环节。说实话,单元测试是很多团队容易忽视的一环,觉得「差不多就行了」。但麻醉机这东西,你想想看,一个通气参数算错了,那可是要出人命的。所以我个人习惯,单元测试必须扎扎实实做,不能糊弄。
4.1 单元测试范围界定
先说说范围怎么划。很多新手上来就问:「我要测哪些函数?」其实这个问题得反过来想——哪些函数出了问题,后果最严重?
我一般按这个优先级来定范围:
- 核心算法模块:比如潮气量计算、呼吸频率控制、压力监测这些。这些是麻醉机的「心脏」,必须全覆盖。
- 安全关键函数:报警逻辑、超限保护、参数校验。我在项目中遇到过,一个报警阈值判断写反了,结果压力超标了愣是不报警。嗯,这种坑踩过一次就记住了。
- 状态机转换逻辑:麻醉机有多个工作模式(手动、自主、控制等),状态跳转错了,机器行为就全乱了。
- 数据采集与处理:传感器数据滤波、AD转换后的数值处理。这些看似简单,但噪声干扰下容易出幺蛾子。
我的经验法则:凡是涉及患者安全、参数计算、模式切换的函数,一律纳入单元测试范围。其他工具类函数、界面显示函数,可以适当降低优先级。
还有一个容易忽略的点——边界条件。比如潮气量范围是50mL到1500mL,那50和1500这两个边界值,还有稍微超出一点的值(49、1501),都得测。我曾经就因为没测边界值,结果产品在临床使用时,设置到最大潮气量时直接触发了异常保护,搞得临床医生一头雾水。
3.2 桩函数与驱动函数设计
单元测试里,桩函数和驱动函数是绕不开的。说白了,驱动函数就是「调用者」,桩函数就是「被调用者的替身」。
驱动函数设计要点:
- 负责构造测试输入数据
- 调用被测函数
- 捕获输出结果并比对预期值
桩函数设计要点:
- 模拟底层硬件行为(比如传感器返回值)
- 返回预设的测试数据
- 记录调用次数和参数,方便验证交互逻辑
举个例子,假设我们要测试一个压力校准函数 calibrate_pressure(),它内部调用了硬件读取函数 read_adc():
// 被测函数
float calibrate_pressure(void) {
uint16_t adc_val = read_adc(PRESSURE_CHANNEL);
return (float)adc_val * PRESSURE_SCALE_FACTOR;
}
// 桩函数 - 模拟ADC读取
uint16_t read_adc(uint8_t channel) {
// 预设返回值,模拟不同压力场景
static const uint16_t test_values[] = {0, 512, 1023, 2047, 4095};
static int idx = 0;
return test_values[idx++ % 5];
}
// 驱动函数
void test_calibrate_pressure(void) {
float result;
// 测试用例1:ADC值为0
result = calibrate_pressure();
assert(result == 0.0f);
// 测试用例2:ADC值为4095(满量程)
result = calibrate_pressure();
assert(result == 4095.0f * PRESSURE_SCALE_FACTOR);
printf("压力校准测试通过\n");
}
小技巧:桩函数最好设计成可配置的,方便不同测试用例复用。我习惯用一个全局结构体来管理桩函数的返回值,这样测试代码看起来清爽多了。
4.3 覆盖率要求
覆盖率这东西,说多了都是泪。很多公司只要求语句覆盖,但麻醉机这种安全关键系统,远远不够。
语句覆盖:最基本的要求,每行代码至少执行一次。但说实话,这玩意儿只能证明「代码跑过了」,不能证明「代码跑对了」。比如一个if-else分支,语句覆盖可能只测了if分支,else分支根本没测到。
分支覆盖:每个判断条件的真/假分支都要覆盖到。这个比语句覆盖强一些,但还不够。为什么?因为分支覆盖不考虑条件内部的组合情况。
MC/DC覆盖:这才是麻醉机软件应该追求的目标。MC/DC要求每个条件都能独立地影响判断结果。说白了,就是你要证明每个条件「单独起作用」。
举个例子,一个报警逻辑:
if (pressure > HIGH_LIMIT && flow < LOW_FLOW) {
trigger_alarm(ALARM_HIGH_PRESSURE);
}
MC/DC要求我们设计测试用例,使得:
- pressure > HIGH_LIMIT 为真时,flow < LOW_FLOW 的变化能改变结果
- flow < LOW_FLOW 为真时,pressure > HIGH_LIMIT 的变化能改变结果
这需要至少3个测试用例:
| pressure > HIGH_LIMIT | flow < LOW_FLOW | 结果 | 说明 |
|---|---|---|---|
| 真 | 真 | 触发报警 | 两个条件都满足 |
| 真 | 假 | 不触发 | 证明flow条件独立影响结果 |
| 假 | 真 | 不触发 | 证明pressure条件独立影响结果 |
注意:MC/DC的测试用例数量会随着条件数量增加而爆炸。对于超过5个条件的复杂表达式,我建议先重构代码,把复杂条件拆分成多个简单条件,再分别测试。这是合规的,也是明智的。
我个人习惯,麻醉机软件的单元测试覆盖率目标定成这样:
- 语句覆盖:100%(这是底线,没得商量)
- 分支覆盖:100%(所有if-else、switch-case都要走到)
- MC/DC覆盖:安全关键模块必须达到100%,非安全模块可以放宽到80%
你可能会问:「MC/DC做到100%是不是太苛刻了?」嗯,我理解你的顾虑。但你想啊,麻醉机在手术室里,患者全靠它维持呼吸。一个条件判断失误,可能就是一次医疗事故。所以,该花的功夫一点都不能省。
最后说一句,覆盖率数据只是手段,不是目的。我见过有人为了凑覆盖率,写一堆没意义的测试用例。这其实是在自欺欺人。真正重要的是,每个测试用例都要有明确的测试意图,能发现潜在的缺陷。这才是单元测试的价值所在。