title: Система тестирования testc
author: Сергей Каличев <serj.kalichev(at)gmail.com>
date: 2020
...
Система тестирования testc
является частью проекта faux
(библиотека вспомогательных функций) и предназначена для модульного тестирования (unit-test) программного обеспечения, написанного на языке C (Си). Утилита testc
последовательно запускает набор тестов, получает результат (успех/неуспех) и генерирует отчет. Каждый тест представляет собой функцию. Источником тестов может являться любой двоичный исполняемый файл - программа или разделяемая библиотека. Для этого внутри исполняемого файла должен быть определен символ с фиксированным именем. Символ указывает на массив, в котором хранится список тестовых функций. Таким образом исполняемый файл может содержать и рабочий код и тестовый код одновременно. Также тестовый код может содержаться и в отдельном модуле. В этом случае модуль должен быть слинкован с необходимыми ему библиотеками.
Система расчитана на максимальную простоту создания тестов, а также на максимальную интеграцию тестирования в процесс разработки кода.
testc
Утилита testc
принимает на вход список исполняемых двоичных файлов и запускает все функции тестирования, содержащиеся в них. В процессе исполнения тестов генерируется отчет и выводится на экран.
Каждая функция тестирования исполняется в отдельном процессе. Благодаря этому неудачные тесты, которые могут быть прерваны сигналом, не влияют на работу самой утилиты testc
, которая продолжит запускать тесты и собирать статистику. Для каждого теста на экран выводится результат его выполнения. Это может быть успех
, неуспех
или сообщение, что тест прерван сигналом
. Если тест завершился с ошибкой, то в отчете появится также текстовый вывод (stdout, stderr) теста. В случае успешного завершения теста, его текстовый вывод подавляется.
Код возврата утилиты testc
будет равен нулю, если все тесты выполнились успешно. Если хоть один тест выполнился с ошибкой или был прерван сигналом, то утилита вернет значение отличное от нуля.
Утилита testc
написана и собрана таким образом, чтобы не иметь внешних зависимостей, за исключением стандартной библиотека libc
. Это позволяет использовать переменную окружения LD_LIBRARY_PATH
для указания пути к тестируемым библиотекам, что полезно для тестирования программного обеспечения на месте, без установки его в систему.
Тестируемые файлы могут задаваться в командной строке утилиты с абсолютным или относительным путем, а также без пути, т.е. только имя разделяемой библиотеки. В случае, когда путь не указан, утилита будет искать файл в путях LD_LIBRARY_PATH
, а затем в стандартных системных путях.
Ниже приведены примеры запуска утилиты testc
с указанием относительного пути, абсолютного пути, поиска по стандартным путям и поиска по путям LD_LIBRARY_PATH
, соответственно.
$ testc ./.libs/libfaux.so.1.0.0
$ testc /home/pkun/faux/.libs/libfaux.so.1.0.0
$ testc libfaux.so
$ LD_LIBRARY_PATH=/home/pkun/faux/.libs testc libfaux.so
-v
, --version
- Показать версию утилиты.-h
, --help
- Показать справку по использованию утилиты.-d
, --debug
- Отображать в отчете вывод всех тестов, независимо от кода возврата.-t
, --preserve-tmp
- Сохранять все временные файлы тестов. Используется для отладки. Опция появилась начиная с версии faux-1.1.0
.$ LD_LIBRARY_PATH=.libs/ testc libfaux.so absent.so libsecond.so
--------------------------------------------------------------------------------
Processing module "libfaux.so" v1.0 ...
Test #001 testc_faux_ini_good() INI subsystem good: success
(!) Test #002 testc_faux_ini_bad() INI bad: failed (-1)
Some debug information here
[!] Test #003 testc_faux_ini_signal() Interrupted by signal: terminated (11)
Module tests: 3
Module errors: 2
--------------------------------------------------------------------------------
Error: Can't open module "absent.so"... Skipped
--------------------------------------------------------------------------------
Processing module "libsecond.so" v1.0 ...
Test #001 testc_faux_ini_good() INI subsystem good: success
(!) Test #002 testc_faux_ini_bad() INI bad: failed (-1)
Some debug information here
[!] Test #003 testc_faux_ini_signal() Interrupted by signal: terminated (11)
Module tests: 3
Module errors: 2
================================================================================
Total modules: 2
Total tests: 6
Total errors: 5
Система тестирования построена так, чтобы можно было писать тесты не линкуясь ни с какими специальными библиотеками тестирования и даже не использовать никакие специальные заголовочные файлы. Все, что требуется, это соответствие функций тестирования прототипу и объявление трех символов со специальными фиксированными именами. Это версия testc
API (старший байт и младший байт) и список функций тестирования.
Функция тестирования должна иметь следующий прототип:
int testc_my_func(void) {
...
}
Имя функции может быть произвольным. Рекомендуется использовать префикс testc_
, чтобы отличать функции тестирования от других.
Функция должна вернуть 0
в случае успеха или любое другое число при возникновении ошибки. Также функция тестирования может выводить на экран (stdout, stderr) любую отладочную информацию. Однако надо помнить, что отладочная информация появится в отчете утилиты testc
только в случае завершения теста с ошибкой. Для успешных тестов вывод подавляется.
Далее приведены простейшие примеры тестов.
Следующий тест всегда завершается успешно. Если тесту для работы нужны какие-либо внешние данные (или информация где эти данные взять), то можно передавать их тестовой функции через переменные окружения. В данном примере выводимая на экран строка никогда не появится в отчете, так как функция завершается успешно, а текстовый вывод успешных тестов подавляется.
int testc_faux_ini_good(void) {
char *path = NULL;
path = getenv("FAUX_INI_PATH");
if (path)
printf("Env var is [%s]\n", path);
return 0;
}
Следующая функция всегда завершается с ошибкой. В отчете появится выводимая на экран строка.
int testc_faux_ini_bad(void) {
printf("Some debug information here\n");
return -1;
}
Следующая функция приводит к Segmentation fault
и тест прерывается сигналом.
int testc_faux_ini_signal(void) {
char *p = NULL;
printf("%s\n", p);
return -1;
}
Отчет о выполнении трех этих функций можно увидеть выше, в разделе Пример отчета
.
Если в будущем прототип тестовой функции изменится, либо изменится формат списка тестовых функций, то утилита testc
должна узнать об этом. Для этого тестируемый объект должен содержать следующие символы:
const unsigned char testc_version_major = 1;
const unsigned char testc_version_minor = 0;
Таким образом тестируемый модуль объявляет версию API, которой он соответствует. Имена символов фиксированы и не могут быть другими. Сейчас существует только одна версия API 1.0
. Однако в будущем это может изменится. И хотя, строго говоря, объявление версии API является необязательным, рекомендуется всегда указывать эту версию. Если версия не указана, то утилита testc
считает, что модуль соответствует самой свежей версии API.
Чтобы утилита testc
узнала о существовании тестовой функции, имя этой функции должно быть упомянуто в списке тестовых функций модуля. Символ, ссылающийся на список тестовых функций, имеет фиксированное имя и тип. Это массив пар текстовых строк.
const char *testc_module[][2] = {
{"testc_faux_ini_good", "INI subsystem good"},
{"testc_faux_ini_bad", "INI bad"},
{"testc_faux_ini_signal", "Interrupted by signal"},
{NULL, NULL}
};
Каждая пара текстовых строк описывает одну тестовую функцию. Первая строка - имя тестовой функции. По этому имени утилита testc
будет искать символ внутри разделяемого объекта.
Вторая строка - произвольное однострочное описание теста. Эта строка печатается в отчете утилиты testc
при выполнении соответствующего теста. Используется для информирования пользователя.
Список тестовых функций должен оканчиваться обязательной нулевой парой {NULL, NULL}
. Без этого утилита не узнает, где кончается список.
Все тестовые функции могут находится в отдельной разделяемой библиотеке, специально предусмотренной для тестирования. Сама библиотека может даже не входить в состав тестируемого проекта.
Тестовые функции могут входить в состав тестируемого проекта, но находиться в отдельных файлах. Эти файлы могут компилироваться или не компилироваться в зависимости от флагов сборки.
# Makefile.am
...
if TESTC
include $(top_srcdir)/testc_module/Makefile.am
endif
...
Тестовые функции могут входить в состав тестируемого проекта, и находиться непосредственно рядом с тестируемыми функциями. Эти тестовые функции могут компилироваться или не компилироваться в зависимости от флагов сборки.
# Makefile.am
if TESTC
libfaux_la_CFLAGS = -DTESTC
endif
int foo(int y) {
...
}
#ifdef TESTC
int testc_foo(void) {
...
if (foo(7)) ...
}
#endif
Такой способ позволит тестировать не только интерфейс библиотеки, но также и локальные статические функции.
Некоторые сложные тесты требуют работы с файлами. Для этого предусмотрено создание отдельной директории с временными файлами для каждого теста. Такая возможность появилась начиная с версии faux-1.1.0
. Утилита testc
создает временную директорию для теста, записывает её путь в переменную окружения TESTC_TMPDIR
и передает тесту. Тест может создавать в этой директории любые, нужные ему, файлы. Директория индивидуальна для каждого теста и никакие файлы других тестов не могут появиться в ней. Тест не должен заботиться о пересечении по именам файлов с другими тестами. После завершения теста, утилита testc
самостоятельно удаляет все содержимое временной директории. Таким образом тест может сам за собой чистить файловую систему, либо оставить эту задачу утилите testc
. Для получения пути временной директории тесту достаточно выполнить следующую команду:
const char *tmpdir = getenv("TESTC_TMPDIR");
Иногда, для отладки тестов, требуется сохранить содержимой временной директории. Для этого используется флаг --preserve-tmp
при вызове утилиты testc
. В этом случае никакие временные файлы не будут удаляться автоматически. Имена временных директорий можно узнать из отчета тестирования. После отладки придется удалить временные файлы самостоятельно вручную.
testc_helpers
Начиная с версии faux-1.1.0
в составе библиотеки faux
появились функции, помогающие в написании тестов. Отмечу, что эти функции, также как и вся библиотека faux
, не являются обязательными при написании тестов. Тесты не обязаны быть слинкованы с библиотекой и не обязаны использовать заголовочные файлы этой библиотеки. Функции лишь помогают и могут быть использованы или не использованы по усмотрению автора тестов. Вспомогательная библиотека содержит заголовочный файл, набор функций и дополнительные утилиты.
faux-file2c
Некоторые тесты требуют для работы наличия определенных файлов. Это могут быть файлы с входными данными, файлы с эталонными данными и т.д. Когда тесты внедрены в разделяемые объекты (библиотеки, исполняемые файлы) не совсем понятно, где хранить файлы, необходимые тестам для работы. Ведь в общем случае тесты могут быть выполнены уже на целевой машине, а не в дереве исходных кодов тестируемого проекта. Одно из возможных решений - внедрение данных в C-код. Утилита faux-file2c
помогает привести внешний файл к формату, пригодному для внедрения в C-файл.
На вход утилиты поступает список файлов, которые необходимо внедрить. На выходе - кусок кода на C, где каждому файлу соответствует переменная типа const char *
, проинициализированная текстовой формой содержимого файла. Утилита может работать в двух режимах - текстовом и двоичном. Текстовый режим предназначен для внедрения текстовых файлов и в C-коде такой файл представлен построчно и доступен для понимания и редактирования человеком, т.к. только специальные символы заменяются на шестнадцатиричные коды, либо экранируются, а большая часть текста представлена "как есть". В двоичном режиме все байты входного файла заменяются на соответствующие коды. Такое представление нечитаемо для человека. Примеры работы утилиты представлены ниже.
$ cat tmpfile
# Comment
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.4 LTS"
COMPLEX_VAR=" Ubuntu 1818 "
WO_QUOTES_VAR = qwerty
WO_QUOTES_VAR2 = qwerty 98989898
EMPTY_VAR3 =
EMPTY_VAR4 =
EMPTY_VAR5 = ""
ANOTHER_VAR6 = "Normal var"
TABBED_VAR = "Normal tabbed var"
# Another comment
# Yet another comment
# Tabbed comment
VAR_WITHOUT_EOL=zxcvbnm
Если внедрять такой файл в текстовом режиме (по-умолчанию):
$ faux-file2c tmpfile
// File "tmpfile"
const char *txt1 =
"# Comment\n"
"DISTRIB_ID=Ubuntu\n"
"DISTRIB_RELEASE=18.04\n"
"DISTRIB_CODENAME=bionic\n"
"DISTRIB_DESCRIPTION=\"Ubuntu 18.04.4 LTS\"\n"
"COMPLEX_VAR=\" Ubuntu\t\t1818 \"\n"
"WO_QUOTES_VAR = qwerty\n"
"WO_QUOTES_VAR2 = qwerty 98989898\n"
"EMPTY_VAR3 = \n"
"EMPTY_VAR4 =\n"
" EMPTY_VAR5 = \"\"\t \n"
" ANOTHER_VAR6 = \"Normal var\"\t \n"
"\tTABBED_VAR = \"Normal tabbed var\"\t \n"
"# Another comment\n"
" # Yet another comment\n"
"\t# Tabbed comment\n"
"VAR_WITHOUT_EOL=zxcvbnm"
;
Если внедрять такой файл в двоичном режиме:
// File "tmpfile"
const char *bin1 =
"\x23\x20\x43\x6f\x6d\x6d\x65\x6e\x74\x0a\x44\x49\x53\x54\x52\x49\x42\x5f\x49\x44"
"\x3d\x55\x62\x75\x6e\x74\x75\x0a\x44\x49\x53\x54\x52\x49\x42\x5f\x52\x45\x4c\x45"
"\x41\x53\x45\x3d\x31\x38\x2e\x30\x34\x0a\x44\x49\x53\x54\x52\x49\x42\x5f\x43\x4f"
"\x44\x45\x4e\x41\x4d\x45\x3d\x62\x69\x6f\x6e\x69\x63\x0a\x44\x49\x53\x54\x52\x49"
"\x42\x5f\x44\x45\x53\x43\x52\x49\x50\x54\x49\x4f\x4e\x3d\x22\x55\x62\x75\x6e\x74"
"\x75\x20\x31\x38\x2e\x30\x34\x2e\x34\x20\x4c\x54\x53\x22\x0a\x43\x4f\x4d\x50\x4c"
"\x45\x58\x5f\x56\x41\x52\x3d\x22\x20\x20\x55\x62\x75\x6e\x74\x75\x09\x09\x31\x38"
"\x31\x38\x20\x22\x0a\x57\x4f\x5f\x51\x55\x4f\x54\x45\x53\x5f\x56\x41\x52\x20\x3d"
"\x20\x71\x77\x65\x72\x74\x79\x0a\x57\x4f\x5f\x51\x55\x4f\x54\x45\x53\x5f\x56\x41"
"\x52\x32\x20\x3d\x20\x71\x77\x65\x72\x74\x79\x20\x39\x38\x39\x38\x39\x38\x39\x38"
"\x0a\x45\x4d\x50\x54\x59\x5f\x56\x41\x52\x33\x20\x3d\x20\x0a\x45\x4d\x50\x54\x59"
"\x5f\x56\x41\x52\x34\x20\x3d\x0a\x20\x20\x20\x20\x20\x45\x4d\x50\x54\x59\x5f\x56"
"\x41\x52\x35\x20\x3d\x20\x22\x22\x09\x20\x20\x20\x0a\x20\x20\x20\x20\x20\x41\x4e"
"\x4f\x54\x48\x45\x52\x5f\x56\x41\x52\x36\x20\x3d\x20\x22\x4e\x6f\x72\x6d\x61\x6c"
"\x20\x76\x61\x72\x22\x09\x20\x20\x20\x0a\x09\x54\x41\x42\x42\x45\x44\x5f\x56\x41"
"\x52\x20\x3d\x20\x22\x4e\x6f\x72\x6d\x61\x6c\x20\x74\x61\x62\x62\x65\x64\x20\x76"
"\x61\x72\x22\x09\x20\x20\x20\x0a\x23\x20\x41\x6e\x6f\x74\x68\x65\x72\x20\x63\x6f"
"\x6d\x6d\x65\x6e\x74\x0a\x20\x20\x23\x20\x59\x65\x74\x20\x61\x6e\x6f\x74\x68\x65"
"\x72\x20\x63\x6f\x6d\x6d\x65\x6e\x74\x0a\x09\x23\x20\x54\x61\x62\x62\x65\x64\x20"
"\x63\x6f\x6d\x6d\x65\x6e\x74\x0a\x56\x41\x52\x5f\x57\x49\x54\x48\x4f\x55\x54\x5f"
"\x45\x4f\x4c\x3d\x7a\x78\x63\x76\x62\x6e\x6d"
;
Полученные фрагменты кода копируются в C-файл. Вспомогательная библиотека testc_helpers
содержит специальные функции, позволяющие одной строкой кода создать файл на диске, используя внедренные данные. Далее тест может работать с настоящими файлами.
-v
, --version
- Показать версию утилиты.-h
, --help
- Показать справку по использованию утилиты.-b
, --binary
- Двоичные режим преобразования.-t
, --text
- Текстовый режим преобразования (по-умолчанию).Заголовочный файл faux/testc_helpers.h
содержит объявления всех вспомогательных функций. Также он содержит макрос FAUX_TESTC_TMPDIR_ENV
с именем переменной окружения, определяющей путь до директории с временными файлами.
Функция создает файл с именем, указанным первым аргументом, и записывает в него содержимое строкового буфера (кончается на '\0'
), указанного вторым аргументом. В буфере может храниться содержимое файла, внедренного при помощи утилиты faux-file2c
.
// Etalon file
const char *etalon_file =
"ANOTHER_VAR6=\"Normal var\"\n"
"COMPLEX_VAR=\" Ubuntu\t\t1818 \"\n"
;
char *etalon_fn = NULL;
int ret = 0;
etalon_fn = str_faux_sprintf("%s/%s", getenv(FAUX_TESTC_TMPDIR_VAR), "etalon.txt");
ret = faux_testc_file_deploy(etalon_fn, etalon_file);
...
faux_str_free(etalon_fn);
Функция работает аналогично функции faux_testc_file_deploy()
, только генерирует уникальное имя файла самостоятельно и возвращает строку с именем созданного файла. Функция использует переменную окружения TESTC_TMPDIR
для того, чтобы создать временный файл в специальной директории, созданной для хранения временных файлов теста.
// Etalon file
const char *etalon_file =
"ANOTHER_VAR6=\"Normal var\"\n"
"COMPLEX_VAR=\" Ubuntu\t\t1818 \"\n"
;
char *etalon_fn = NULL;
etalon_fn = faux_testc_tmpfile_deploy(etalon_file);
...
faux_str_free(etalon_fn);
Функция по-байтово сравнивает два файла, имена которых заданы аргументами. Возвращает 0
, если файлы идентичны.
if (faux_testc_file_cmp(dst_fn, etalon_fn) != 0) {
fprintf(stderr, "Generated file is not equal to etalon.\n");
...
}