Подтвердить что ты не робот

Советы по развязыванию системных вызовов

У меня есть класс, который вызывает getaddrinfo для поиска в DNS. Во время тестирования я хочу моделировать различные ошибки, связанные с этим системным вызовом. Какой рекомендуемый метод для издевательских системных вызовов вроде этого? Я использую Boost.Test для тестирования модулей.

4b9b3361

Ответ 1

В этом случае вам не нужно высмеивать getaddrinfo, скорее вам нужно протестировать, не полагаясь на его функциональность. У Патрика и Ноя есть хорошие моменты, но у вас есть как минимум два других варианта:

Вариант 1: Подкласс для тестирования

Поскольку у вас уже есть свой объект в классе, вы можете подклассы тестировать. Например, предположим, что ваш фактический класс:

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

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

Затем для тестирования вы должны подклассифицироваться следующим образом:

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

Теперь вы можете использовать подкласс FailingDnsClass для генерации ошибок, но все же убедитесь, что все ведет себя правильно, когда возникает условие ошибки. Зависимость Инъекция часто является вашим другом в этом случае.

ПРИМЕЧАНИЕ. Это очень похоже на ответ Патрика, но (надеюсь) не связано с изменением производственного кода, если вы еще не настроите инъекцию зависимостей.

Вариант 2: используйте шов ссылки

В С++ у вас также есть временные швы ссылок, которые Майкл Фейрс описывает в Эффективно работает с устаревшим кодом.

Основная идея - использовать компоновщик и вашу систему сборки. При компиляции модульных тестов связывайте свою собственную версию 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

Посмотрите шаблоны для "Injection Dependency".

Dependency Injection работает следующим образом: вместо вызова getaddrinfo непосредственно в вашем коде код использует интерфейс, который имеет виртуальный метод "getaddrinfo".

В реальном коде вызывающий передает реализацию интерфейса, который отображает виртуальный метод "getaddrinfo" интерфейса на реальную функцию:: getaddrinfo.

В модульных тестах вызывающая сторона передает реализацию, которая может имитировать сбои, проверить условия ошибки,... быть короткими: высмеять все, что вы хотите высмеять.

EDIT: прочитайте "Эффективно работайте с устаревшим кодом" Michael Feathers для получения дополнительных советов.

Ответ 3

3 Опции

1. Используйте способности gnu linker mocking, параметр --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 в вашем приложении unit test, но невероятно просто реализовать.

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. Определите свою собственную промежуточную функцию с тем же именем. Затем вы можете по-прежнему вызывать оригинал, если хотите. Это намного проще с некоторыми макросами, чтобы помочь с повторением. Также вам придется использовать расширения gnu, если вы хотите имитировать вариационные функции (printf, open и т.д.).

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;

Ответ 4

Хотя технически возможно, я не думаю, что это было бы возможно. Вы должны иметь возможность заменить реализацию этой функции, и вы, вероятно, не сможете и по-прежнему ссылаетесь на стандартную библиотеку в своей системе.

Что вы должны сделать, так это позвонить посреднику. Затем вы можете издеваться над посредником во время теста и просто переходить к фактической функции в процессе производства. Вы даже можете подумать о создании класса, который взаимодействует с этой функцией, и других, подобных ей, и предоставляет более общий интерфейс для вашей программы. Этот класс на самом деле ничего не делал, кроме переадресации вызовов, но во время теста его можно было бы издеваться, и вы можете проверить, что его использует.

Дело в том, чтобы хранить вещи вроде этого, вещи, которые не могут быть протестированы, завернуты в нечто настолько тривиальное, что на самом деле не нуждаются в тестировании, а затем издеваются над этой оболочкой, чтобы протестировать более сложные взаимодействия. KISS особенно важен здесь.

Ответ 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, Энтони Шомихина, здесь.