第23章:测试策略:单元测试、集成测试、压力测试、混沌工程

说实话,很多做嵌入式开发的朋友,对测试这件事的态度是「能跑就行」。我以前也这么想,直到有一次在量产前夜,一个隐藏了三个月的bug突然爆发——消息队列里的指针野了,导致整个系统随机死机。那晚我盯着示波器,心里只有一个念头:测试不是成本,是保险

这一章,我就把消息队列与事件驱动架构中,最实用的四种测试策略掰开揉碎讲清楚。你想想看,代码写得再漂亮,没有测试兜底,那就是在钢丝上跳舞。

23.1 单元测试:把每个模块锁死在牢笼里

单元测试,说白了就是单独验证每个函数或模块的行为。在消息队列系统里,我们通常要测这几个核心单元:

  • 队列的入队/出队操作
  • 事件分发器的回调调用
  • 内存池的分配/释放
  • 定时器模块的触发逻辑

我个人习惯用 CmockaUnity 这类轻量级测试框架。它们不依赖操作系统,在裸机上也能跑。

核心原则:每个测试用例只测一件事。入队就只测入队,别同时测出队和内存释放。

举个例子,测试消息队列的入队函数:

#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include "msg_queue.h"

static void test_enqueue_normal(void **state) {
    msg_queue_t q;
    msg_queue_init(&q, 10);

    msg_t msg = {.id = 1, .data = "hello"};
    int ret = msg_queue_enqueue(&q, &msg);

    assert_int_equal(ret, 0);
    assert_int_equal(msg_queue_count(&q), 1);
}

static void test_enqueue_full(void **state) {
    msg_queue_t q;
    msg_queue_init(&q, 2);  // 容量只有2

    msg_t msg1 = {.id = 1};
    msg_t msg2 = {.id = 2};
    msg_t msg3 = {.id = 3};

    msg_queue_enqueue(&q, &msg1);
    msg_queue_enqueue(&q, &msg2);
    int ret = msg_queue_enqueue(&q, &msg3);

    assert_int_equal(ret, -1);  // 队列满,应该返回错误
}

int main(void) {
    const struct CMUnitTest tests[] = {
        cmocka_unit_test(test_enqueue_normal),
        cmocka_unit_test(test_enqueue_full),
    };
    return cmocka_run_group_tests(tests, NULL, NULL);
}

我的经验:写单元测试时,一定要把边界条件写全。空队列出队、满队列入队、单元素队列反复操作——这些最容易出bug的地方,恰恰是很多人懒得测的地方。

23.2 集成测试:让模块们开始对话

单元测试通过,不代表系统能跑。我曾经遇到一个案例:每个模块的单元测试全绿,但一集成就死机。原因是事件分发器在中断上下文调用了带锁的函数——单元测试里根本不会触发中断。

集成测试的目标是验证模块之间的交互。在事件驱动架构中,重点测这几条链路:

  • 生产者 → 消息队列 → 消费者
  • 定时器事件 → 事件分发器 → 回调函数
  • 中断服务程序 → 消息队列 → 任务处理

我一般会写一个集成测试场景,模拟真实的数据流:

// 集成测试:模拟传感器数据采集到处理的全链路
void test_sensor_data_pipeline(void) {
    // 1. 初始化所有模块
    msg_queue_init(&sensor_queue, 20);
    event_dispatcher_init();
    sensor_driver_init();

    // 2. 模拟传感器中断产生数据
    sensor_data_t data = {.value = 42.5, .timestamp = 100};
    sensor_driver_simulate_interrupt(&data);

    // 3. 等待事件处理(实际项目中用信号量或轮询)
    os_delay_ms(10);

    // 4. 验证最终处理结果
    assert_true(processing_result_ready());
    assert_float_equal(get_last_processed_value(), 42.5, 0.01);
}

注意:集成测试的环境要尽量接近真实硬件。如果条件不允许,至少要用硬件模拟层(HAL Mock)来模拟中断、DMA、外设寄存器等行为。

23.3 压力测试:把系统往死里逼

压力测试,就是让系统在极限负载下运行,看它什么时候崩溃。对于消息队列系统,我主要关注三个指标:

指标含义典型阈值
吞吐量每秒处理的消息数≥ 10000 msg/s(视硬件而定)
延迟消息从入队到出队的平均时间≤ 1ms(裸机) / ≤ 5ms(RTOS)
丢包率队列满时丢弃的消息比例0%(硬实时系统) / ≤ 0.1%(软实时)

