GTest中测试调用多子函数的函数用例失败如何解决
问题根因
测试运行失败的核心原因很明确:onStartingExecute()内部调用的tt_init_device()、tt_init_port()、tt_init_pipeline()这类函数属于强依赖真实运行环境的底层接口,一般需要直连物理硬件、占用系统端口、加载完整业务管线的运行时资源,而GTest单元测试运行在普通宿主机环境,没有对应资源支撑,调用这类接口自然会触发崩溃、返回非预期错误,最终导致用例失败。
单元测试不需要真的执行底层初始化逻辑,核心验证目标是onStartingExecute()本身的逻辑正确性:也就是它有没有按约定顺序、约定次数调用所有要求的初始化子函数,子函数内部的具体实现不属于这个用例的测试范围,必须做依赖隔离。
标准测试实现方案(依赖注入+GMock)
这是工业界最常用的可维护测试方案,基于GTest配套的GMock框架完成依赖打桩:
1. 抽象底层初始化接口
首先把零散的全局tt_init_*函数封装成统一接口类,方便后续替换不同场景的实现:
// 初始化抽象接口 class ITtInitializer { public: virtual ~ITtInitializer() = default; virtual int tt_init_device() = 0; virtual int tt_init_port() = 0; virtual int tt_init_pipeline() = 0; // 其余同类初始化函数统一在此声明 }; // 生产环境用的真实实现,直接包装原有全局函数 class RealTtInitializer : public ITtInitializer { public: int tt_init_device() override { return ::tt_init_device(); } int tt_init_port() override { return ::tt_init_port(); } int tt_init_pipeline() override { return ::tt_init_pipeline(); } };
2. 修改被测类支持依赖注入
给FusaTelltaleClientAppTh新增依赖注入入口,生产环境默认传入真实初始化实现,测试环境传入Mock对象:
class FusaTelltaleClientAppTh { private: ITtInitializer* initializer_ = nullptr; public: // 原有构造函数保留,生产环境默认加载真实初始化逻辑 FusaTelltaleClientAppTh(int id, const std::string& name) : FusaTelltaleClientAppTh(id, name, new RealTtInitializer()) {} // 测试用的构造函数,支持外部传入自定义初始化实现 FusaTelltaleClientAppTh(int id, const std::string& name, ITtInitializer* initializer) : initializer_(initializer) { // 原有构造逻辑保持不变 } void onStartingExecute() { // 原全局函数调用替换为接口实例调用 initializer_->tt_init_device(); initializer_->tt_init_port(); initializer_->tt_init_pipeline(); // 其余初始化调用按原有顺序补充 } };
3. 编写Mock类与测试用例
// Mock实现类 class MockTtInitializer : public ITtInitializer { public: MOCK_METHOD(int, tt_init_device, (), (override)); MOCK_METHOD(int, tt_init_port, (), (override)); MOCK_METHOD(int, tt_init_pipeline, (), (override)); // 其余初始化接口对应添加MOCK_METHOD声明 }; TEST_F(ICFusaTelltaleClientAppThTest, Test_onStartingExecute_InitFlowValid) { // 初始化Mock对象,给所有接口设置默认返回成功的桩 NiceMock<MockTtInitializer> mockInit; ON_CALL(mockInit, tt_init_device()).WillByDefault(Return(0)); ON_CALL(mockInit, tt_init_port()).WillByDefault(Return(0)); ON_CALL(mockInit, tt_init_pipeline()).WillByDefault(Return(0)); // 传入Mock对象构造被测实例 FusaTelltaleClientAppTh AppThobj(1, "abc", &mockInit); // 断言1:所有初始化函数均被调用恰好1次 EXPECT_CALL(mockInit, tt_init_device()).Times(1); EXPECT_CALL(mockInit, tt_init_port()).Times(1); EXPECT_CALL(mockInit, tt_init_pipeline()).Times(1); // 断言2:初始化调用顺序严格符合 device -> port -> pipeline 的要求 ::testing::Sequence initSeq; EXPECT_CALL(mockInit, tt_init_device()).InSequence(initSeq); EXPECT_CALL(mockInit, tt_init_port()).InSequence(initSeq); EXPECT_CALL(mockInit, tt_init_pipeline()).InSequence(initSeq); // 执行被测函数 AppThobj.onStartingExecute(); }
- 用
NiceMock包装Mock对象可以屏蔽未设置EXPECT_CALL的接口调用报错,初始化函数较多时可以减少大量冗余代码 - 如果业务不强制要求严格的初始化顺序,可以删除Sequence相关断言,只保留调用次数验证即可
临时快速验证方案(不推荐长期使用)
如果暂时不想重构代码做依赖注入,可以直接在测试文件中定义同名全局桩函数覆盖原有实现,让原有用例可以直接跑通:
// 测试文件内直接实现桩函数,注意C接口要加extern "C" extern "C" { int tt_init_device() { return 0; } int tt_init_port() { return 0; } int tt_init_pipeline() { return 0; } // 其余初始化函数统一做空实现返回成功 } // 原有测试用例无需修改即可运行 TEST_F(ICFusaTelltaleClientAppThTest,Test_onStartingExecute) { FusaTelltaleClientAppTh AppThobj(1,"abc"); AppThobj.onStartingExecute(); }
注意:这种全局桩的方式耦合度极高,一旦原函数签名、返回值约定变更,不会触发编译报错,只适合临时快速验证场景,长期迭代维护必须使用依赖注入+GMock的方案。
内容的提问来源于stack exchange,提问作者Kiran JP
相关产品推荐
相关产品推荐

