问题1 mock cpp 编写单元测试(UT)的完整例子,并详细说明 为什么需要使用 Mock 方法,以及它的必要性

当然可以!下面是一个使用 C++Google Mock(gMock) 编写单元测试(UT)的完整例子,并详细说明 为什么需要使用 Mock 方法,以及它的必要性。


🎯 场景描述

我们有一个汽车雷达模块中的类 BlockageDetector,它依赖于一个硬件接口 RadarSensor 来获取原始数据。我们要为 BlockageDetector 写单元测试。

但我们不能在测试中直接调用真实的雷达传感器——因为它:

  • 是物理设备,不可控
  • 返回值不稳定(受环境影响)
  • 测试无法覆盖异常情况(如信号丢失)

👉 所以我们需要 Mock(模拟) 这个依赖。


✅ 步骤 1:定义接口类(用于依赖注入)

// radar_sensor.h
#pragma once
#include <string>

class RadarSensor {
public:
    virtual ~RadarSensor() = default;
    virtual double getSignalStrength() const = 0;  // 虚函数,便于 mock
    virtual std::string getStatus() const = 0;
};

✅ 步骤 2:实现被测类(依赖接口)

// blockage_detector.h
#pragma once
#include "radar_sensor.h"

class BlockageDetector {
private:
    const RadarSensor& sensor_;  // 通过引用依赖抽象接口

public:
    explicit BlockageDetector(const RadarSensor& sensor) : sensor_(sensor) {}

    bool isBlocked() const {
        std::string status = sensor_.getStatus();
        if (status == "DISCONNECTED") {
            return true;  // 断开连接视为堵塞
        }
        double strength = sensor_.getSignalStrength();
        return strength < 0.5;  // 信号低于阈值也视为堵塞
    }
};

✅ 步骤 3:使用 gMock 创建 Mock 类

// mock_radar_sensor.h
#pragma once
#include "radar_sensor.h"
#include <gmock/gmock.h>

class MockRadarSensor : public RadarSensor {
public:
    MOCK_CONST_METHOD0(getSignalStrength, double());
    MOCK_CONST_METHOD0(getStatus, std::string());
};

🔹 MOCK_CONST_METHOD0 表示无参数、const 的虚函数
🔹 gMock 自动为你生成可控制的行为和期望断言


✅ 步骤 4:编写单元测试(使用 Mock)

// blockage_detector_test.cc
#include "blockage_detector.h"
#include "mock_radar_sensor.h"
#include <gtest/gtest.h>

using ::testing::Return;
using ::testing::Test;

class BlockageDetectorTest : public Test {
protected:
    MockRadarSensor mock_sensor_;
    BlockageDetector detector_{mock_sensor_};
};

TEST_F(BlockageDetectorTest, ShouldReturnTrue_WhenSignalIsWeak) {
    // Arrange: 设置 mock 行为 —— 模拟弱信号
    EXPECT_CALL(mock_sensor_, getSignalStrength())
        .WillOnce(Return(0.3));  // 返回低信号
    EXPECT_CALL(mock_sensor_, getStatus())
        .WillOnce(Return("CONNECTED"));

    // Act
    bool blocked = detector_.isBlocked();

    // Assert
    EXPECT_TRUE(blocked);
}

TEST_F(BlockageDetectorTest, ShouldReturnFalse_WhenSignalIsStrong) {
    // Arrange
    EXPECT_CALL(mock_sensor_, getSignalStrength())
        .WillOnce(Return(0.8));
    EXPECT_CALL(mock_sensor_, getStatus())
        .WillOnce(Return("CONNECTED"));

    // Act
    bool blocked = detector_.isBlocked();

    // Assert
    EXPECT_FALSE(blocked);
}

TEST_F(BlockageDetectorTest, ShouldReturnTrue_WhenDisconnected) {
    // Arrange: 模拟断开连接状态,即使信号正常也不可信
    EXPECT_CALL(mock_sensor_, getStatus())
        .WillOnce(Return("DISCONNECTED"));
    // 注意:这里不会调用 getSignalStrength()

    // Act
    bool blocked = detector_.isBlocked();

    // Assert
    EXPECT_TRUE(blocked);
}

✅ 构建与运行(Bazel 示例)

如果你使用 Bazel,BUILD 文件类似:

cc_test(
    name = "blockage_detector_test",
    srcs = ["blockage_detector_test.cc"],
    deps = [
        ":blockage_detector",
        "@com_google_googletest//:gtest_main",
        "@com_google_googletest//:gmock",
    ],
)

运行测试:

bazel test //ecu/ipnext/domains/mrr/00_mrr_tracker:blockage_detector_test

