--- title: Система тестирования `testc` author: Сергей Каличев <serj.kalichev(at)gmail.com> date: 2020 ... # О проекте Система тестирования `testc` является частью проекта `faux` (библиотека вспомогательных функций) и предназначена для модульного тестирования (unit-test) программного обеспечения, написанного на языке C (Си). Утилита `testc` последовательно запускает набор тестов, получает результат (успех/неуспех) и генерирует отчет. Каждый тест представляет собой функцию. Источником тестов может являться любой двоичный исполняемый файл - программа или разделяемая библиотека. Для этого внутри исполняемого файла должен быть определен символ с фиксированным именем. Символ указывает на массив, в котором хранится список тестовых функций. Таким образом исполняемый файл может содержать и рабочий код и тестовый код одновременно. Также тестовый код может содержаться и в отдельном модуле. В этом случае модуль должен быть слинкован с необходимыми ему библиотеками. Система расчитана на максимальную простоту создания тестов, а также на максимальную интеграцию тестирования в процесс разработки кода. ## Ссылки * Репозиторий GIT * Релизы * Список рассылки # Утилита `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; } ``` Отчет о выполнении трех этих функций можно увидеть выше, в разделе `Пример отчета`. ## Версия API Если в будущем прототип тестовой функции изменится, либо изменится формат списка тестовых функций, то утилита `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` с именем переменной окружения, определяющей путь до директории с временными файлами. ### Вспомогательные функции #### Функция faux_testc_file_deploy() Функция создает файл с именем, указанным первым аргументом, и записывает в него содержимое строкового буфера (кончается на `'\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_tmpfile_deploy() Функция работает аналогично функции `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); ``` #### Функция faux_testc_file_cmp() Функция по-байтово сравнивает два файла, имена которых заданы аргументами. Возвращает `0`, если файлы идентичны. ``` if (faux_testc_file_cmp(dst_fn, etalon_fn) != 0) { fprintf(stderr, "Generated file is not equal to etalon.\n"); ... } ```