GoogleTest gMock Actions 参考指南:从内置 Action 到自定义 Action 的完整实战手册
【免费下载链接】googletestGoogleTest - Google Testing and Mocking Framework项目地址: https://gitcode.com/gh_mirrors/googl/googletest
导读
在 GoogleTest 的 gMock 框架中,Action(动作)定义了 mock 函数被调用时应执行的“行为”:返回什么值、产生什么副作用、调用哪个真实函数。本文以 docs/reference/actions.md 为骨架,逐类讲解 gMock 提供的全部内置 Action(返回值、副作用、调用可调用对象、默认动作、复合动作),并结合 gmock-actions.h 源码与 gmock-actions_test.cc 测试用例,深入说明底层实现原理与实战陷阱,最后完整覆盖ACTION*宏体系,让你能够为复杂测试场景编写自定义 Action。读完本文,你将能在ON_CALL()/EXPECT_CALL()中熟练挑选、组合并自定义 Action,写出行为精确、可维护的 mock 测试代码。
前置阅读:如果你还不熟悉 mock 函数与期望(expectation)的基本写法,建议先阅读 docs/gmock_for_dummies.md 中的 “Actions: What Should It Do?” 一节(第 432 行起),本文会在该基础上做全面展开。
一、Action 是什么:在期望语句中扮演的角色
Action回答的问题是“mock 函数被调用时应该做什么”。Mock 对象本身没有真实实现,用户必须在期望中告诉它:
- 用
WillOnce()指定“第一次(或前几次)匹配时”执行的动作; - 用
WillRepeatedly()指定“剩余调用次数”执行的动作; - 用
ON_CALL()为匹配的调用设置默认动作。
例如(来自 gmock_for_dummies.md):
using ::testing::Return; // 恰好被调用三次,分别返回 100、200、300。 EXPECT_CALL(turtle, GetX()) .WillOnce(Return(100)) .WillOnce(Return(200)) .WillOnce(Return(300)); // 至少被调用两次:前两次返回 100、200,之后一直返回 300。 EXPECT_CALL(turtle, GetY()) .WillOnce(Return(100)) .WillOnce(Return(200)) .WillRepeatedly(Return(300));所有内置 Action 均定义在::testing命名空间中,并作为Action<F>类型的多态动作使用。
默认动作(Default Action)
即使你不写任何WillOnce(),mock 函数也存在默认动作(见 gmock_for_dummies.md):
void函数直接返回;bool函数返回false;- 其他内置类型函数返回 0;
- 在 C++ 11 及以上,若返回类型是“可默认构造”的(具有默认构造函数),默认动作是返回一个默认构造的值。
当显式写了Times(n)而WillOnce()数量不足时,多余的调用同样会回落到默认动作——例如int函数在WillOnce(Return(100))用完后会返回 0(gmock_for_dummies.md)。
关键陷阱:动作子句只求值一次
EXPECT_CALL()语句对动作子句只求值一次,即使动作会被执行多次(gmock_for_dummies.md):
int n = 100; EXPECT_CALL(turtle, GetX()) .Times(4) .WillRepeatedly(Return(n++)); // n++ 只执行一次!结果并非依次返回 100、101、102…,而是始终返回 100;同理Return(new Foo)也只在设置期望时创建一次对象。若需要“每次调用都产生副作用”,必须自定义 Action。
二、返回一个值(Returning a Value)
| Action | 说明 |
|---|---|
Return() | 从一个voidmock 函数返回。 |
Return(value) | 返回value。若value的类型与 mock 函数的返回类型不同,会在设置期望的那一刻(而非执行动作时)转换为后者类型。 |
ReturnArg<N>() | 返回第N个(从 0 开始)参数。 |
ReturnNew<T>(a1, ..., ak) | 返回new T(a1, ..., ak);每次调用都会创建一个新对象。 |
ReturnNull() | 返回空指针。 |
ReturnPointee(ptr) | 返回ptr所指向的值。 |
ReturnRef(variable) | 返回对variable的引用。 |
ReturnRefOfCopy(value) | 返回value的一份拷贝的引用;该拷贝与 Action 生命周期相同。 |
ReturnRoundRobin({a1, ..., ak}) | 每次调用依次返回列表中的下一个ai,到列表末尾后从头开始循环。 |
ReturnRoundRobin的源码实现
ReturnRoundRobin是 gMock 中一个“多态动作”(可用于任意返回vector元素类型的函数),其核心实现位于 gmock-actions.h:
template <typename T> class ReturnRoundRobinAction { public: explicit ReturnRoundRobinAction(std::vector<T> values) { GTEST_CHECK_(!values.empty()) << "ReturnRoundRobin requires at least one element."; state_->values = std::move(values); } template <typename... Args> T operator()(Args&&...) const { return state_->Next(); } private: struct State { T Next() { T ret_val = values[i++]; if (i == values.size()) i = 0; // 到末尾后回到开头 return ret_val; } std::vector<T> values; size_t i = 0; }; std::shared_ptr<State> state_ = std::make_shared<State>(); };两个值得注意的实现细节:
- 至少需要一个元素:构造函数通过
GTEST_CHECK_断言列表非空,空列表会在运行期直接报错; - 共享状态:循环位置
i保存在shared_ptr<State>中,因此把同一个 Action 复制多份也不会丢失“下一次该返回哪个”的进度。
其公开工厂函数支持初始化列表与std::vector两种入参形式(gmock-actions.h)。测试用例 gmock-actions_test.cc 分别验证了两种用法:
TEST(ReturnRoundRobinTest, WorksForInitList) { Action<int()> ret = ReturnRoundRobin({1, 2, 3}); ... } TEST(ReturnRoundRobinTest, WorksForVector) { std::vector<double> v = {3.5, 2.0, 1.5}; Action<double()> ret = ReturnRoundRobin(v); ... }ReturnRef/ReturnRefOfCopy的使用场景
ReturnRef(variable):适合返回对由 mock 持有、且生命周期稳定的成员的引用;ReturnRefOfCopy(value):返回对 Action 内部一份拷贝的引用,避免悬垂引用——因为引用指向的拷贝与 Action 同生命周期,即使原value之后被销毁也安全。
参数索引约定
ReturnArg<N>()中的N是0 起始的参数索引:ReturnArg<0>()返回第 1 个参数,ReturnArg<2>()返回第 3 个参数。这条约定贯穿全文所有带<N>的动作与复合动作。
三、副作用(Side Effects)
| Action | 说明 |
|---|---|
Assign(&variable, value) | 将value赋给variable。 |
DeleteArg<N>() | delete第N个(0 起始)参数,该参数必须是指针。 |
SaveArg<N>(pointer) | 将第N个参数保存到*pointer。 |
SaveArgPointee<N>(pointer) | 将第N个参数所指向的值保存到*pointer。 |
SetArgReferee<N>(value) | 将value赋给第N个参数所引用的变量。 |
SetArgPointee<N>(value) | 将value赋给第N个参数所指向的变量。 |
SetArgumentPointee<N>(value) | 同SetArgPointee<N>(value)。已废弃,将在 v1.7.0 移除。 |
SetArrayArgument<N>(first, last) | 将源区间[first, last)中的元素拷贝到第N个参数指向的数组(该参数可以是指针或迭代器)。动作不接管源区间元素的所有权。 |
SetErrnoAndReturn(error, value) | 将errno设为error并返回value。 |
Throw(exception) | 抛出给定异常,exception可以是任意可拷贝值。自 v1.1.0 起可用。 |
底层实现佐证
Assign的实现类是AssignAction(gmock-actions.h),核心是Perform中的一行*ptr_ = value_;——它只做赋值,不持有引用语义之外的状态;SetErrnoAndReturn的实现类是SetErrnoAndReturnAction(gmock-actions.h 附近),用于模拟那些依赖errno的错误探测型 API;SetArgPointee<N>在测试中覆盖了多种入参形态(gmock-actions_test.cc):int、char字面量、宽字符、char*指针等,例如:
Action<MyFunction> a = SetArgPointee<1>(2); // 第 2 个参数指向的变量被赋为 2 Action<MyFunction> b = SetArgPointee<0>("hi"); // 支持字符串字面量实战示例:模拟“回填”型 API
using ::testing::_; using ::testing::SetArgPointee; using ::testing::SetArrayArgument; // 模拟一个“读取缓冲区并返回字节数”的接口 EXPECT_CALL(mock_reader, Read(_, _)) .WillOnce(DoAll(SetArgPointee<1>(42), // 回填输出参数 Return(1))); // 模拟将源区间 [first, last) 拷贝到第 0 个参数指向的数组 std::vector<int> src = {1, 2, 3}; EXPECT_CALL(mock, Fill(_)) .WillOnce(SetArrayArgument<0>(src.begin(), src.end()));四、把函数、仿函数或 Lambda 当作 Action
下面用“callable”统称自由函数、std::function、仿函数(functor)与 lambda。
| Action | 说明 |
|---|---|
f | 直接用可调用对象f作为动作:以传给 mock 函数的参数调用f。 |
Invoke(f) | 以传给 mock 函数的参数调用f,其中f可以是全局/静态函数或仿函数。 |
Invoke(object_pointer, &class::method) | 以传给 mock 函数的参数在object_pointer上调用&class::method成员函数。 |
InvokeWithoutArgs(f) | 调用f(全局/静态函数或仿函数),f必须不接收任何参数。 |
InvokeWithoutArgs(object_pointer, &class::method) | 调用对象上的成员函数,该成员函数不接收参数。 |
InvokeArgument<N>(arg1, arg2, ..., argk) | 以k个参数调用 mock 函数的第N个(0 起始)参数,该参数必须是一个函数或仿函数。 |
被调用函数的返回值将作为该动作的返回值。
用Unused忽略不关心的参数
为配合Invoke*()定义可调用对象时,可以将任何未使用的参数声明为Unused(类型由 gMock 提供):
using ::testing::Invoke; double Distance(Unused, double x, double y) { return sqrt(x*x + y*y); } ... EXPECT_CALL(mock, Foo("Hi", _, _)).WillOnce(Invoke(Distance));这里mock.Foo的 3 个参数中只有第 2、3 个被用到,第一个用Unused占位,类型自动匹配。
关于所有权与基类类型的限制
Invoke(callback)和InvokeWithoutArgs(callback)会接管callback的所有权,因此callback必须是永久有效(不能是栈上临时对象)。此外,callback的类型必须是基类回调类型而非派生类型,否则无法编译:
BlockingClosure* done = new BlockingClosure; ... Invoke(done) ...; // 编译失败:类型不是基类类型 Closure* done2 = new BlockingClosure; ... Invoke(done2) ...; // 正确:基类指针InvokeArgument与按引用传参
在InvokeArgument<N>(...)中,如果需要按引用传递某个参数,需用std::ref()包裹:
using ::testing::InvokeArgument; ... InvokeArgument<2>(5, string("Hi"), std::ref(foo))这条语句会调用 mock 函数的第 3 个参数(一个函数/仿函数),向它传入5与string("Hi")(按值),以及foo(按引用)。
五、默认动作(Default Action)
| Action | 说明 |
|---|---|
DoDefault() | 执行默认动作(由ON_CALL()指定,或使用内置默认动作)。 |
DoDefault()让你在“先ON_CALL兜底、再WillOnce特化”的场景中显式回落默认行为。其实现类是 gmock-actions.h 中的DoDefaultAction——它通过模板类型转换操作符把DoDefault()转换为任意签名F的Action<F>,因此可以在任意返回类型的 mock 函数中使用。
注意:由于技术原因,
DoDefault()不能用于复合动作(composite action)内部,强行使用会导致运行期错误。例如DoAll(DoDefault(), ...)是不允许的。
典型用法:
using ::testing::_; using ::testing::DoDefault; using ::testing::Return; // 兜底默认:任何参数都返回 -1 ON_CALL(mock, Lookup(_)).WillByDefault(Return(-1)); // 特化:仅对 "admin" 返回 1,其余走默认 EXPECT_CALL(mock, Lookup("admin")).WillOnce(Return(1)); EXPECT_CALL(mock, Lookup(_)).WillRepeatedly(DoDefault());六、复合动作(Composite Actions)
| Action | 说明 |
|---|---|
DoAll(a1, a2, ..., an) | 每次调用依次执行动作a1到an,并返回an的结果。前n - 1个子动作必须返回 void,且它们收到的是参数的只读视图。 |
IgnoreResult(a) | 执行动作a并忽略其结果,a不能返回 void。 |
WithArg<N>(a) | 把 mock 函数的第N个(0 起始)参数传给动作a并执行。 |
WithArgs<N1, N2, ..., Nk>(a) | 把选中的(0 起始)参数传给动作a并执行。 |
WithoutArgs(a) | 不传任何参数执行动作a。 |
DoAll的源码级理解
DoAll在源码中是一组偏特化模板(gmock-actions.h 附近):DoAllAction<FinalAction>处理“只有一个收尾动作”的情形,DoAllAction<InitialAction, OtherActions...>递归处理“前导动作 + 其余动作”。这种设计保证了:
- 只有最后一个子动作可以产生非 void 返回值;
- 前导子动作只拿到参数的只读视图(const 引用),因此无法通过前导动作修改参数——需要“先改参数再返回”时,应把修改动作放到最后或使用
SetArg*系列。
测试中对DoAll的移动语义也有覆盖:由于前导动作只能读取参数,从参数中移动内容(如std::unique_ptr形参)的DoAll组合是无法编译的(gmock-actions_test.cc)。
典型用法——先记录副作用再返回值:
using ::testing::DoAll; using ::testing::Invoke; using ::testing::Return; EXPECT_CALL(mock, GetConfig(_)) .WillOnce(DoAll(SaveArg<0>(&last_key), // 先把入参存下来(返回 void) Return(config))); // 再返回结果IgnoreResult让“丢弃返回值”合法化
当外层函数需要void动作、而内层动作有返回值时,IgnoreResult负责“吞掉”结果:
using ::testing::IgnoreResult; using ::testing::Invoke; using ::testing::Return; Action<void()> a = IgnoreResult(Return(5)); // 多态动作 Action<void()> b = IgnoreResult(Invoke(ReturnOne)); // 单态动作测试 gmock-actions_test.cc 验证了IgnoreResult可用于多态动作、单态动作以及返回非默认可构造类类型的动作。
WithArgs/WithArg:重排、抽取参数
WithArgs<N1, N2, ..., Nk>(a)是解决“mock 函数参数与被调用函数参数不一致”的标准手段(详见 docs/gmock_cook_book.md):
using ::testing::WithArgs; using ::testing::Invoke; // IsVisibleInQuadrant1(bool x_is_positive, bool y_is_positive) // mock 的 Forward(x, y, visible) 只关心前两个参数 EXPECT_CALL(mock, Forward(_, _, _)) .WillOnce(WithArgs<0, 2>(Invoke(IsVisibleInQuadrant1)));要点:
- 索引是 0 起始的,且可以重复、可以重排,例如
WithArgs<2, 3, 3, 5>(...); - 可以改变参数顺序,例如
WithArgs<3, 2, 1>(...); - 更简单时也可用
WithArg<N>(a)只抽取单个参数; - 如果只是想忽略某些参数,也可以用第一节的
Unused声明,避免包一层WithArgs。
七、定义自己的 Action(Defining Actions)
| 宏 | 说明 |
|---|---|
ACTION(Sum) { return arg0 + arg1; } | 定义一个动作Sum(),返回 mock 函数第 0、1 个参数之和。 |
ACTION_P(Plus, n) { return arg0 + n; } | 定义一个带参动作Plus(n),返回第 0 个参数与n之和。 |
ACTION_Pk(Foo, p1, ..., pk) { statements; } | 定义带k个参数的动作Foo(p1, ..., pk),执行给定语句。 |
ACTION*宏不能在函数或类内部使用,必须在文件作用域(或命名空间作用域)中定义。
宏体系内的自动符号
在ACTION/ACTION_P*宏体内,gMock 自动提供以下符号(可从 gmock_cook_book.md 确认):
arg0,arg1, …:mock 函数的各参数(0 起始);args:参数元组(供ACTION_TEMPLATE等使用);arg0_type,arg1_type, …:各参数的类型;param_type(或n_type等):ACTION_P*中参数的推导类型;function_type/return_type:mock 函数的函数类型与返回类型。
从简单到参数化
// 无参动作:返回前两个参数之和 ACTION(Sum) { return arg0 + arg1; } // 单参动作:返回 arg0 + n ACTION_P(Add, n) { return arg0 + n; } // 使用:... WillOnce(Add(5)); // 返回参数 #0 + 5 // 多参动作:计算 (arg0, arg1) 到 (x, y) 的距离 ACTION_P2(ReturnDistanceTo, x, y) { double dx = arg0 - x; double dy = arg1 - y; return sqrt(dx*dx + dy*dy); } // 使用:... WillOnce(ReturnDistanceTo(5.0, 26.5));注意术语区分(gmock_cook_book.md):arguments指调用 mock 函数时传入的值,parameters指实例化 Action 时传入的值。ACTION可以视为参数个数为 0 的ACTION_P,且同名的ACTION_P、ACTION_P2可以按参数个数重载:
ACTION_P(Plus, a) { ... } ACTION_P2(Plus, a, b) { ... }限制参数/参数类型
由于宏让编译器自动推导类型,若需显式约束类型,可在宏体内用类型转换或 gMock 的编译期断言StaticAssertTypeEq:
ACTION(Foo) { int n = arg0; // 约束 arg0 可转换为 int ... } ACTION_P(Bar, param) { ::testing::StaticAssertTypeEq<const char*, arg1_type>(); // 约束 arg1 的类型 bool flag = param; // 约束 param 可转换为 bool }进阶:ACTION_TEMPLATE编写显式模板参数的 Action
当动作需要无法从值参数推导出的显式模板参数时,可使用ACTION_TEMPLATE(gmock_cook_book.md),它是ACTION/ACTION_P*的扩展:
ACTION_TEMPLATE(DuplicateArg, HAS_2_TEMPLATE_PARAMS(int, k, typename, T), AND_1_VALUE_PARAMS(output)) { *output = T(std::get<k>(args)); } // 使用:ActionName<t1, ..., tm>(v1, ..., vn) // 例如:DuplicateArg<1, std::string>(&out)HAS_m_TEMPLATE_PARAMS(kind1, name1, ..., kind_m, name_m)声明m个模板参数(m ∈ [1, 10],kind可为typename、整型常量或模板),AND_n_VALUE_PARAMS(p1, ..., p_n)声明n个值参数(n ∈ [0, 10])。注意多个模板参数之间用逗号分隔(如上例int, k与typename, T)。
组合动作的约束回顾
最后提醒两点与自定义动作相关的组合规则:
DoAll中的前导子动作必须返回 void,且只能只读访问参数——需要修改参数时请使用SetArgPointee/SetArgReferee或把修改动作放在收尾位置;DoDefault()不能嵌套在复合动作内部使用。
八、完整实战:把 Action 组合起来
下面综合运用返回值、副作用、复合动作与自定义动作,模拟一个典型的缓存服务:
#include "gmock/gmock.h" #include "gtest/gtest.h" using ::testing::_; using ::testing::DoAll; using ::testing::Invoke; using ::testing::Return; using ::testing::ReturnRoundRobin; using ::testing::SaveArg; using ::testing::SetArgPointee; class Cache { public: virtual ~Cache() = default; virtual int Lookup(const std::string& key) = 0; virtual bool Put(const std::string& key, int value) = 0; virtual size_t Scan(const std::string* keys, size_t n) = 0; }; class MockCache : public Cache { public: MOCK_METHOD(int, Lookup, (const std::string&), (override)); MOCK_METHOD(bool, Put, (const std::string&, int), (override)); MOCK_METHOD(size_t, Scan, (const std::string*, size_t), (override)); }; // 文件作用域自定义动作(不能在函数/类内定义) ACTION_P2(LogThenReturn, key, value) { GTEST_LOG_(INFO) << "Lookup(" << key << ") -> " << value; return value; } TEST(CacheMockTest, CompositeActions) { MockCache mock; std::string last_key; // 1. 命中路径:记录入参并返回轮换值 EXPECT_CALL(mock, Lookup("a")) .WillOnce(DoAll(SaveArg<0>(&last_key), Return(10))) .WillRepeatedly(ReturnRoundRobin({10, 20, 30})); EXPECT_EQ(mock.Lookup("a"), 10); EXPECT_EQ(mock.Lookup("a"), 20); // 轮换到 20 EXPECT_EQ(mock.Lookup("a"), 30); // 轮换到 30 EXPECT_EQ(mock.Lookup("a"), 10); // 回到开头 EXPECT_EQ(last_key, "a"); // 2. 未命中路径:默认动作兜底 EXPECT_CALL(mock, Lookup("miss")).WillRepeatedly(Return(-1)); EXPECT_EQ(mock.Lookup("miss"), -1); // 3. 回填型接口:设置输出参数再返回 EXPECT_CALL(mock, Scan(_, _)) .WillOnce(DoAll(SetArgPointee<0>("cached"), Return(1u))); const std::string* out = nullptr; EXPECT_EQ(mock.Scan(out, 1), 1u); EXPECT_EQ(*out, "cached"); // 4. 自定义动作 EXPECT_CALL(mock, Lookup("k")) .WillOnce(LogThenReturn("k", 7)); EXPECT_EQ(mock.Lookup("k"), 7); }九、相关文档与进一步阅读
- 动作在期望语句中的完整语法(
WillOnce/WillRepeatedly/ON_CALL的搭配与基数推导规则):docs/gmock_for_dummies.md - 匹配器(Matcher)参考——在
ON_CALL/EXPECT_CALL中筛选参数与断言值: docs/reference/matchers.md - 自定义动作的深入教程(
ACTION_TEMPLATE、WithArgs、Unused等):docs/gmock_cook_book.md - 单页速查表(含 Action 与 Cardinality 列表):docs/gmock_cheat_sheet.md
- 内置 Action 与自定义 Action 的源码实现:googlemock/include/gmock/gmock-actions.h
- 覆盖本文绝大多数动作的测试用例:googlemock/test/gmock-actions_test.cc
【免费下载链接】googletestGoogleTest - Google Testing and Mocking Framework项目地址: https://gitcode.com/gh_mirrors/googl/googletest
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考