Gtest 中 Mock C Function 那点事

  • Gtest 中 Mock C Function 那点事

  • 资料来源:

    <>

  • 更新

    1
    2024.11.21 初始

导语

gtest 是项目主要使用的测试框架, 但项目本身是纯 C 项目 (历史遗留问题…)

这一篇主要是 gtest 如何 mock C 函数:

  • gtest mock C 函数 (不包括可变参数) 测试
  • 尽力简便使用
  • 不影响原函数调用

前日谈

Wrap

wrap 是 GNU linker (ld) 的链接器选项, --wrap=symbol

  • 原始函数 symbol 被重命名为 __real_symbol
  • symbol 的所有调用都被重定向到 __wrap_symbol

GMOCK

GMOCK 官方文档;

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" // 为每一个 cpp 文件生成不同的 MockClass

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
# Read file
file(READ ${FILE} CONTENT)
# Find both X macro invocations
string(REGEX MATCHALL "X\\(([a-zA-Z_][a-zA-Z_0-9]*)," X_FUNCTIONS ${CONTENT})

# Process X macro functions
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 to test
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;

// helper macros
#define EMPTY()
#define DEFER(id) id EMPTY()
#define EXPAND(…) __VA_ARGS__

// __real_XXX function
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]() {
// default behavior for mock functions
#define X(name, ret, args) ON_CALL(*this, name).WillByDefault(testing::Invoke(this, &MockClass::real_##name));
MOCK_FUNCTIONS
#undef X
});
}

// mock functions
#define X(name, ret, args) MOCK_METHOD(ret, name, args, ());
MOCK_FUNCTIONS
#undef X

// real functions pointers
#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;
};

// wrap functions definition
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.