使用GTest/GMock对类A进行单元测试时,如何验证私有指针成员b的foo()返回值对A::init()行为的影响?
GTest/GMock对类A进行单元测试时,如何验证私有指针成员b的foo()返回值对A::init()行为的影响?
这种场景我太熟悉了——明明想验证公共方法A::init()的逻辑,可关键的判断依赖却是私有成员b的foo()返回值,而且还没法直接mock私有成员,确实有点卡壳。不过用GTest/GMock有几种靠谱的方案,不用硬改封装性就能搞定,咱们一步步说:
方案一:依赖注入重构(最推荐)
这其实是最优解,不仅能解决测试问题,还能让你的代码耦合度更低、更符合面向对象设计原则。核心思路是把B和C抽象成接口,然后给A添加一个支持依赖注入的构造函数,这样测试时就能传入我们的Mock对象了。
第一步:抽象B和C的接口
先把B和C的核心方法抽成纯虚基类,这样才能用GMock创建Mock类:
// IB.h class IB { public: virtual bool foo() const = 0; virtual ~IB() = default; // 一定要加虚析构,避免内存泄漏 }; // B.h class B : public IB { // 原来B类的实现逻辑 }; // IC.h class IC { public: virtual void on() = 0; virtual ~IC() = default; }; // C.h class C : public IC { // 原来C类的硬件操作逻辑 };
第二步:修改类A的定义
把私有成员换成接口指针,并添加一个用于测试的构造函数:
// A.h class A { public: // 原来的无参构造函数,保持生产代码兼容 A() : b(new B()), c(new C()) {} // 用于测试的依赖注入构造函数 explicit A(IB* b_ptr, IC* c_ptr) : b(b_ptr), c(c_ptr) {} void init(); private: IB* b; IC* c; };
A.cpp里的实现不用大改,只需要保证init()调用的是接口方法:
A::init() { if (b->foo()) { c->on(); } }
第三步:编写测试用例
先定义Mock类,然后用它们来验证逻辑:
#include <gmock/gmock.h> #include <gtest/gtest.h> #include "A.h" #include "IB.h" #include "IC.h" // 定义MockB和MockC class MockB : public IB { public: MOCK_METHOD(bool, foo, (), (const, override)); }; class MockC : public IC { public: MOCK_METHOD(void, on, (), (override)); }; // 测试当b->foo()返回true时,c->on()会被调用 TEST(ClassATest, InitCallsCOnWhenBFooReturnsTrue) { auto mock_b = std::make_unique<MockB>(); auto mock_c = std::make_unique<MockC>(); // 设置期望:b->foo()返回true EXPECT_CALL(*mock_b, foo()) .WillOnce(testing::Return(true)); // 期望c->on()被调用一次 EXPECT_CALL(*mock_c, on()) .Times(1); // 用Mock对象构造A A a(mock_b.get(), mock_c.get()); a.init(); } // 测试当b->foo()返回false时,c->on()不会被调用 TEST(ClassATest, InitDoesNotCallCOnWhenBFooReturnsFalse) { auto mock_b = std::make_unique<MockB>(); auto mock_c = std::make_unique<MockC>(); EXPECT_CALL(*mock_b, foo()) .WillOnce(testing::Return(false)); // 期望c->on()完全不被调用 EXPECT_CALL(*mock_c, on()) .Times(0); A a(mock_b.get(), mock_c.get()); a.init(); }
方案二:友元测试(应急临时方案)
如果实在没法修改生产代码的结构(比如项目进度紧张),可以用友元的方式让测试类访问A的私有成员,从而替换成Mock对象。不过这个方法会破坏封装性,只建议作为临时手段。
修改A的头文件
添加条件编译的友元声明,只在测试时生效:
// A.h #ifdef TESTING class ClassATest; // 提前声明测试类 #endif class A { public: A(); void init(); private: B* b; C* c; #ifdef TESTING friend class ClassATest; // 让测试类能访问私有成员 #endif };
编写测试用例
在测试中替换私有成员为Mock对象,验证逻辑:
#include <gmock/gmock.h> #include <gtest/gtest.h> #include "A.h" class MockB { public: MOCK_METHOD(bool, foo, (), ()); }; class MockC { public: MOCK_METHOD(void, on, (), ()); }; class ClassATest : public testing::Test { protected: A a; // 保存原始对象,测试后恢复避免内存泄漏 B* original_b = a.b; C* original_c = a.c; ~ClassATest() override { // 恢复原始对象,让A的析构函数正常清理 a.b = original_b; a.c = original_c; } }; TEST_F(ClassATest, InitBehaviorDependsOnBFoo) { auto mock_b = std::make_unique<MockB>(); auto mock_c = std::make_unique<MockC>(); // 替换A的私有成员 a.b = reinterpret_cast<B*>(mock_b.get()); a.c = reinterpret_cast<C*>(mock_c.get()); // 测试foo返回true的情况 EXPECT_CALL(*mock_b, foo()).WillOnce(testing::Return(true)); EXPECT_CALL(*mock_c, on()).Times(1); a.init(); // 重置Mock期望,测试foo返回false的情况 testing::Mock::VerifyAndClearExpectations(mock_b.get()); testing::Mock::VerifyAndClearExpectations(mock_c.get()); EXPECT_CALL(*mock_b, foo()).WillOnce(testing::Return(false)); EXPECT_CALL(*mock_c, on()).Times(0); a.init(); }
总结
优先选择依赖注入重构的方式,它不仅能完美解决测试问题,还能提升代码的可维护性和扩展性。如果实在无法修改生产代码结构,友元测试可以作为临时应急方案,但长期来看还是重构更合适。
备注:内容来源于stack exchange,提问作者LPo
相关产品推荐
相关产品推荐

