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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 04:47:12