❓ 为什么要使用 Mock?—— 必要性分析

问题 使用真实对象 使用 Mock
依赖外部设备(如雷达) ❌ 不稳定、难复现 ✅ 可模拟任意状态
测试异常场景困难 ❌ 真实设备很少返回错误码 ✅ 可设定返回值、抛异常
测试速度慢 ❌ 每次需通信初始化 ✅ 内存中快速执行
测试可重复性差 ❌ 外界干扰影响结果 ✅ 完全可控
并行测试受限 ❌ 多测试争抢硬件资源 ✅ 完全独立

✅ 使用 Mock 的核心优势

  1. 解耦被测代码与外部依赖
    → 测试只关注逻辑,不关心硬件是否在线。

  2. 精准控制输入和行为
    → 可模拟“信号突然中断”、“返回 NaN”等极端情况。

  3. 提高测试覆盖率
    → 能覆盖错误路径、边界条件,提升代码健壮性。

  4. 加速 CI/CD 流程
    → 单元测试秒级完成,无需等待硬件启动。

  5. 支持 TDD(测试驱动开发)
    → 即使硬件接口尚未实现,也可先写测试和业务逻辑。


🚫 如果不使用 Mock 会怎样?

// 错误做法:直接依赖真实传感器
class BlockageDetector {
    RealRadarSensor sensor;  // ❌ 紧耦合!
public:
    bool isBlocked() {
        auto strength = sensor.getSignalStrength();  // 测试时可能崩溃或超时
        return strength < 0.5;
    }
};

后果:

  • 单元测试必须连接真实雷达 → 无法自动化
  • CI 构建失败频繁(硬件问题)
  • 开发效率极低

✅ 总结:Mock 的必要性

Mock 不是“可选技巧”,而是现代 C++ 单元测试的基石。

尤其是在车载软件开发中:

  • 硬件昂贵、资源紧张(如你提到的“vehicle resource is quite limited”)
  • 安全关键系统要求高覆盖率(ASIL B/C/D)
  • 需要在无实车环境下完成大部分验证

✅ 因此,使用 Mock 是实现高效、可靠、自动化测试的必要手段


如果你想,我也可以提供这个例子的完整 GitHub 项目结构或 Bazel 配置模板。欢迎继续提问!

问题2 还可以怎么设置mock行为

非常好的问题!在使用 Google Mock(gMock)时,EXPECT_CALL 提供了非常丰富的行为设置方式,远不止 .WillOnce(Return(...))。下面我来为你详细介绍 常见的 mock 行为设置方法,并结合你的 C++ 单元测试场景举例说明。


✅ 一、基础语法结构

EXPECT_CALL(mock_object, method(matchers))
    .Times(...)           // 调用次数约束(可选)
    .WillOnce(action)     // 第一次调用返回什么
    .WillRepeatedly(action); // 后续调用返回什么

如果不写 .Times(),默认是 1 次。


✅ 二、常用 .WillOnce().WillRepeatedly() 行为

1. Return(value) —— 返回固定值(你已知)

EXPECT_CALL(mock_sensor_, getSignalStrength())
    .WillOnce(Return(0.3));

👉 适用于模拟正常/异常的返回值。


2. ReturnRef(variable) —— 返回引用(避免拷贝)

std::string status = "CONNECTED";
EXPECT_CALL(mock_sensor_, getStatus())
    .WillOnce(ReturnRef(status));  // 返回对 status 的引用

✅ 适合大对象或需要保持同一实例的情况。


3. ReturnPointee(pointer) —— 返回指针指向的值

double strength = 0.4;
EXPECT_CALL(mock_sensor_, getSignalStrength())
    .WillOnce(ReturnPointee(&strength));  // 返回 *(&strength) => 0.4

4. Throw(exception) —— 模拟抛出异常

EXPECT_CALL(mock_sensor_, getStatus())
    .WillOnce(Throw(std::runtime_error("Sensor timeout")));

然后你可以测试你的代码是否能正确处理异常:

TEST_F(BlockageDetectorTest, HandlesSensorException) {
    EXPECT_CALL(mock_sensor_, getStatus())
        .WillOnce(Throw(std::runtime_error("Sensor timeout")));

    // 假设 isBlocked() 有 try-catch,否则测试会崩溃
    EXPECT_TRUE(detector_.isBlocked());  // 或者根据逻辑设计预期结果
}

⚠️ 注意:只有当被测函数能捕获异常时才安全使用。


5. Invoke(callback) —— 调用自定义函数或 lambda

a. 使用函数指针:
double generateWeakSignal() {
    return 0.2 + (rand() % 10) * 0.05;  // 随机弱信号
}

