testc.ru.md 27 KB


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

Отчет о выполнении трех этих функций можно увидеть выше, в разделе Пример отчета.

Версия 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");
	...
}