Skip to content

Codelab: Mock 硬件依赖

预计时间:20 分钟

学习目标

完成本教程后,你将能够:

  • 理解 Mock 的工作原理
  • 使用用户实现模式 Mock 函数
  • 使用 mock() API 验证调用
  • 使用声明式模式配置 Mock

场景描述

你正在开发一个嵌入式温度监控系统。系统需要:

  1. 读取温度传感器
  2. 检查温度是否超过阈值
  3. 触发告警

问题是:没有物理硬件,如何测试?

解决方案:使用 Mock 模拟硬件。

步骤 1: 创建项目结构

bash
mkdir -p sensor-demo/{src,include,build,tests}
cd sensor-demo

步骤 2: 编写硬件抽象层

创建 include/sensor.h

c
#ifndef SENSOR_H
#define SENSOR_H

// 硬件抽象层(HAL)- 读取传感器
int read_sensor(int sensor_id);

// 业务逻辑 - 检查温度
// 返回: 0=正常, 1=超温, -1=读取错误
int check_temperature(int sensor_id, int threshold);

// 业务逻辑 - 获取平均温度
int get_average_temperature(int sensor_id, int samples);

#endif

创建 src/sensor_hw.c(硬件实现):

c
#include "sensor.h"

// 实际硬件读取(测试时会被 Mock)
int read_sensor(int sensor_id) {
    // 真实硬件读取代码
    // 这里返回 -1 表示没有实际硬件
    return -1;
}

创建 src/sensor.c(业务逻辑):

c
#include "sensor.h"

int check_temperature(int sensor_id, int threshold) {
    int temp = read_sensor(sensor_id);

    if (temp < 0) {
        return -1;  // 读取错误
    }

    return temp > threshold ? 1 : 0;
}

int get_average_temperature(int sensor_id, int samples) {
    int sum = 0;
    int valid_count = 0;

    for (int i = 0; i < samples; i++) {
        int temp = read_sensor(sensor_id);
        if (temp >= 0) {
            sum += temp;
            valid_count++;
        }
    }

    if (valid_count == 0) {
        return -1;  // 没有有效读数
    }

    return sum / valid_count;
}

步骤 3: 编译源代码

bash
gcc -c src/sensor.c -o build/sensor.o -Iinclude
gcc -c src/sensor_hw.c -o build/sensor_hw.o -Iinclude

步骤 4: 编写 Mock 测试(用户实现模式)

创建 tests/test_sensor.c

c
#include "sensor.h"
#include <anvil/mock.h>

// Mock 状态变量
static int mock_temperature = 25;

// 用户实现模式 - Mock read_sensor
__attribute__((mock))
int read_sensor(int sensor_id) {
    return mock_temperature;
}

// 初始化:每个测试前重置状态
__attribute__((test_initialize))
int setup(void) {
    mock_temperature = 25;  // 默认温度
    mock(read_sensor).reset();
    return 0;
}

/*
 * 设计说明:测试正常温度(低于阈值)
 * 预期结果:25度低于80阈值,返回0
 */
__attribute__((test_method))
int test_temp_normal(void) {
    mock_temperature = 25;
    int result = check_temperature(0, 80);
    return result == 0;
}

/*
 * 设计说明:测试高温(超过阈值)
 * 预期结果:85度超过80阈值,返回1
 */
__attribute__((test_method))
int test_temp_high(void) {
    mock_temperature = 85;
    int result = check_temperature(0, 80);
    return result == 1;
}

/*
 * 设计说明:测试边界值(等于阈值)
 * 预期结果:80度等于80阈值,返回0(不超过)
 */
__attribute__((test_method))
int test_temp_boundary(void) {
    mock_temperature = 80;
    int result = check_temperature(0, 80);
    return result == 0;
}

/*
 * 设计说明:验证传感器ID正确传递
 * 预期结果:调用时使用传感器ID 5
 */
__attribute__((test_method))
int test_sensor_id_passed(void) {
    check_temperature(5, 80);

    // 验证调用参数
    int passed_id = mock(read_sensor).arg0_val;
    return passed_id == 5;
}