EXPECT_CALL(mock_sensor_, getSignalStrength())
    .WillRepeatedly(Invoke(generateWeakSignal));
b. 使用 Lambda(更灵活):
EXPECT_CALL(mock_sensor_, getSignalStrength())
    .WillRepeatedly([]() -> double {
        static int count = 0;
        return count++ < 2 ? 0.1 : 0.9;  // 前两次弱信号,第三次强信号
    });

👉 可以实现状态变化逻辑,比如模拟“信号逐渐恢复”。


6. DoAll(actions...) —— 执行多个动作(如返回 + 日志)

#include <iostream>
using ::testing::DoAll;
using ::testing::WithArg;
using ::testing::Invoke;

EXPECT_CALL(mock_sensor_, getStatus())
    .WillOnce(DoAll(
        WithArg<0>([](const std::string& s) {
            std::cout << "[MOCK] Sensor status called, returning: " << s << std::endl;
        }),
        Return("DISCONNECTED")
    ));

👉 可用于调试 mock 是否被调用。


7. SetArgReferee<N>(value) —— 修改输出参数(适用于非 const 引用参数)

假设接口改为:

virtual bool getSignalStrength(double& out_strength) const = 0;

则可以这样 mock:

EXPECT_CALL(mock_sensor_, getSignalStrength(::testing::_))
    .WillOnce(DoAll(
        SetArgReferee<0>(0.3),  // 给第一个参数(out_strength)赋值为 0.3
        Return(true)
    ));

8. .Times() 控制调用次数

用法 说明
.Times(1) 必须调用一次(默认)
.Times(2) 必须调用两次
.Times(AtLeast(1)) 至少一次
.Times(AtMost(3)) 最多三次
.Times(AnyNumber()) 任意次数(慎用,可能掩盖错误)
EXPECT_CALL(mock_sensor_, getStatus())
    .Times(AtLeast(1))
    .WillRepeatedly(Return("CONNECTED"));

✅ 三、高级技巧示例

示例:模拟“前两次失败,第三次成功”的重试逻辑

EXPECT_CALL(mock_sensor_, getStatus())
    .WillOnce(Return("ERROR"))
    .WillOnce(Return("ERROR"))
    .WillRepeatedly(Return("CONNECTED"));

可用于测试你的 BlockageDetector 是否具备容错或重试机制。


示例:根据输入参数返回不同值(配合匹配器)

using ::testing::_;
using ::testing::Lt;

EXPECT_CALL(mock_sensor_, getSignalStrength())
    .WillOnce(Return(0.1));  // 默认弱信号

// 更复杂的:根据其他条件判断(虽然本例不适用,但可拓展)

如果你的方法带参数,可以用匹配器动态响应:

EXPECT_CALL(mock_sensor_, setValue(Lt(0)))
    .WillOnce(Throw(std::invalid_argument("Negative input")));

✅ 四、最佳实践建议

建议 说明
✅ 使用 .WillOnce(...).WillRepeatedly(...) 明确行为 避免意外行为
✅ 尽量不要用 .Times(AnyNumber()) 容易隐藏调用次数 bug
✅ 在析构前确保所有 EXPECT_CALL 满足 否则测试失败
✅ 优先使用接口抽象 + 依赖注入 才能有效 mock
✅ 避免 mock 太多层级(避免过度设计) 只 mock 直接依赖

✅ 总结:你可以怎么设置 Mock 行为?

方法 用途
Return(value) 最常用,返回固定值
ReturnRef(var) 返回引用,避免拷贝
Throw(ex) 测试异常处理路径
Invoke(func) 自定义逻辑(函数/lambda)
DoAll(a1, a2) 多个动作组合
SetArgReferee<N> 设置输出参数
WillRepeatedly(...) 设置重复行为
结合 Times() 和 匹配器 精确控制调用条件

如果你想看一个完整工程中如何组织这些 mock 行为(比如在 CI 中运行),我也可以提供模板项目结构 😊

继续问吧,mock 掌握好了,UT 就成功了一大半!

问题3 为什么经常要为mock对象创建一个纯虚父类

这是一个非常关键、也非常有深度的问题!

“为什么我们经常要为 mock 对象创建一个纯虚父类(即抽象接口)?”

简单回答是:

为了实现依赖倒置和解耦,让被测代码依赖于“抽象”而非“具体实现”,从而可以被 Mock 替换。


🎯 举个真实例子回顾

假设你有一个雷达传感器的类:

class RadarSensor {
public:
    double getSignalStrength() const;  // 普通成员函数
};

然后你的 BlockageDetector 直接使用它:

class BlockageDetector {
    RadarSensor sensor_;  // 直接依赖具体类
public:
    bool isBlocked() {
        return sensor_.getSignalStrength() < 0.5;
    }
};

