You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为标记为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接口变了,代理类得同步修改,维护成本高。

关于代码覆盖的讨论

  1. 核心覆盖目标:我们要测的是SensorClient的业务逻辑,不是VelocitySensor的实现。所以测试时只需要关注SensorClient如何处理不同的返回值,不用去测VelocitySensor本身。
  2. 对100%代码覆盖的理性认知:
    • 核心业务逻辑、工具类这类代码,追求100%覆盖很有必要;
    • 一些极端边界分支(比如空指针异常、极低概率的错误场景),如果测试成本极高或者生产环境几乎不会触发,可以适当权衡;
    • 代码覆盖只是测试的一个指标,不能代表测试的有效性,得结合场景测试、边界测试等多种手段一起用。

内容的提问来源于stack exchange,提问作者dzikidzik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 04:52:14