导语gtest 是项目主要使用的测试框架, 但项目本身是纯 C 项目 (历史遗留问题…)
这一篇主要是 gtest 如何 mock C 函数:
gtest mock C 函数 (不包括可变参数) 测试 尽力简便使用 不影响原函数调用 前日谈 Wrapwrap 是 GNU linker (ld) 的链接器选项, --wrap=symbol
时
原始函数 symbol 被重命名为 __real_symbol
对 symbol
的所有调用都被重定向到 __wrap_symbol
GMOCKGMOCK 官方文档;
C 函数没有那么多妖魔鬼怪, 只取 MOCK_METHOD
宏 足矣, 非常方便 mock 函数声明
1 2 3 4 5 6 class MyMock { public : MOCK_METHOD (ReturnType, MethodName, (Args…)); MOCK_METHOD (ReturnType, MethodName, (Args…), (Specs…)); };
还有另外一种 Old-Style MOCK_METHODn
Macros 如果 gmock 版本依赖比较低, 也可以选择这样的实现
1 MOCK_METHOD1 (Foo, bool (int ))
方案需求:
mock c 函数 方便声明, 方便使用 mock 函数不设置行为时最好执行原函数 方案 0.1
将源代码编译为 静态库, 编译测试时传入 wrap = mock_function
产生 __real_symbol
和 __wrap_symbol
. 为了实现 不设置行为时执行原函数, 在 __wrap_symbol
-> __real_symbol
中间加一层代理.将 __wrap_symbol
指向 class MockClass::symbol
方法 symbol
方法默认指向 __real_symbol
, 这样不单独设置 mock 行为, 最终还是执行到原函数. ^ndzp 这样可以声明一堆的 mock 函数, 然后只对部分 symbol
方法 真正设置 mock 行为, 其他函数还是会执行到原函数. 方案 0.1 可行, 但是需要大量简化;
每个 test 编译时都需要一堆的 --wrap=xxxx
每个 test 中还需要声明一堆的 mock XXX 相似 test 中大量重复的声明 mock 函数应该只需要在一个地方声明一次, 否则修改成本就太高了.
声明一个 mock 函数应该也避免重复,最好就 3 个: 函数名, 返回值, 入参;
方案 0.2
mock 函数只在每个 test 所在的 cpp 文件声明, 一个 cpp 文件共享一组 mock 函数声明 test 编译时可以通过 cmake 中解析 cpp 文件, 正则匹配到有哪些声明的 mock 函数,写入 test 编译命令, 这样避免大量 --wrap=xxxx
. 将 mock 函数组成 X 宏, 借助 X 宏,可以一个 hpp 文件, 为所有 test case 生成不同的 mock 类; Code声明时候非常简便, 每个 cpp 文件头部, (函数名, 返回类型, 函数参数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #define MOCK_FUNCTIONS \ X(demo1, int, ()) \ X(demo2, void, (void *conf)) \ X(demo3, void *, (int a, int b)) \ X(demo4, int, (char *a, char *b)) #include "mock.hpp" class DemoTest : public ::testing::Test{ protected: void SetUp () override { MockClass::Instance().init(); testing::Mock::AllowLeak(&MockClass::Instance()); } void TearDown () override { MockClass::Instance().reset(); } }; TEST_F(DemoTest, demo1) { auto &mock = MockClass::Instance(); EXPECT_CALL(mock, demo()).WillOnce(Return(1 )); ASSERT_EQ(demo1(), 1 ); }
Cmake通过正则匹配 X
宏, 非常方便生成一堆 --wrap=symbol
警告: 因为是将 cpp 文件作为纯文本解析, 只将 X 宏注释掉, 还是会有 wrap 生成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 file (READ ${FILE} CONTENT) string (REGEX MATCHALL "X\\(([a-zA-Z_][a-zA-Z_0-9]*)," X_FUNCTIONS ${CONTENT} )if (X_FUNCTIONS) foreach (FUNC ${X_FUNCTIONS} ) string (REGEX REPLACE "^ *X\\(([a-zA-Z_][a-zA-Z_0-9]*)," "\\1" FUNC_NAME ${FUNC} ) list (APPEND WRAP_OPTIONS "-Wl,--wrap=${FUNC_NAME}" ) endforeach () endif ()add_executable ("${TESTNAME}_test" ${FILE} )target_link_libraries ("${TESTNAME}_test" PRIVATE gtest gmock gtest_main ${WRAP_OPTIONS} )
mock.hpp使用全局单例等尽力避免 test 运行时警告
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #pragma once #include "gmock/gmock.h" #include "gtest/gtest.h" #include <mutex> using ::testing::_;using ::testing::DoAll;using ::testing::Invoke;using ::testing::Return;using ::testing::SetArgPointee;using ::testing::StrEq;#define EMPTY() #define DEFER(id) id EMPTY() #define EXPAND(…) __VA_ARGS__ extern "C" { #define X(name, ret, args) ret __real_##name args; MOCK_FUNCTIONS #undef X } class MockClass { public : static MockClass &Instance () { static MockClass instance; return instance; } MockClass () { #define X(name, ret, args) real_##name = __real_##name; MOCK_FUNCTIONS #undef X } void init () { static std::once_flag initFlag; std::call_once (initFlag, [this ]() { #define X(name, ret, args) ON_CALL(*this, name).WillByDefault(testing::Invoke(this, &MockClass::real_##name)); MOCK_FUNCTIONS #undef X }); } #define X(name, ret, args) MOCK_METHOD(ret, name, args, ()); MOCK_FUNCTIONS #undef X #define X(name, ret, args) ret(*real_##name) args; MOCK_FUNCTIONS #undef X void reset () { testing::Mock::VerifyAndClearExpectations (this ); } private : ~MockClass () { reset (); } MockClass (const MockClass &) = delete ; MockClass &operator =(const MockClass &) = delete ; }; extern "C" { #define X(name, ret, args) \ ret __wrap_##name args \ { \ return MockClass::Instance().name EXPAND args; \ } MOCK_FUNCTIONS #undef X }
尾巴实践中发现有两个遗憾:
第一个自然是对可变参数 C 函数不可行; 好在 可变参数函数使用频率并不高;
第二个是: Gtest 中 Mock C Function 那点事#^ndzp 会触发一个警告, 不影响测试结果就是了:
1 2 3 4 5 6 2│ │ GMOCK WARNING: $2│ │ Uninteresting mock function call - taking default action specified at: $2│ │ /demo/tests/./util/mock.hpp:56: $2│ │ Function call: mock_test(0x1f615b0, 0x7ffd3b967300, 16-byte object <32-D7 F2-57 00-00 00-00 00-00 00-00 00-00 00-00>, 16-byte object <F3-09 B7-9B 00-00 00-00 00-00 00-00 00-00 00-00>) $2│ │ Returns: 0 $2│ │ NOTE: You can safely ignore the above warning unless this call should not happen. Do not suppress it by blindly adding an EXPECT_CALL() if you don't mean to enforce the call. See https://github.com/google/googletest/blob/main/docs/gmock_cook_book.md#knowing-when-to-expect-useoncall for details.