C++无依赖注入时Mock类成员是否可行?及HungryMan测试方案咨询
问题场景与优化方案
原始代码结构
以下是简化后的项目代码,核心是HungryMan内部持有CakeMaker依赖,且两者存在循环引用(CakeMaker需要引用HungryMan):
class IEaterOfCake { public: virtual void EatCake() = 0; }; class CakeMaker { public: CakeMaker(IEaterOfCake& e) : mCakeEater(e) {} void MakeCake() { mCakeEater.EatCake(); } protected: IEaterOfCake& mCakeEater; }; class HungryMan : public IEaterOfCake { public: HungryMan() : mCakeMaker(*this) {} void HaveLunch() { mCakeMaker.MakeCake(); } virtual void EatCake() override {/* mmmm cake */}; private: CakeMaker mCakeMaker; }; int main() { HungryMan m; m.HaveLunch(); }
当前测试困境
CakeMaker可以通过实现IEaterOfCake测试替身轻松完成单元测试,但HungryMan的内部依赖(如CakeMaker)无法直接替换。- 实际项目中
HungryMan存在多个类似内部依赖,整体测试的组合数呈指数级增长。 - 尝试过条件编译替换
mCakeMaker类型为Mock,但私有成员无法在测试中验证行为。 - 要求:不能使用动态分配(禁止
new),且需隐藏HungryMan的内部依赖细节。
优化方案
1. 模板参数化依赖(编译期注入,推荐)
将HungryMan改为模板类,默认使用真实的CakeMaker,测试时传入Mock类型。这种方式既保留了封装性,又无需动态分配,同时支持测试时灵活替换依赖。
修改后的生产代码:
class IEaterOfCake { public: virtual void EatCake() = 0; }; class CakeMaker { public: CakeMaker(IEaterOfCake& e) : mCakeEater(e) {} void MakeCake() { mCakeEater.EatCake(); } protected: IEaterOfCake& mCakeEater; }; // 模板化HungryMan,默认使用真实CakeMaker template<typename MakerType = CakeMaker> class HungryMan : public IEaterOfCake { public: HungryMan() : mCakeMaker(*this) {} void HaveLunch() { mCakeMaker.MakeCake(); } virtual void EatCake() override {/* mmmm cake */}; // 仅在测试模式下暴露依赖访问接口 #ifdef UNIT_TEST MakerType& GetCakeMaker() { return mCakeMaker; } #endif private: MakerType mCakeMaker; }; // 生产环境直接使用默认模板参数 int main() { HungryMan<> m; m.HaveLunch(); }
测试代码:
TEST(HungryManTests, HaveLunchTriggersMakeCake) { // 定义MockCakeMaker,实现测试所需的验证逻辑 class MockCakeMaker { public: MockCakeMaker(IEaterOfCake& e) : mEater(e), makeCalled(false) {} void MakeCake() { makeCalled = true; } bool WasMakeCalled() const { return makeCalled; } private: IEaterOfCake& mEater; bool makeCalled; }; HungryMan<MockCakeMaker> hungryMan; hungryMan.HaveLunch(); // 通过测试专用接口验证Mock行为 ASSERT_TRUE(hungryMan.GetCakeMaker().WasMakeCalled()); }
2. 友元测试类(限定测试访问)
如果不想修改HungryMan为模板类,可以通过友元机制仅允许测试代码访问其私有成员,结合条件编译替换依赖类型。
修改后的HungryMan:
class HungryMan : public IEaterOfCake { // 仅允许测试类访问私有成员 #ifdef UNIT_TEST friend class HungryManTestFixture; #endif public: HungryMan() : mCakeMaker(*this) {} void HaveLunch() { mCakeMaker.MakeCake(); } virtual void EatCake() override {/* mmmm cake */}; private: #ifdef UNIT_TEST MockCakeMaker mCakeMaker; // 测试时替换为Mock #else CakeMaker mCakeMaker; #endif };
测试代码:
class HungryManTestFixture : public testing::Test { protected: class MockCakeMaker { public: MockCakeMaker(IEaterOfCake& e) : mEater(e), called(false) {} void MakeCake() { called = true; } bool called; IEaterOfCake& mEater; }; }; TEST_F(HungryManTestFixture, HaveLunchCallsMakeCake) { HungryMan man; man.HaveLunch(); // 友元身份直接访问私有成员验证 ASSERT_TRUE(man.mCakeMaker.called); }
3. 接口提取+静态实例指针(编译期替换)
给CakeMaker定义抽象接口,通过条件编译切换真实实现与Mock实现,并在HungryMan构造时将Mock实例注册到静态指针,供测试验证。
修改后的代码:
class IEaterOfCake { public: virtual void EatCake() = 0; }; // 定义CakeMaker的抽象接口 class ICakeMaker { public: virtual void MakeCake() = 0; }; // 真实实现 class RealCakeMaker : public ICakeMaker { public: RealCakeMaker(IEaterOfCake& e) : mEater(e) {} void MakeCake() override { mEater.EatCake(); } private: IEaterOfCake& mEater; }; // Mock实现(仅测试时编译) #ifdef UNIT_TEST class MockCakeMaker : public ICakeMaker { public: MockCakeMaker(IEaterOfCake& e) : mEater(e), called(false) {} void MakeCake() override { called = true; } bool called; static MockCakeMaker* CurrentInstance; // 静态指针供测试访问 private: IEaterOfCake& mEater; }; MockCakeMaker* MockCakeMaker::CurrentInstance = nullptr; #endif class HungryMan : public IEaterOfCake { public: HungryMan() #ifdef UNIT_TEST : mCakeMaker(*this) { MockCakeMaker::CurrentInstance = &mCakeMaker; } #else : mCakeMaker(*this) {} #endif void HaveLunch() { mCakeMaker.MakeCake(); } virtual void EatCake() override {/* mmmm cake */}; private: #ifdef UNIT_TEST MockCakeMaker mCakeMaker; #else RealCakeMaker mCakeMaker; #endif };
测试代码:
TEST(HungryManTests, HaveLunchTriggersMakeCake) { HungryMan man; man.HaveLunch(); // 通过静态指针验证Mock行为 ASSERT_TRUE(MockCakeMaker::CurrentInstance->called); }
方案对比
- 模板参数化:最干净的方案,生产代码无冗余,测试灵活,无运行时开销,优先推荐。
- 友元测试类:无需修改核心类结构,但友元会打破封装性,仅限定测试场景使用。
- 接口+静态指针:适合无法使用模板的场景,但静态指针存在线程安全风险,需注意测试隔离。
内容的提问来源于stack exchange,提问作者Ashley Duncan
相关产品推荐
相关产品推荐

