У меня есть класс, который вызывает getaddrinfo для поиска в DNS. Во время тестирования я хочу моделировать различные ошибки, связанные с этим системным вызовом. Какой рекомендуемый метод для издевательских системных вызовов вроде этого? Я использую Boost.Test для тестирования модулей.
Советы по развязыванию системных вызовов
Ответ 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, Энтони Шомихина, здесь.