【问题标题】:Advice on Mocking System Calls关于模拟系统调用的建议
【发布时间】:2011-02-24 20:24:12
【问题描述】:

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

【问题讨论】:

    标签: c++ unit-testing mocking


    【解决方案1】:

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

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

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

    在单元测试中,调用者传递一个可以模拟失败、测试错误条件……简而言之:模拟任何你想模拟的东西。

    编辑:阅读 Michael Feathers 的 "Working effectively with legacy code" 了解更多提示。

    【讨论】:

    • 为什么不在我自己的类中使用 NVI 声明一个私有虚拟 _getaddrinfo,然后在子类中覆盖它以进行测试。为什么要涉及整个其他课程?
    • @Robert:因为这可能在继承树中发生冲突,以及继承的“过度使用”。假设明年您决定将 DnsClass 拆分为 DnsClassInternal(解析本地网络中的 DNS)和 DnsClassExternal(解析本地网络外的 DNS)。如果您使用继承来实现模拟,您将不得不重做模拟甚至实现 2 个不同的模拟类(对于 2 个 Dns 子类)。如果您决定将其进一步拆分为 DnsClassForIP4 和 DnsClassForIP6(您将有 4 个类,因此有 4 个模拟类),问题会变得更糟?
    【解决方案2】:

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

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

    问题是保留这样的东西,无法测试的东西,包装在如此微不足道的东西中,实际上不需要测试,然后模拟该包装器以测试更复杂的交互。 KISS 在这里尤为重要。

    【讨论】:

    【解决方案3】:

    在这种情况下,您不需要模拟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
    

    【讨论】:

    • 我使用选项 2 来测试生产代码。只要您永远不需要在单元测试中调用真实版本,它就可以工作。
    • +1 选项 2 很酷,当然也有 Caspin 提到的缺点。
    • @deft_code elf-hook 实用程序允许您执行选项 2 并仍然调用原始版本。
    • 这些都被称为stub,和mocking无关...所以这不是获得100%(线路)覆盖率的方法。对于每个参数组合,您可能需要不同的输出来全面测试您的代码。
    【解决方案4】:

    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;
    

    【讨论】:

    • 您的选项 1 是 PowerMock 采用的方法 - 显然他们在 Windows 上使用的 MSVC++ 等效项。
    • @KalebPederson 你是说这个PowerMock吗?我浏览了文档,但它看起来像一个 Java 测试框架。我忽略了什么吗?
    【解决方案5】:

    在 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

    【讨论】:

      【解决方案6】:

      免责声明:我写了 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

      【讨论】:

      猜你喜欢
      • 2019-03-22
      • 2011-07-16
      • 2011-07-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-03
      相关资源
      最近更新 更多