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

如何替换单例设计模式?TimerHandler单例引发单元测试难题求解

解决Timer类与单例TimerHandler耦合的测试友好设计思路

这种单例硬编码依赖导致的测试难题确实很常见,尤其是当你要测试依赖Timer的其他类时,单例的全局状态很容易让测试用例互相干扰。我分享几个不用大规模修改现有代码就能解决问题的思路:

1. 给单例TimerHandler添加测试替换接口

既然TimerHandler是单例,我们可以给它加一个仅测试环境使用的接口,允许替换全局实例为Mock版本。这样Timer构造时依然会调用RegisterTimer,但实际注册到的是我们控制的Mock实例。

示例代码:

class TimerHandler {
private:
    static TimerHandler* instance_;
public:
    static TimerHandler& GetInstance() {
        if (!instance_) {
            instance_ = new TimerHandler();
        }
        return *instance_;
    }

    // 测试专用:替换全局实例
    static void SetTestInstance(TimerHandler* test_instance) {
        // 清理原有实例(如果需要)
        delete instance_;
        instance_ = test_instance;
    }

    // 测试专用:恢复默认实例
    static void ResetToDefault() {
        delete instance_;
        instance_ = nullptr; // 下次GetInstance会重建默认实例
    }

    // 原有的RegisterTimer方法
    void RegisterTimer(Timer& timer) { /* ... */ }
};

测试时的用法:

TEST(SomeClassTest, UsesTimerCorrectly) {
    // 创建Mock TimerHandler
    auto mock_handler = new MockTimerHandler();
    TimerHandler::SetTestInstance(mock_handler);

    // 执行测试逻辑,比如创建依赖Timer的SomeClass实例
    SomeClass obj;
    // ... 验证Mock的交互 ...

    // 测试结束后恢复默认实例,避免影响其他用例
    TimerHandler::ResetToDefault();
}

这个方案对原有代码的改动极小,只需要给TimerHandler加两个静态方法,完全不影响生产环境的逻辑。

2. 扩展Timer的构造函数,支持可选的Handler注入

给Timer加一个带默认参数的构造函数,默认使用单例TimerHandler,但测试时可以传入自定义的Handler。这样原有代码不需要任何修改,测试时灵活注入Mock。

示例代码:

class Timer {
public:
    // 默认参数保持原有行为,兼容所有旧代码
    Timer(TimerHandler& handler = handlers::TimerHandler::GetInstance()) 
        : tick_counter_(0), target_tick_(0), increment_(false), started_(false), elapsed_(false) {
        handler.RegisterTimer(*this);
    }

    // 原有的其他成员...
private:
    int tick_counter_;
    int target_tick_;
    bool increment_;
    bool started_;
    bool elapsed_;
};

测试时直接传入MockHandler:

TEST(SomeClassTest, HandlesTimerElapsed) {
    MockTimerHandler mock_handler;
    Timer timer(mock_handler); // 注册到Mock
    SomeClass obj(timer); // 假设SomeClass依赖Timer

    // 模拟Timer触发,验证obj的行为...
}

这个方案的优点是完全不污染生产代码,只是对Timer做了小扩展,测试时按需注入,非常灵活。

3. 用工厂类封装Timer的创建与注册

把Timer的创建和注册逻辑抽离到一个工厂类中,生产环境使用默认工厂(绑定单例TimerHandler),测试环境使用Mock工厂(绑定MockHandler)。这样依赖Timer的类只需要依赖工厂,而不是直接创建Timer。

示例代码:

// 抽象工厂接口
class TimerFactory {
public:
    virtual ~TimerFactory() = default;
    virtual std::unique_ptr<Timer> CreateTimer() = 0;
};

// 生产环境用的默认工厂
class DefaultTimerFactory : public TimerFactory {
public:
    std::unique_ptr<Timer> CreateTimer() override {
        auto timer = std::make_unique<Timer>();
        // 注意:这里需要修改Timer,去掉构造时的自动注册,由工厂负责注册
        handlers::TimerHandler::GetInstance().RegisterTimer(*timer);
        return timer;
    }
};

// 测试用的Mock工厂
class MockTimerFactory : public TimerFactory {
private:
    MockTimerHandler& mock_handler_;
public:
    MockTimerFactory(MockTimerHandler& handler) : mock_handler_(handler) {}

    std::unique_ptr<Timer> CreateTimer() override {
        auto timer = std::make_unique<Timer>();
        mock_handler_.RegisterTimer(*timer);
        return timer;
    }
};

然后修改依赖Timer的类,让它依赖TimerFactory而不是直接new Timer:

class SomeClass {
public:
    // 构造函数注入工厂,默认用全局的默认工厂
    SomeClass(std::unique_ptr<TimerFactory> factory = std::make_unique<DefaultTimerFactory>()) 
        : timer_(factory->CreateTimer()) {}

private:
    std::unique_ptr<Timer> timer_;
};

测试时注入Mock工厂:

TEST(SomeClassTest, TimerTriggersAction) {
    MockTimerHandler mock_handler;
    auto mock_factory = std::make_unique<MockTimerFactory>(mock_handler);
    SomeClass obj(std::move(mock_factory));

    // 模拟Timer事件,验证obj的响应...
}

这个方案稍微改动大一点,但好处是把依赖关系解耦得更彻底,不仅解决了测试问题,也让代码的扩展性更好。

4. 编译期条件编译(不推荐但应急可用)

如果不想改太多代码,可以用编译宏区分测试和生产环境,让Timer在测试时注册到测试用的Handler。

示例代码:

Timer::Timer() : tick_counter_(0), target_tick_(0), increment_(false), started_(false), elapsed_(false) {
#ifdef UNIT_TEST
    // 测试环境下注册到测试Handler
    TestTimerHandler::GetInstance().RegisterTimer(*this);
#else
    // 生产环境正常注册
    handlers::TimerHandler::RegisterTimer(*this);
#endif
}

这个方案的缺点是代码里混入了测试相关的条件编译,长期来看不太优雅,但如果只是临时应急,改动最小。


个人最推荐方案2或方案1,它们对现有代码的侵入最小,同时能完美解决测试时的依赖问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:13:34