/*
 * 设计说明:测试调用次数
 * 预期结果:get_average_temperature 调用 read_sensor 5次
 */
__attribute__((test_method))
int test_call_count(void) {
    mock_temperature = 30;
    get_average_temperature(0, 5);

    return mock(read_sensor).call_count == 5;
}

// includes: -I../include

步骤 5: 运行测试

bash
anvil

预期输出:

Discovered 5 test cases in 1 file.
test_sensor::test_temp_normal ......... PASSED
test_sensor::test_temp_high ........... PASSED
test_sensor::test_temp_boundary ....... PASSED
test_sensor::test_sensor_id_passed .... PASSED
test_sensor::test_call_count .......... PASSED

All 5 tests passed.

步骤 6: 模拟错误情况

tests/test_sensor.c 中添加:

c
/*
 * 设计说明:测试读取错误处理
 * 预期结果:读取失败时返回-1
 */
__attribute__((test_method))
int test_read_error(void) {
    mock_temperature = -1;  // 模拟读取错误
    int result = check_temperature(0, 80);
    return result == -1;
}

步骤 7: 使用声明式模式

创建另一个测试文件 tests/test_sensor_declarative.c

c
#include "sensor.h"
#include <anvil/mock.h>

// 声明式模式 - 仅声明,不提供函数体
__attribute__((mock))
int read_sensor(int sensor_id);

__attribute__((test_initialize))
int setup(void) {
    mock(read_sensor).reset();
    return 0;
}

/*
 * 设计说明:使用 set_return 测试正常温度
 * 预期结果:返回设定的温度值
 */
__attribute__((test_method))
int test_set_return(void) {
    mock(read_sensor).set_return(25);

    int result = check_temperature(0, 80);
    return result == 0;
}

/*
 * 设计说明:使用 set_return 测试高温
 * 预期结果:超过阈值时返回1
 */
__attribute__((test_method))
int test_set_return_high(void) {
    mock(read_sensor).set_return(90);

    int result = check_temperature(0, 80);
    return result == 1;
}

/*
 * 设计说明:使用自定义函数
 * 预期结果:使用自定义逻辑返回值
 */
static int custom_sensor_read(int sensor_id) {
    // 根据 sensor_id 返回不同温度
    return 20 + sensor_id * 10;
}

__attribute__((test_method))
int test_set_custom(void) {
    mock(read_sensor).set_custom(custom_sensor_read);

    // sensor_id=0 -> 20度
    // sensor_id=3 -> 50度
    return read_sensor(0) == 20 && read_sensor(3) == 50;
}

// includes: -I../include

步骤 8: 运行所有测试

bash
anvil

现在应该看到两个测试文件的结果。

步骤 9: 验证调用历史

tests/test_sensor.c 中添加:

c
/*
 * 设计说明:验证多次调用的参数历史
 * 预期结果:每次调用使用正确的参数
 */
__attribute__((test_method))
int test_call_history(void) {
    mock_temperature = 25;

    // 使用不同的传感器ID调用
    check_temperature(1, 80);
    check_temperature(2, 80);
    check_temperature(3, 80);

    // 验证历史调用参数
    int id1 = *(int *)mock(read_sensor).history(0, 0);
    int id2 = *(int *)mock(read_sensor).history(1, 0);
    int id3 = *(int *)mock(read_sensor).history(2, 0);

    return id1 == 1 && id2 == 2 && id3 == 3;
}

知识点总结

你已学会:

技能说明
Mock 原理使用 --wrap 链接器选项拦截函数
用户实现模式提供完整的 Mock 函数体
声明式模式仅声明,使用 set_return/set_custom
mock().reset()重置调用计数和参数历史
mock().arg0_val获取最后调用的参数
mock().call_count获取调用次数
mock().history(i, n)获取历史调用参数

完整项目结构

sensor-demo/
├── src/
│   ├── sensor.c
│   └── sensor_hw.c
├── include/
│   └── sensor.h
├── build/
│   ├── sensor.o
│   └── sensor_hw.o
└── tests/
    ├── test_sensor.c
    └── test_sensor_declarative.c

下一步

本页面内容遵循 Luna 软件源代码授权条款 (LSLA) 发布