在 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 中定义的函数在动态链接过程中被预加载并用作第一个匹配项,因此执行您的代码,而不是系统库中的代码。

更新:

如果您想使用“原始”函数,您应该使用 dlopendlmapdlclose 函数从适当的共享库中“手动”提取它们。因为我不想弄乱之前的例子,这里是新的,和之前的一样 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!