如何为标记为final的VelocitySensor类编写Mock以测试SensorClient
final类VelocitySensor指针的SensorClient写单元测试的可行方案 场景说明
要给SensorClient写单元测试,但这个类持有final修饰的VelocitySensor指针,而且有两个限制:不能改VelocitySensor的代码,也不想用友元或者#define这类投机取巧的手段。下面是几种可行的解决方案,以及各自的优劣分析,最后补充代理类思路和对代码覆盖的看法。
方案1:把SensorClient改成模板类
实现方式
把SensorClient重构为模板类,将VelocitySensor作为模板参数传入。测试时,自己实现一个和VelocitySensor接口完全一致的MockVelocitySensor,用它替代真实类作为模板参数注入到SensorClient里,就能自由控制模拟对象的行为了。
示例代码
// 重构后的模板化SensorClient template<typename SensorType> class SensorClient { private: SensorType* sensor; public: SensorClient(SensorType* s) : sensor(s) {} double getVelocity() { return sensor->readVelocity(); } }; // 测试用的Mock类(用gmock框架举例) class MockVelocitySensor { public: MOCK_METHOD(double, readVelocity, (), (const)); }; // 测试用例 TEST(SensorClientTest, GetVelocityReturnsMockValue) { MockVelocitySensor mockSensor; EXPECT_CALL(mockSensor, readVelocity()).WillOnce(Return(10.0)); SensorClient<MockVelocitySensor> client(&mockSensor); ASSERT_EQ(client.getVelocity(), 10.0); }
优劣对比
- 👍 优势:改动成本低,测试时能完全掌控依赖的输出,核心业务逻辑不用动,只是调整类的定义形式。
- 👎 劣势:必须修改
SensorClient的类定义,从普通类转为模板类,原有调用代码可能需要调整(比如用typedef SensorClient<VelocitySensor> RealSensorClient;简化调用)。
方案2:适配器模式
实现方式
先定义一个抽象的传感器接口AbstractSensor,让真实的VelocitySensor通过一个适配器类VelocitySensorAdapter适配到这个接口;同时写一个测试用的MockSensor实现该接口。然后修改SensorClient,让它持有AbstractSensor指针,而不是直接持有VelocitySensor指针。
示例代码
// 抽象传感器接口 class AbstractSensor { public: virtual ~AbstractSensor() = default; virtual double readVelocity() const = 0; }; // 适配真实VelocitySensor的适配器 class VelocitySensorAdapter : public AbstractSensor { private: VelocitySensor* realSensor; public: VelocitySensorAdapter(VelocitySensor* s) : realSensor(s) {} double readVelocity() const override { return realSensor->readVelocity(); } }; // 测试用的Mock类 class MockSensor : public AbstractSensor { public: MOCK_METHOD(double, readVelocity, (), (const)); }; // 修改后的SensorClient class SensorClient { private: AbstractSensor* sensor; public: SensorClient(AbstractSensor* s) : sensor(s) {} double getVelocity() { return sensor->readVelocity(); } }; // 生产环境使用 VelocitySensor realSensor; VelocitySensorAdapter adapter(&realSensor); SensorClient client(&adapter); // 测试环境使用 MockSensor mockSensor; EXPECT_CALL(mockSensor, readVelocity()).WillOnce(Return(20.0)); SensorClient testClient(&mockSensor); ASSERT_EQ(testClient.getVelocity(), 20.0);
优劣对比
- 👍 优势:原有业务代码调用
SensorClient的方式基本不用改(只需要用适配器包一下真实对象),符合依赖倒置原则,后续加其他传感器也更方便。 - 👎 劣势:要新增抽象接口和适配器类,代码量增加;必须修改
SensorClient的成员变量类型,从VelocitySensor*换成AbstractSensor*。
方案3:Pimpl(指针实现)模式
实现方式
把SensorClient的具体实现隐藏到一个私有内部类Impl里,Impl持有VelocitySensor指针。测试时,写一个测试专用的MockImpl,替换掉原来的Impl,就能注入模拟逻辑了。
示例代码
// SensorClient头文件 class SensorClient { private: class Impl; std::unique_ptr<Impl> pImpl; public: SensorClient(VelocitySensor* sensor); ~SensorClient(); double getVelocity(); // 测试专用构造函数,仅在测试编译时启用 #ifdef TEST_BUILD SensorClient(std::unique_ptr<Impl> impl); #endif }; // 生产环境的Impl实现 class SensorClient::Impl { private: VelocitySensor* sensor; public: Impl(VelocitySensor* s) : sensor(s) {} double getVelocity() { return sensor->readVelocity(); } }; // SensorClient的构造和析构实现 SensorClient::SensorClient(VelocitySensor* sensor) : pImpl(std::make_unique<Impl>(sensor)) {} SensorClient::~SensorClient() = default; double SensorClient::getVelocity() { return pImpl->getVelocity(); } // 测试用的MockImpl class MockImpl { public: MOCK_METHOD(double, getVelocity, ()); }; // 测试用例 TEST(SensorClientTest, GetVelocityUsesMockImpl) { auto mockImpl = std::make_unique<NiceMock<MockImpl>>(); EXPECT_CALL(*mockImpl, getVelocity()).WillOnce(Return(15.0)); SensorClient client(std::move(mockImpl)); ASSERT_EQ(client.getVelocity(), 15.0); }
优劣对比
- 👍 优势:
SensorClient对外的接口完全不变,原有调用代码不用改;实现细节完全隐藏,测试时只需要替换内部的Impl对象即可。 - 👎 劣势:得重构
SensorClient的结构,引入Pimpl模式会增加代码复杂度;如果没预留测试专用构造函数,可能需要借助测试工具访问私有成员(但这比友元温和)。
补充方案:代理类思路
给VelocitySensor写个代理类VelocitySensorProxy,这个类持有真实对象或者模拟对象的指针,完全复刻VelocitySensor的所有接口。测试时让代理指向模拟对象,生产环境指向真实的VelocitySensor。SensorClient依旧持有VelocitySensorProxy指针,不用改原来的依赖类型。
示例代码
class VelocitySensorProxy { private: std::variant<VelocitySensor*, MockVelocitySensor*> sensor; public: VelocitySensorProxy(VelocitySensor* s) : sensor(s) {} VelocitySensorProxy(MockVelocitySensor* m) : sensor(m) {} double readVelocity() const { return std::visit([](auto* s) { return s->readVelocity(); }, sensor); } }; // SensorClient无需修改,直接用代理类 class SensorClient { private: VelocitySensorProxy* sensor; public: SensorClient(VelocitySensorProxy* s) : sensor(s) {} double getVelocity() { return sensor->readVelocity(); } };
优劣对比
- 👍 优势:完全不用改
SensorClient的原有代码,新增一个代理类就行,对原有代码侵入性极小。 - 👎 劣势:代理类得完全复刻
VelocitySensor的所有接口,要是VelocitySensor接口变了,代理类得同步修改,维护成本高。
关于代码覆盖的讨论
- 核心覆盖目标:我们要测的是
SensorClient的业务逻辑,不是VelocitySensor的实现。所以测试时只需要关注SensorClient如何处理不同的返回值,不用去测VelocitySensor本身。 - 对100%代码覆盖的理性认知:
- 核心业务逻辑、工具类这类代码,追求100%覆盖很有必要;
- 一些极端边界分支(比如空指针异常、极低概率的错误场景),如果测试成本极高或者生产环境几乎不会触发,可以适当权衡;
- 代码覆盖只是测试的一个指标,不能代表测试的有效性,得结合场景测试、边界测试等多种手段一起用。
内容的提问来源于stack exchange,提问作者dzikidzik

