---
title: Система тестирования `testc`
author: Сергей Каличев <serj.kalichev(at)gmail.com>
date: 2020
...


# О проекте

Система тестирования `testc` является частью проекта `faux` (библиотека вспомогательных функций) и предназначена для модульного тестирования (unit-test) программного обеспечения, написанного на языке C (Си). Утилита `testc` последовательно запускает набор тестов, получает результат (успех/неуспех) и генерирует отчет. Каждый тест представляет собой функцию. Источником тестов может являться любой двоичный исполняемый файл - программа или разделяемая библиотека. Для этого внутри исполняемого файла должен быть определен символ с фиксированным именем. Символ указывает на массив, в котором хранится список тестовых функций. Таким образом исполняемый файл может содержать и рабочий код и тестовый код одновременно. Также тестовый код может содержаться и в отдельном модуле. В этом случае модуль должен быть слинкован с необходимыми ему библиотеками.

Система расчитана на максимальную простоту создания тестов, а также на максимальную интеграцию тестирования в процесс разработки кода.


## Ссылки

* Репозиторий GIT <https://src.libcode.org/pkun/faux>
* Релизы <http://libcode.org/projects/faux>
* Список рассылки <http://groups.google.com/group/libfaux>


# Утилита `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");
	...
}
```