关于模拟系统调用的建议

时间:2010-05-27 19:58:56

标签: c++ unit-testing mocking

我有一个类调用getaddrinfo进行DNS查找。在测试期间,我想模拟涉及此系统调用的各种错误情况。模拟这样的系统调用的推荐方法是什么?我正在使用Boost.Test进行单元测试。

6 个答案:

答案 0 :(得分:27)

在这种情况下,您不需要模拟getaddrinfo,而是需要在不依赖其功能的情况下进行测试。帕特里克和诺亚都有很好的分数,但你至少还有两个选择:

选项1:要测试的子类

由于您已经在类中拥有了对象,因此可以子类化以进行测试。例如,假设以下是您的实际类:

class DnsClass {
    int lookup(...);
};

int DnsClass::lookup(...) {
    return getaddrinfo(...);
}

然后,为了测试,你会像这样继承:

class FailingDnsClass {
    int lookup(...) { return 42; }
};

您现在可以使用FailingDnsClass子类生成错误,但仍会在发生错误情况时验证一切正常。在这种情况下,依赖注入通常是你的朋友。

注意:这与Patrick的答案非常相似,但如果您尚未设置依赖注入,则不会(希望)涉及更改生产代码。

选项2:使用链接接缝

在C ++中,您还拥有Michael Feathers在Working Effectively with Legacy Code中描述的链接时间接缝。

基本思想是利用链接器和构建系统。编译单元测试时,请链接到您自己的getaddrinfo版本,该版本将优先于系统版本。例如:

TEST.CPP:

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <iostream>

int main(void)
{
        int retval = getaddrinfo(NULL, NULL, NULL, NULL);
        std::cout << "RV:" << retval << std::endl;
        return retval;
}

lib.cpp:

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node, const char *service,
        const struct addrinfo *hints, struct addrinfo **res
        )
{
        return 42;
}

然后进行测试:

$ g++ test.cpp lib.cpp -o test
$ ./test 
RV:42

答案 1 :(得分:13)

查找“依赖注入”的模式。

依赖注入的工作原理如下:代码不是直接在代码中调用getaddrinfo,而是使用具有虚拟方法“getaddrinfo”的接口。

在实际代码中,调用者传递接口的实现,该接口将接口的虚拟方法“getaddrinfo”映射到real :: getaddrinfo函数。

在单元测试中,调用者传递一个可以模拟失败,测试错误条件的实现,......简短:模拟你想要模拟的任何内容。

编辑:阅读Michael Feathers的"Working effectively with legacy code"以获取更多提示。

答案 2 :(得分:10)

3个选项

<强> 1 即可。使用gnu链接器的模拟功能--wrap选项。我从来没有用它来测试生产代码,因为直到我们的开发团队已经提交到方法3之前我才发现它。我希望我们能早点找到它

ld --wrap=getaddrinfo /*the rest of the link line*/
or
g++ -Wl,--wrap=getaddrinfo /* the rest of the build line*/

// this in the unit tests.
bool g_getaddrinfo_use_real = true;
int g_getaddrinfo_ret = -1;
int g_getaddrinfo_errno = something;
int __wrap_getaddrinfo( const char *node, const char *service,
                        const struct addrinfo *hints,
                        struct addrinfo **res )
{
   if( g_getaddrinfo_use_real )
      return __real_getaddrinfo(node,service,hints,res);

   errno = g_getaddrinfo_errno;
   return g_getaddrinfo_ret;
}

<强> 2 即可。定义您自己的getaddrinfo并将其静态链接到您的测试应用程序。这只有在libc动态链接时才有效,99%的情况下都是如此。此方法还有一个缺点,即在单元测试应用程序中永久禁用真正的getaddrinfo,但实现起来非常简单。

int g_getadderinfo_ret = -1;
int g_getaddrinfo_errno = something;
int getaddrinfo( const char *node, const char *service,
                 const struct addrinfo *hints,
                 struct addrinfo **res )
{
   errno = g_getaddrinfo_errno
   return g_getaddrinfo_ret;
}

