[toolschain] UT 使用Mock 工具方法记录 来自与Ai问答
问题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 的核心优势
-
解耦被测代码与外部依赖
→ 测试只关注逻辑,不关心硬件是否在线。 -
精准控制输入和行为
→ 可模拟“信号突然中断”、“返回 NaN”等极端情况。 -
提高测试覆盖率
→ 能覆盖错误路径、边界条件,提升代码健壮性。 -
加速 CI/CD 流程
→ 单元测试秒级完成,无需等待硬件启动。 -
支持 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 工程结构模板,帮助你在实际项目中落地这种模式 😊
更多推荐

所有评论(0)