在 C/C++ 代码中寻找 'mock' posix 函数的方法
Looking for ways to 'mock' posix functions in C/C++ code
我正在尝试寻找一些优雅的方法来模拟和存根对标准 C 库函数的函数调用。
虽然通过在测试中 linking 其他 C 文件很容易停止对项目的 C 文件的调用,但停止标准 C 函数更难。
link他们就在那里。
目前,我的方法是从我的 test.cpp 文件中包含被测代码,并像这样放置定义:
#include <stdio.h>
#include <gtest/gtest.h>
#include "mymocks.h"
CMockFile MockFile;
#define open MockFile.open
#define close MockFile.close
#define read MockFile.read
#include "CodeUnderTestClass.cpp"
#undef open
#undef close
#undef read
// test-class here
这很麻烦,有时我 运行 跨代码使用 'open' 作为其他地方的成员名称或导致其他冲突和问题。也有代码需要与测试代码不同的定义和包含的情况。
那么还有其他选择吗?一些 link 时间技巧或 运行 时间技巧来覆盖标准 C 函数?我考虑过 运行-time hook 函数,但这可能太过分了,因为通常二进制代码是以只读方式加载的。
我的单元测试 运行 仅在 Debian-Linux 上使用 gcc 在 amd64 上进行。所以也欢迎 gcc、x64 或 Linux 特定技巧。
我知道重写所有被测代码以使用 C 函数的抽象版本是一种选择,但该提示对我来说不是很有用。
使用库预加载将系统库替换为您自己的。
考虑以下测试程序代码,mytest.c
:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
char buf[256];
int fd = open("file", O_RDONLY);
if (fd >= 0) {
printf("fd == %d\n", fd);
int r = read(fd, buf, sizeof(buf));
write(0, buf, r);
close(fd);
} else {
printf("can't open file\n");
}
return 0;
}
它将从当前目录打开一个名为 file
的文件,打印它的描述符编号(通常是 3
),读取它的内容然后在标准输出上打印它(描述符 0
).
现在这是你的测试库代码,mock.c
:
#include <string.h>
#include <unistd.h>
int open(const char *pathname, int flags) {
return 100;
}
int close(int fd) {
return 0;
}
ssize_t read(int fd, void *buf, size_t count) {
strcpy(buf, "TEST!\n");
return 7;
}
将其编译到名为 mock.so
:
的共享库
$ gcc -shared -fpic -o mock.so mock.c
如果您将 mytest.c
编译为 mytest
二进制文件,运行 它使用以下命令:
$ LD_PRELOAD=./mock.so ./mytest
您应该看到输出:
fd == 100
TEST!
mock.c
中定义的函数在动态链接过程中被预加载并用作第一个匹配项,因此执行您的代码,而不是系统库中的代码。
更新:
如果您想使用“原始”函数,您应该使用 dlopen
、dlmap
和 dlclose
函数从适当的共享库中“手动”提取它们。因为我不想弄乱之前的例子,这里是新的,和之前的一样 mock.c
加上动态符号加载的东西:
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <gnu/lib-names.h>
// this declares this function to run before main()
static void startup(void) __attribute__ ((constructor));
// this declares this function to run after main()
static void cleanup(void) __attribute__ ((destructor));
static void *sDlHandler = NULL;
ssize_t (*real_write)(int fd, const void *buf, size_t count) = NULL;
void startup(void) {
char *vError;
sDlHandler = dlopen(LIBC_SO, RTLD_LAZY);
if (sDlHandler == NULL) {
fprintf(stderr, "%s\n", dlerror());
exit(EXIT_FAILURE);
}
real_write = (ssize_t (*)(int, const void *, size_t))dlsym(sDlHandler, "write");
vError = dlerror();
if (vError != NULL) {
fprintf(stderr, "%s\n", vError);
exit(EXIT_FAILURE);
}
}
void cleanup(void) {
dlclose(sDlHandler);
}
int open(const char *pathname, int flags) {
return 100;
}
int close(int fd) {
return 0;
}
ssize_t read(int fd, void *buf, size_t count) {
strcpy(buf, "TEST!\n");
return 7;
}
ssize_t write(int fd, const void *buf, size_t count) {
if (fd == 0) {
real_write(fd, "mock: ", 6);
}
real_write(fd, buf, count);
return count;
}
编译它:
$ gcc -shared -fpic -o mock.so mock.c -ldl
注意命令末尾的 -ldl
。
所以:startup
函数会在main
之前运行(所以你不需要在你原来的程序中放任何初始化代码)并且初始化real_write
到成为原始的 write
函数。 cleanup
函数将在 main
之后 运行,因此您也不需要在 main
函数的末尾添加任何“清理”代码。
除了新实现的 write
函数外,其余所有工作与前面的示例完全相同。对于几乎所有的描述符,它将作为原始文件工作,而对于文件描述符 0,它将在原始内容之前写入一些额外的数据。在这种情况下,程序的输出将是:
$ LD_PRELOAD=./mock.so ./mytest
fd == 100
mock: TEST!
我正在尝试寻找一些优雅的方法来模拟和存根对标准 C 库函数的函数调用。 虽然通过在测试中 linking 其他 C 文件很容易停止对项目的 C 文件的调用,但停止标准 C 函数更难。 link他们就在那里。
目前,我的方法是从我的 test.cpp 文件中包含被测代码,并像这样放置定义:
#include <stdio.h>
#include <gtest/gtest.h>
#include "mymocks.h"
CMockFile MockFile;
#define open MockFile.open
#define close MockFile.close
#define read MockFile.read
#include "CodeUnderTestClass.cpp"
#undef open
#undef close
#undef read
// test-class here
这很麻烦,有时我 运行 跨代码使用 'open' 作为其他地方的成员名称或导致其他冲突和问题。也有代码需要与测试代码不同的定义和包含的情况。
那么还有其他选择吗?一些 link 时间技巧或 运行 时间技巧来覆盖标准 C 函数?我考虑过 运行-time hook 函数,但这可能太过分了,因为通常二进制代码是以只读方式加载的。
我的单元测试 运行 仅在 Debian-Linux 上使用 gcc 在 amd64 上进行。所以也欢迎 gcc、x64 或 Linux 特定技巧。
我知道重写所有被测代码以使用 C 函数的抽象版本是一种选择,但该提示对我来说不是很有用。
使用库预加载将系统库替换为您自己的。
考虑以下测试程序代码,mytest.c
:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
char buf[256];
int fd = open("file", O_RDONLY);
if (fd >= 0) {
printf("fd == %d\n", fd);
int r = read(fd, buf, sizeof(buf));
write(0, buf, r);
close(fd);
} else {
printf("can't open file\n");
}
return 0;
}
它将从当前目录打开一个名为 file
的文件,打印它的描述符编号(通常是 3
),读取它的内容然后在标准输出上打印它(描述符 0
).
现在这是你的测试库代码,mock.c
:
#include <string.h>
#include <unistd.h>
int open(const char *pathname, int flags) {
return 100;
}
int close(int fd) {
return 0;
}
ssize_t read(int fd, void *buf, size_t count) {
strcpy(buf, "TEST!\n");
return 7;
}
将其编译到名为 mock.so
:
$ gcc -shared -fpic -o mock.so mock.c
如果您将 mytest.c
编译为 mytest
二进制文件,运行 它使用以下命令:
$ LD_PRELOAD=./mock.so ./mytest
您应该看到输出:
fd == 100
TEST!
mock.c
中定义的函数在动态链接过程中被预加载并用作第一个匹配项,因此执行您的代码,而不是系统库中的代码。
更新:
如果您想使用“原始”函数,您应该使用 dlopen
、dlmap
和 dlclose
函数从适当的共享库中“手动”提取它们。因为我不想弄乱之前的例子,这里是新的,和之前的一样 mock.c
加上动态符号加载的东西:
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <gnu/lib-names.h>
// this declares this function to run before main()
static void startup(void) __attribute__ ((constructor));
// this declares this function to run after main()
static void cleanup(void) __attribute__ ((destructor));
static void *sDlHandler = NULL;
ssize_t (*real_write)(int fd, const void *buf, size_t count) = NULL;
void startup(void) {
char *vError;
sDlHandler = dlopen(LIBC_SO, RTLD_LAZY);
if (sDlHandler == NULL) {
fprintf(stderr, "%s\n", dlerror());
exit(EXIT_FAILURE);
}
real_write = (ssize_t (*)(int, const void *, size_t))dlsym(sDlHandler, "write");
vError = dlerror();
if (vError != NULL) {
fprintf(stderr, "%s\n", vError);
exit(EXIT_FAILURE);
}
}
void cleanup(void) {
dlclose(sDlHandler);
}
int open(const char *pathname, int flags) {
return 100;
}
int close(int fd) {
return 0;
}
ssize_t read(int fd, void *buf, size_t count) {
strcpy(buf, "TEST!\n");
return 7;
}
ssize_t write(int fd, const void *buf, size_t count) {
if (fd == 0) {
real_write(fd, "mock: ", 6);
}
real_write(fd, buf, count);
return count;
}
编译它:
$ gcc -shared -fpic -o mock.so mock.c -ldl
注意命令末尾的 -ldl
。
所以:startup
函数会在main
之前运行(所以你不需要在你原来的程序中放任何初始化代码)并且初始化real_write
到成为原始的 write
函数。 cleanup
函数将在 main
之后 运行,因此您也不需要在 main
函数的末尾添加任何“清理”代码。
除了新实现的 write
函数外,其余所有工作与前面的示例完全相同。对于几乎所有的描述符,它将作为原始文件工作,而对于文件描述符 0,它将在原始内容之前写入一些额外的数据。在这种情况下,程序的输出将是:
$ LD_PRELOAD=./mock.so ./mytest
fd == 100
mock: TEST!