System_function_mock
1.背景
最近在写单元测试,势必就要涉及到对一些函数或者类对象 mock 。
当在 linux 下使用 gtest 以及 gmock 做 c/c++ 代码的单元测试时,就会涉及到一些系统函数例如 fopen, open ,close 等函数的 mock 。这时想起陈硕的书《Linux多线程服务端编程:使用muduoC++网络库》中 12.4 有谈到《在单元测试中mock系统调用》。由于项目在最开始编写的时候没有考虑到要做单元测试,所以就只能通过 12.4.2 中描述的来处理 mock 系统调用了。
为了做验证,我重构了我测试项目中的googletest模块中的一些代码(commit-id:b0f32b00d2dc457593943aa259490750810c31be),主要体现在t4_system_interface_mock.cpp中。
2.实现与细节
代码均基于陈硕的《Linux多线程服务端编程:使用muduoC++网络库》中的代码进行修改
(1)获取要 mock 的系统函数信息
例如对于 fopen 系统函数,我们可以轻易获取到他的函数定义
extern FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes) __wur;
然后去掉一些我们不关心的信息可以获得一个比较简洁的 C 函数接口
FILE *fopen(const char *__filename,
const char *__modes);
(2)处理一些必要信息
1-声明一个函数指针;
typedef FILE* (*fopen_func_t)(
const char *__restrict __filename,
const char *__restrict __modes);
2-实例化该函数指针指针;
通过 dlsym 获取系统函数的真实地址(调用这个函数指针的实例的时候,就会动态获取到系统函数的真实地址,实现动态切换 mock 函数和真实系统调用)。
fopen_func_t fopen_func = reinterpret_cast<fopen_func_t>(dlsym(RTLD_NEXT,"fopen"));
3-然后设置一些 mock 的参数和分支;
这里的代码和书上不太一样,因为我按照我的想法稍微修改了一些。
static bool mock_fopen = false; // mock 开关
constexpr int mock_fopen_errno = 1; // 因为这个我基本没有用到,所以我做了一些简化处理
enum class fopen_case_des : int { // 该枚举类主要用来处理 mock 中的各种返回
ret_nullptr,
ret_FILE,
};
static fopen_case_des fopen_case = fopen_case_des::ret_nullptr; // 初始化分支
4-编写 mock 系统函数;
extern "C" FILE* fopen(const char *__restrict __filename,
const char *__restrict __modes) {
if (mock_fopen) {
if (fopen_case == fopen_case_des::ret_nullptr) {
return nullptr;
} else if (fopen_case == fopen_case_des::ret_FILE) {
return new FILE;
} else {
return nullptr;
}
} else {
return fopen_func(__filename, __modes);
}
}
请注意,对于可变参数需要用以下方法来处理
#include <cstdarg> // va_list
extern "C" int open(const char *__path, int __oflag, ...) {
if (mock_open) {
if (open_case == open_case_des::retBigerThanZero) {
return 1;
} else if (open_case == open_case_des::retZero) {
return 0;
} else {
return 0;
}
} else {
va_list args; // 使用 va_list 处理可变参数
va_start(args, __oflag);
int result = open_func(__path, __oflag, args);
va_end(args);
return result;
}
}
(3)在单元测试中使用
1-调用系统接口的函数;
fclose 的 mock 做了省略,可以去查看项目的源代码,里面有响相应的完整实现);
int test_function_with_system_interface() {
const std::string file_path = "../README.md";
// open file
FILE* file_handle = fopen(file_path.c_str(), "r");
if (file_handle == nullptr) {
return -1;
}
// close file
if (fclose(file_handle) != 0) {
return -2;
}
return 0;
}
2-在 gtest 的 TEST 组件里实现走遍所有分支;
TEST(t4_system_interface_mock, linux_system_interface) {
mock_fopen = true; // 开启 mock fopen
mock_fclose = true; // 开启 mock fclose
// The code wants the function to return -1
fopen_case = fopen_case_des::ret_nullptr; // 让 fopen 走指定分支并且指定返回值
fclose_case = fclose_case_des::ret_0; // 让 fclose 走指定分支并且指定返回值
EXPECT_EQ(test_function_with_system_interface(), -1); // return -1
// The code wants the function to return 0
fopen_case = fopen_case_des::ret_FILE; // 同理
fclose_case = fclose_case_des::ret_0; // 同理
EXPECT_EQ(test_function_with_system_interface(), 0); // return 0
// The code wants the function to return -2
fopen_case = fopen_case_des::ret_FILE; // 同理
fclose_case = fclose_case_des::ret_1; // 同理
EXPECT_EQ(test_function_with_system_interface(), -2); // return -2
// 显示关闭 mock 从而不影响其他地方的使用
mock_fopen = false; // close mock fopen
mock_fclose = false; // close mock fclose
}
至此,我们完成了系统函数的 mock 。
3.一些辅助工具
由于通过系统接口,来生成函数指针,实例化,然后添加一些 mock 的参数和分支选项,到最后 mock 系统函数。这一系列的动作,可以认为都是类似的动作,除了函数的信息不同之外(返回值,函数名,参数),没什么不同。
至此,我编写了这份代码,它可以帮你生成相应的重复代码,它可以根据输入的 c 函数的不同,自动生成与之匹配的代码。
缺点就是你需要自己去填写一些 mock 的分支的选项,比如添加分支,然后处理相应的返回值,最后在单元测试中设置指定的分支然后获取想要的返回值。
使用方法如下
$ python3 generated.py
input function [example int func(int a)]: use enter to end the input
FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes)
function name: fopen
function return: FILE*
function param: const char *__restrict __filename, const char *__restrict __modes
function name list: __filename, __modes
Parameters:
Type: const char *__restrict, Name: __filename
Type: const char *__restrict, Name: __modes
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* global mock set */
static bool mock_fopen = false;
constexpr int mock_fopen_errno = 1;
enum class fopen_case_des : int {
ret_1,
};
static fopen_case_des fopen_case = fopen_case_des::ret_1;
/* fopen mock set */
typedef FILE* (*fopen_func_t)(const char *__restrict __filename, const char *__restrict __modes);
/* The real function address function */
fopen_func_t fopen_func = reinterpret_cast<fopen_func_t>(dlsym(RTLD_NEXT,"fopen"));
/* fopen mock */
extern "C" FILE* fopen(const char *__restrict __filename, const char *__restrict __modes) {
if (mock_fopen) {
if (fopen_case == fopen_case_des::ret_1) {
return FILE*{};
} else if ( 1 ) {
return FILE*{};
} else {
return FILE*{};
}
} else {
return fopen_func(__filename, __modes);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
$
现在你可以把//环绕的代码放到你的单元测试中去了,然后做一些 mock 操作