第四章 麻醉机软件单元测试策略

好,咱们进入单元测试这个环节。说实话,单元测试是很多团队容易忽视的一环,觉得「差不多就行了」。但麻醉机这东西,你想想看,一个通气参数算错了,那可是要出人命的。所以我个人习惯,单元测试必须扎扎实实做,不能糊弄。

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%是不是太苛刻了?」嗯,我理解你的顾虑。但你想啊,麻醉机在手术室里,患者全靠它维持呼吸。一个条件判断失误,可能就是一次医疗事故。所以,该花的功夫一点都不能省。

最后说一句,覆盖率数据只是手段,不是目的。我见过有人为了凑覆盖率,写一堆没意义的测试用例。这其实是在自欺欺人。真正重要的是,每个测试用例都要有明确的测试意图,能发现潜在的缺陷。这才是单元测试的价值所在。