我做过一个压力测试脚本,往队列里以最大速率塞消息,同时监控内存使用:

// 压力测试:持续发送100万条消息
void stress_test_throughput(void) {
    uint32_t sent = 0, received = 0;
    uint32_t start_tick = get_tick();

    while (sent < 1000000) {
        msg_t msg = {.id = sent % 256};
        if (msg_queue_enqueue(&q, &msg) == 0) {
            sent++;
        } else {
            // 队列满,记录丢包
            drop_count++;
        }
    }

    uint32_t elapsed = get_tick() - start_tick;
    printf("Sent: %u, Dropped: %u, Time: %u ms\n",
           sent, drop_count, elapsed);
    printf("Throughput: %u msg/s\n",
           sent * 1000 / elapsed);
}

关键发现:我在项目中遇到过,当队列深度超过某个阈值(比如512),内存碎片会导致分配失败。压力测试帮我提前发现了这个坑,后来改用固定大小的环形缓冲区才解决。

23.4 混沌工程:故意搞破坏

混沌工程,是测试的最高境界。它不是测系统「能不能正常工作」,而是测系统在故障中能不能优雅恢复

你想想看,一个事件驱动系统,如果突然发生这些情况:

  • 某个任务被高优先级中断抢占,长时间不返回
  • 消息队列的内存池被意外写坏
  • 定时器回调里发生了死循环
  • 两个事件同时触发,竞争同一个资源

系统会怎样?是崩溃、死锁,还是能自我修复?

我常用的混沌工程手段包括:

  1. 注入延迟:随机让某个任务睡眠 10~100ms,观察队列是否溢出
  2. 模拟内存故障:随机把某个消息的数据域改成 0xDEADBEEF
  3. 中断风暴:以最高频率触发外部中断,看事件分发器是否丢失事件
  4. 看门狗触发:故意让某个任务不喂狗,验证系统重启后的状态恢复
// 混沌测试:随机注入消息损坏
void chaos_test_corrupt_message(void) {
    for (int i = 0; i < 1000; i++) {
        msg_t msg = {.id = i, .data = "normal"};
        msg_queue_enqueue(&q, &msg);

        // 随机损坏第500条消息
        if (i == 500) {
            msg_t *p = msg_queue_peek(&q, 500);
            if (p) p->data[0] = 0xFF;  // 故意写坏
        }
    }

    // 验证系统能否检测到损坏并丢弃
    int corrupted = msg_queue_scan_for_corruption(&q);
    assert_true(corrupted > 0);
    assert_true(msg_queue_is_still_alive(&q));
}

我的建议:混沌工程不要一开始就上猛药。先从小故障开始,比如只注入一个错误,观察系统行为。慢慢增加复杂度,直到系统出现你意想不到的「惊喜」。

23.5 四种策略的协同关系

这四种测试不是孤立的。我画了一张图,帮你理清它们的关系:

测试策略金字塔 单元测试 验证每个函数/模块的独立行为 集成测试 验证模块间的交互与数据流 压力测试 验证极限负载下的吞吐、延迟、稳定性 混沌工程 故意注入故障,验证系统自愈能力 测试范围 → 越来越广 测试成本 → 越来越高

从这张图你能看到:

  • 单元测试是地基,覆盖最广,成本最低
  • 集成测试往上走一层,验证模块协作
  • 压力测试再往上,挑战系统的极限
  • 混沌工程在塔尖,测试系统的韧性

我个人习惯的配比是:70% 单元测试 + 20% 集成测试 + 8% 压力测试 + 2% 混沌工程。这个比例不是固定的,但底层测试一定要扎实,否则上层的测试就是在沙子上盖楼。

血的教训:我曾经跳过单元测试,直接做压力测试。结果发现系统在1000 msg/s时崩溃,但排查了三天才发现是入队函数里一个指针没判空。如果先写单元测试,这个bug十分钟就能揪出来。

好了,关于测试策略,我就讲这么多。记住一句话:测试不是做完代码后的附加项,而是写代码时就该想好的事。你的消息队列系统能跑多稳、能扛多狠,全看测试这关过得硬不硬。


公众号:蓝海资料掘金营,微信 deep3321