第3 即可。使用相同的名称定义您自己的中间函数。然后,如果您愿意,您仍然可以拨打原件。使用某些宏来帮助重复更容易。如果你想模拟可变参数函数(printfopen等),你还必须使用gnu扩展。

typedef (*getaddrinfo_func_type)( const char *node, const char *service,
                               const struct addrinfo *hints,
                               struct addrinfo **res );

getaddrinfo_func_type g_getaddrinfo_func;

int getaddrinfo( const char *node, const char *service,
                 const struct addrinfo *hints,
                 struct addrinfo **res )
{
   return g_getaddrinfo_func( node, service, hints, res )
}

int g_mock_getadderinfo_ret = -1;
int g_mock_getaddrinfo_errno = something;
int mock_getaddrinfo( const char *node, const char *service,
                      const struct addrinfo *hints,
                      struct addrinfo **res )
{
   errno = g_mock_getaddrinfo_errno;
   return g_mock_getaddrinfo_ret;
}

// use the original
g_getaddrinfo_func = dlsym(RTDL_NEXT, "getaddrinfo");

// use the mock version
g_getaddrinfo_func = &mock_getaddrinfo;

答案 3 :(得分:2)

在ELF系统上,您可以使用elf_hook临时替换动态链接的符号。

它允许您创建任意函数并用它替换动态链接函数。

  • 创建包含待测代码的共享库
  • 创建一个动态加载共享库的测试(dlopen
  • 将要模拟的符号重定向到测试函数

elf_hook具有以下签名:

void* elf_hook(char const* library_filename, 
               void const* library_address, 
               char const* function_name, 
               void const* substitution_address);

你会用这样的:

int hooked_getaddrinfo(const char* node, 
                       const char* service,
                       const struct addrinfo* hints,
                       struct addrinfo** res)
{
    return 42;
}

const char* lib_path = "path/to/library/under/test.so";
void* lib_handle = dlopen(lib_path, RTLD_LAZY);

elf_hook(lib_path, LIBRARY_ADDRESS_BY_HANDLE(lib_handle), "getaddrinfo", hooked_getaddrinfo);

现在,在受测试的库中对getaddrinfo的任何来电都会调用hooked_getaddrinfo

elf_hook作者Anthony Shoumikhin的综合文章是here

答案 4 :(得分:1)

虽然技术上可行,但我认为不可行。您必须能够替换该功能的实现,并且您可能无法并仍然链接到系统上的标准库。

你应该做的是打电话给中间人。然后你可以在测试期间模拟中介,然后转发到生产中的实际功能。您甚至可以考虑创建一个与此函数交互的类以及其他类似函数,并为您的程序提供更通用的接口。这个类实际上不会做任何事情,只能在大多数时间转发,但在测试期间它可以被有效地模拟,你可以测试任何用途。

事情是要保留这样的东西,无法测试的东西,包含在如此微不足道的东西中,它实际上不需要测试,然后模拟包装器来测试更复杂的交互。 KISS在这里特别重要。

答案 5 :(得分:0)

免责声明:我写过ELFspy。

您可以在单元测试中使用ELFspy,通过提供具有相同签名的函数,将对库中的getaddrinfo的所有调用重定向到您自己的实现。

示例:

int myaddrinfo(const char* node, const char* service,
                   const struct addrinfo* hints,
                   struct addrinfo** res)
{
  return EAI_NODATA;
}
int main(int argv, char** argc)
{
    spy::initialise(argc, argv);
    auto gai_spy = SPY(&getaddrinfo);
    auto gai_fake = spy::fake(gai_spy, &myaddrinfo);
    ...
}

如果需要,您可以调用原始的getaddrinfo:

gai_spy.invoke_real(node, service, hints, res);

您的代码必须使用-fPIC编译为与位置无关的代码才能使其生效。

有关模拟时间(time_t *)的示例中的更多详细信息,请访问: https://github.com/mollismerx/elfspy/wiki/Example-03:-Faking-time