❌ 问题来了:你怎么 mock RadarSensor

  • 它没有虚函数。
  • 你无法继承并重写它的行为。
  • gMock 也无法生成 MOCK_METHOD
  • 你只能用真实硬件对象测试 → 单元测试失败!

✅ 解决方案:引入纯虚基类(抽象接口)

我们重构代码,先定义一个接口类

// radar_sensor.h
class RadarSensor {
public:
    virtual ~RadarSensor() = default;

    virtual double getSignalStrength() const = 0;  // 纯虚函数
    virtual std::string getStatus() const = 0;
};

这个类:

  • 只有纯虚函数(= 0
  • 没有成员变量
  • 不提供实现
  • 是一个抽象接口

然后让真实类和 mock 类都继承它

✅ 真实实现
class RealRadarSensor : public RadarSensor {
public:
    double getSignalStrength() const override {
        // 实际读取硬件...
        return read_hardware_signal();
    }

    std::string getStatus() const override {
        return is_connected() ? "CONNECTED" : "DISCONNECTED";
    }
};
✅ Mock 实现(用于测试)
#include <gmock/gmock.h>

class MockRadarSensor : public RadarSensor {
public:
    MOCK_CONST_METHOD0(getSignalStrength, double());
    MOCK_CONST_METHOD0(getStatus, std::string());
};

被测代码依赖接口,而不是具体类

class BlockageDetector {
    const RadarSensor& sensor_;  // 依赖抽象接口!

public:
    explicit BlockageDetector(const RadarSensor& sensor) : sensor_(sensor) {}

    bool isBlocked() const {
        return sensor_.getSignalStrength() < 0.5;
    }
};

✅ 现在你可以自由地注入 Mock!

TEST(BlockageDetectorTest, DetectsWeakSignal) {
    MockRadarSensor mock_sensor;
    BlockageDetector detector(mock_sensor);

    EXPECT_CALL(mock_sensor, getSignalStrength())
        .WillOnce(Return(0.3));

    EXPECT_TRUE(detector.isBlocked());
}

🔍 为什么要这样做?—— 核心原因分析

原因 说明
1. 支持多态替换(Polymorphic Substitution) 运行时可以用 MockRadarSensor 替代 RealRadarSensor,只要它们继承同一个接口。这是 OOP 的核心能力。
2. 实现依赖注入(Dependency Injection) 被测代码不自己创建对象,而是由外部传入,便于控制依赖。
3. 隔离外部依赖(Isolation) 测试只关注业务逻辑,不关心硬件、网络、数据库是否可用。
4. 提高可测试性(Testability) 没有接口就无法 mock,没有 mock 就很难写真正的单元测试。
5. 符合 SOLID 原则 特别是:
D: 依赖倒置原则(Depend on abstractions, not concretions)
O: 开闭原则(对扩展开放,对修改关闭)

🚫 如果不这么做会怎样?

后果 描述
❌ 无法 mock gMock 要求方法是虚函数才能重写
❌ 测试依赖真实设备 导致测试慢、不稳定、不可重复
❌ 无法模拟错误场景 比如“信号丢失”、“超时”等难以在实车上复现
❌ 难以自动化 CI/CD 流水线中无法运行
❌ 违反单元测试定义 单元测试应快速、独立、可重复,依赖真实硬件就变成了集成测试

💡 补充:不是所有类都需要接口,但这些情况建议加

场景 是否建议抽象接口
访问硬件(雷达、摄像头、CAN 总线) ✅ 强烈建议
调用外部服务(REST API、数据库) ✅ 建议
时间相关(std::time() ✅ 可封装为 ClockInterface
日志系统 ✅ 方便屏蔽输出或验证日志内容
算法内部计算类(纯逻辑) ❌ 不需要,直接测试即可

✅ 最佳实践总结

// 接口命名建议:以 Interface 结尾 或 I 开头
class SensorInterface { ... };

// 或者更自然的命名(如前面的 RadarSensor)
class RadarSensor {
public:
    virtual ~RadarSensor() = default;
    virtual double getSignalStrength() const = 0;
};

然后:

  • 生产代码中传入 RealRadarSensor
  • 测试代码中传入 MockRadarSensor
  • 被测类只认识 RadarSensor

🧠 一句话总结

“我们为 mock 创建纯虚父类,不是为了 mock 而 mock,而是为了让代码具备可测试性、可维护性和灵活性。”

这不仅是技术手段,更是一种设计思想的体现


如果你想,我可以提供一个完整的 CMake + gTest + gMock 工程结构模板,帮助你在实际项目中落地这种模式 😊

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