单例类底层指针类型替换的单元测试可行性探讨
问题描述
我有个单例类B,里面的RunTimer()是对外部定时器工具的封装。现在要给这个类做单元测试,想跳过定时器相关的逻辑,只测依赖其他参数的部分。Fake是我专门写的测试用类,带了测试需要的额外逻辑。
因为单例始终只有一个实例,想问下在单元测试里把std::unique_ptr<ITimer> _ptr替换成Fake的思路,在测试层面是否合理?
附上代码示例:
class ITimer { public: virtual void foo() = 0; virtual ~Base() = default; // 注:此处应为~ITimer(),代码笔误 }; class Real : public ITimer { public: void foo() override {} }; // 单元测试用 class Fake : public ITimer { public: void foo() override {} }; class B { std::unique_ptr<ITimer> _ptr; // 默认构造函数创建Real实例 B() : B(std::make_unique<Real>()) { } B(std::unique_ptr<ITimer> ptr) : _ptr(std::move(ptr)) { } public: static B& get() { static B b; return b; } void RunTimer() { Timer::Run(); // 外部工具调用 } void change(std::unique_ptr<Base> ptr) // 注:此处应为std::unique_ptr<ITimer>,代码笔误 { _ptr = std::move(ptr); } }; int main() { B::get(); // 实际代码中使用Real // 测试场景(最终会放在TEST_F()中) auto mk = std::make_unique<Fake>(); B::get().change(std::move(mk)); // 对B::_ptr进行测试 }
解答
这个替换思路完全具有测试合理性,不过有几个细节要注意,还有优化空间:
1. 思路可行的核心原因
你这本质是用了依赖注入的变体——通过change方法替换单例内部的依赖实例,这是单例类做单元测试时常用的解耦手段:
- 单例的全局唯一性确实容易导致测试用例互相污染,但只要能在测试前后重置状态,就能把各个用例隔离开;
Fake继承自ITimer接口,符合面向接口编程的原则,既能模拟真实ITimer的行为,还能添加测试需要的逻辑(比如记录调用次数、返回预设值),从而排除真实定时器的干扰,聚焦你要测试的核心逻辑。
2. 先修正代码笔误
你的代码里有两处明显问题,不改会导致编译失败:
ITimer的析构函数声明错误:应改为virtual ~ITimer() = default;,而非~Base();B::change的参数类型错误:应改为std::unique_ptr<ITimer>,而非std::unique_ptr<Base>。
3. 优化测试稳定性的小技巧
单例的全局状态容易让测试用例互相影响,建议在测试框架中添加前后置处理逻辑:
- 每个测试用例执行前,调用
change注入Fake实例; - 测试完成后,再调用
change恢复为Real实例,避免影响后续用例。
比如用Google Test的话可以这么写:
TEST_F(BTest, TestFoo) { // 测试前:替换为Fake auto fake_timer = std::make_unique<Fake>(); B::get().change(std::move(fake_timer)); // 执行测试逻辑 // ... // 测试后:恢复为Real auto real_timer = std::make_unique<Real>(); B::get().change(std::move(real_timer)); }
4. 长远架构优化方向
如果后续测试场景变复杂,这种“事后替换”的方式会显得不够优雅,可以考虑:
- 给单例类添加测试专用初始化接口,允许在程序启动时就注入依赖,而非事后修改;
- 或者重构单例为依赖注入容器管理的类,从根源上避免全局状态带来的测试问题。
内容的提问来源于stack exchange,提问作者xyf
相关产品推荐
相关产品推荐

