开发者

Advice on Mocking System Calls

I have a class which calls getaddrinfo for DNS look ups. During testin开发者_运维知识库g I want to simulate various error conditions involving this system call. What's the recommended method for mocking system calls like this? I'm using Boost.Test for my unit testing.


In this case you don't need to mock getaddrinfo, rather, you need to test without relying on its functionality. Both Patrick and Noah have good points but you have at least two other options:

Option 1: Subclass to Test

Since you already have your object in a class, you can subclass to test. For example, assume the following is your actual class:

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

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

Then, for testing, you would subclass like this:

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

You can now use the FailingDnsClass subclass to generate errors but still verify that everything behaves correctly when an error condition occurs. Dependency Injection is often your friend in this case.

NOTE: This is quite similar to Patrick's answer but doesn't (hopefully) involve changing the production code if you aren't already setup for dependency injection.

Option 2: Use a link seam

In C++, you also have link-time seams which Michael Feathers describes in Working Effectively with Legacy Code.

The basic idea is to leverage the linker and your build system. When compiling the unit tests, link in your own version of getaddrinfo which will take precedence over the system version. For example:

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;
}

And then for testing:

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


Look up patterns for "Dependency Injection".

Dependency Injection works like this: instead of calling getaddrinfo directly in your code, the code uses an interface that has a virtual method "getaddrinfo".

In real-life code, the caller passes an implementation of the interface that maps the virtual method "getaddrinfo" of the interface to the real ::getaddrinfo function.

In unit tests, the caller passes an implementation that can simulate failures, test error conditions, ... to be short: mock anything you want to mock.

EDIT: Read "Working effectively with legacy code" of Michael Feathers for more tips.


3 Options

1. Use the gnu linker's mocking abilities, the --wrap option. I've never used this to test production code as I didn't find out about it until our dev team had commited to method 3. I wish we had found this sooner

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. Define your own getaddrinfo and link it statically to your test application. This will only work if libc is linked dynamically which is true 99% of the time. This method also has the disadvantage of permanently disabling the real getaddrinfo in your unit test application, but is incredibly simple to implement.

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. Define your own intermediary function with the same name. Then you can still call the original if you want. This is much easier with some macros to help with the repetition. Also you will have to use gnu extensions if you wan to mock variadic functions (printf, open, etc).

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;


On ELF systems you can use elf_hook to temporarily replace dynamically linked symbols.

It allows you to create an arbitrary function and replace a dynamically linked function with it.

  • Create a shared library containing the code under test
  • Create a test which loads the shared library dynamically (dlopen)
  • Redirect the symbols you want to mock to your test functions

elf_hook has the following signature:

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

you would use is like this:

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);

Any call to getaddrinfo from within the library under test will now call hooked_getaddrinfo.

A comprehensive article by the elf_hook author, Anthony Shoumikhin, is here.


Although technically possible I don't think it would be feasible to. You'd have to be able to replace the implementation of that function and you probably can't and still link to the standard library on your system.

What you should do is call an intermediary. Then you can mock the intermediary during test and just forward to the actual function in production. You might even consider creating a class that interacts with this function and others like it and provides a more generic interface to your program. This class wouldn't actually do anything but forward calls most the time but during test it could be effectively mocked and you can test whatever uses it.

The thing is to keep stuff like this, things that can't be tested, wrapped in something so trivial it doesn't really need testing and then mock that wrapper to test the more complex interactions. KISS is especially important here.


Disclaimer: I wrote ELFspy.

You can use ELFspy in a unit test to redirect all calls to getaddrinfo in your libraries to an implementation of you own by providing a function with the same signature.

Example:

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);
    ...
}

You can make calls to the original getaddrinfo if you need to as follows:

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

Your code must compiled with -fPIC as position independent code for this to work.

Further details in the example on mocking time(time_t*) can be found here: https://github.com/mollismerx/elfspy/wiki/Example-03:-Faking-time

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