Домашняя страница: https://github.com/mity/acutest
Acutest — это средство модульного тестирования C/C++, призванное быть максимально простым, не мешать разработчику и минимизировать любые внешние зависимости.
Для этого полная реализация размещается в одном заголовочном файле C, а его ядро зависит лишь от нескольких стандартных функций библиотеки C.
Основные особенности:
acutest.h
.main()
).--tap
).--xml-output=FILE
).Особенности C++:
std::exception
, what()
записывается в сообщении об ошибке.Особенности Unix/Posix:
clock_gettime()
), пользователь может измерять время выполнения теста с помощью --time=real
(то же самое, что --time
) и --time=cpu
.Особенности Linux:
Особенности Windows:
--time
.Особенности macOS:
Любой модуль C/C++, реализующий один или несколько модульных тестов и включающий acutest.h
, может быть построен как отдельная программа. В данном документе мы называем полученный двоичный файл «набором тестов». Затем пакет запускается для запуска тестов, как указано в его параметрах командной строки.
Мы говорим, что любой модульный тест успешен тогда и только тогда, когда:
Чтобы использовать Acutest, просто включите заголовочный файл acutest.h
в начало исходного файла C/C++, реализующего один или несколько модульных тестов. Обратите внимание, что заголовок обеспечивает реализацию функции main()
.
#include "acutest.h"
Каждый тест предполагается реализовать как функцию со следующим прототипом:
void test_example ( void );
Тесты могут использовать некоторые макросы препроцессора для проверки условий тестирования. Их можно использовать несколько раз, и если какое-либо из этих условий не выполнено, конкретный тест считается неудавшимся.
TEST_CHECK
— это наиболее часто используемый макрос тестирования, который просто проверяет логическое условие и завершается сбоем, если условие оценивается как ложное (или ноль).
Например:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_CHECK ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_CHECK ( mem != NULL );
}
Обратите внимание, что тесты должны быть полностью независимы друг от друга. При каждом вызове набора тестов пользователь может запускать любое количество тестов в наборе в любом порядке. Более того, по умолчанию на поддерживаемых платформах каждый модульный тест выполняется как отдельный (под)процесс.
Наконец, исходный файл набора тестов должен содержать список модульных тестов с помощью макроса TEST_LIST
. В списке указывается имя каждого теста (оно должно быть уникальным) и указатель на функцию, реализующую тест. Я рекомендую имена, которые легко использовать в командной строке: особенно избегайте пробелов и других специальных символов, которые могут потребовать экранирования в оболочке; также избегайте тире ( -
) в качестве первого символа имени, так как тогда его можно будет интерпретировать как параметр командной строки, а не как имя теста.
TEST_LIST = {
{ "example" , test_example },
...
{ NULL, NULL } /* zeroed record marking the end of the list */
};
Обратите внимание, что список тестов должен заканчиваться обнуленной записью.
Для базовых наборов тестов это более или менее все, что вам нужно знать. Однако Acutest предоставляет еще несколько макросов, которые могут быть полезны в некоторых конкретных ситуациях. Мы рассмотрим их в следующих подразделах.
Существует макрос TEST_ASSERT
, который очень похож на TEST_CHECK
, но в случае неудачи он мгновенно прерывает выполнение текущего модульного теста.
Например:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_ASSERT ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_ASSERT ( mem != NULL );
}
Прерывание в случае неудачи выполняется либо вызовом abort()
(если тест выполняется как дочерний процесс), либо посредством longjmp()
(если это не так).
Поэтому его следует использовать только в том случае, если вы понимаете затраты, связанные с таким жестоким прерыванием теста. В зависимости от того, что делает ваш модульный тест, он может включать в себя несброшенные файловые дескрипторы, утечки памяти, объекты C++, разрушенные без вызова их деструкторов, и многое другое.
В общем, TEST_CHECK
следует отдавать предпочтение перед TEST_ASSERT
, если только вы точно не знаете, что делаете и почему выбрали TEST_ASSERT
в какой-то конкретной ситуации.
Для C++ существует дополнительный макрос TEST_EXCEPTION
для проверки того, что данный код (обычно просто функция или вызов метода) генерирует исключение ожидаемого типа.
Проверка завершается неудачно, если функция не выдает никаких исключений или выдает что-то несовместимое.
Например:
void test_example ( void )
{
TEST_EXCEPTION ( CallSomeFunction (), std:: exception );
}
Если проверка условия не удалась, часто бывает полезно предоставить некоторую дополнительную информацию о ситуации, чтобы облегчить отладку проблемы. Для этой цели Acutest предоставляет макросы TEST_MSG
и TEST_DUMP
.
Первый выводит любое сообщение типа printf
, другой выводит шестнадцатеричный дамп предоставленного блока памяти.
Так, например:
void test_example ( void )
{
char produced [ 100 ];
char expected [] = "Hello World!" ;
SomeSprintfLikeFunction ( produced , "Hello %s!" , "world" );
TEST_CHECK ( strcmp ( produced , expected ) == 0 );
TEST_MSG ( "Expected: %s" , expected );
TEST_MSG ( "Produced: %s" , produced );
/* Or, if the function could provide some binary stuff, we might rather
* use TEST_DUMP instead in order to output a hexadecimal dump of the data.
*/
TEST_DUMP ( "Expected:" , expected , strlen ( expected ));
TEST_DUMP ( "Produced:" , produced , strlen ( produced ));
}
Обратите внимание, что оба макроса выводят что-либо только в том случае, если последний проверяемый макрос не удался. Другими словами, эти два эквивалентны:
if (! TEST_CHECK ( some_condition != 0 ))
TEST_MSG ( "some message" );
TEST_CHECK ( some_condition != 0 );
TEST_MSG ( "some message" );
(Обратите внимание, что для TEST_MSG
требуется компилятор с поддержкой переменных макросов.)
Иногда полезно спроектировать функцию тестирования как цикл обработки данных, обеспечивающий набор тестовых векторов и соответствующих им ожидаемых результатов. Например, представьте, что наш модульный тест должен проверять какую-то реализацию хеш-функции, и у нас есть набор тестовых векторов для нее в спецификации хеш-функции.
В таких случаях очень полезно получить какое-то имя, связанное с каждым тестовым вектором, и вывести это имя в выходной журнал, чтобы в случае неудачной проверки можно было легко определить виновный тестовый вектор. Однако тело цикла может выполнять десятки проверочных макросов, поэтому добавлять такое имя для настройки каждого проверочного сообщения в цикле может оказаться непрактичным.
Чтобы решить эту проблему, Acutest предоставляет макрос TEST_CASE
. Макрос задает строку, служащую именем тестового вектора. При использовании Acutest гарантирует, что в выходном журнале указанное имя предшествует любому сообщению из последующих проверок условий.
Например, предположим, что мы тестируем SomeFunction()
которая должна для данного байтового массива некоторого размера возвращать другой массив байтов в новом буфере, созданном malloc
. Тогда мы можем сделать что-то вроде этого:
struct TestVector {
const char * name ;
const uint8_t * input ;
size_t input_size ;
const uint8_t * expected_output ;
size_t expected_output_size ;
};
struct TestVector test_vectors [] = {
/* some data */
};
void test_example ( void )
{
int i ;
const uint8_t * output ;
size_t output_size ;
for ( i = 0 ; i < sizeof ( test_vectors ) / sizeof ( test_vectors [ 0 ]); i ++ ) {
struct TestVector * vec = & test_vectors [ i ];
/* Output the name of the tested test vector. */
TEST_CASE ( vec . name );
/* Now, we can check the function produces what it should for the
* current test vector. If any of the following checking macros
* produces any output (either because the check fails, or because
* high `--verbose` level is used), Acutest also outputs the currently
* tested vector's name. */
output = SomeFunction ( vec -> input , vec -> input_size , & output_size );
if ( TEST_CHECK ( output != NULL )) {
TEST_CHECK ( output_size == vec -> expected_output_size );
TEST_CHECK ( memcmp ( output , vec -> expected_output , output_size ) == 0 );
free ( output );
}
}
}
Указанное имя применяется ко всем проверкам, выполняемым после использования TEST_CASE
TEST_CASE
не будет использован снова для указания другого имени; илиTEST_CASE
с NULL
в качестве аргумента.Многие из макросов, упомянутых в предыдущих разделах, имеют аналог, который позволяет выводить собственные сообщения вместо некоторых сообщений по умолчанию.
Все они имеют то же имя, что и вышеупомянутые макросы, только с добавлением суффикса подчеркивания. Вместе с суффиксом они ожидают формат строки, подобный printf
, и соответствующие дополнительные аргументы.
Так, например, вместо простой проверки макросов
TEST_CHECK (a == b);
TEST_ASSERT (x < y);
TEST_EXCEPTION (SomeFunction(), std::exception);
мы можем использовать их соответствующие аналоги с собственными сообщениями:
TEST_CHECK_ (a == b, " %d is equal to %d " , a, b);
TEST_ASSERT_ (x < y, " %d is lower than %d " , x, y);
TEST_EXCEPTION_ (SomeFunction(), std::exception, "SomeFunction() throws std::exception");
Вам следует использовать для них какую-то нейтральную формулировку, потому что с параметром командной строки --verbose
сообщения выходят из системы, даже если соответствующая проверка проходит успешно.
(Если вам необходимо вывести некоторую диагностическую информацию на случай, если проверка не удалась, используйте макрос TEST_MSG
. Именно в этом его цель.)
Аналогично, вместо
TEST_CASE ( "name" );
мы можем использовать богаче
TEST_CASE_ ( "iteration #%d" , 42 );
Однако обратите внимание, что все это можно использовать только в том случае, если ваш компилятор поддерживает макросы препроцессора с переменным числом вариантов. Вариативные макросы стали стандартной частью языка C с выходом C99.
Когда мы закончим реализацию тестов, мы можем просто скомпилировать их как простую программу C/C++. Например, предположим, что cc
— это ваш компилятор C:
$ cc test_example.c -o test_example
После компиляции набора тестов полученный двоичный файл тестирования можно использовать для запуска тестов.
Код выхода набора тестов равен 0, если все выполненные модульные тесты пройдены успешно, 1, если какой-либо из них не пройден, или любому другому числу, если возникает внутренняя ошибка.
По умолчанию (без каких-либо параметров командной строки) он запускает все реализованные модульные тесты. Он также может запускать только часть модульных тестов, как указано в командной строке:
$ ./test_example # Runs all tests in the suite
$ ./test_example test1 test2 # Runs only tests specified
$ ./test_example --exclude test3 # Runs all tests but those specified
Обратите внимание, что один аргумент командной строки может выбрать целую группу тестовых модулей, поскольку Acutest реализует несколько уровней выбора модульных тестов (используется первый уровень, соответствующий хотя бы одному тестовому блоку):
Точное соответствие : если аргумент точно соответствует полному имени модульного теста, выбирается только данный тест.
Соответствие слов : если аргумент не соответствует ни одному полному имени теста, но соответствует целому слову в одном или нескольких именах тестов, то выбираются все такие тесты.
Следующие символы распознаются как разделители слов: пробел
, табулятор t
, тире -
, подчеркивание _
, косая черта /
, точка .
, запятая ,
, двоеточие :
, точка с запятой ;
.
Соответствие подстроке : если даже при совпадении слова не удалось выбрать какой-либо тест, то выбираются все тесты, имя которых содержит аргумент в качестве подстроки.
Приняв соответствующую стратегию именования тестов, это позволяет пользователю легко запускать (или пропускать, если используется --exclude
) целое семейство связанных тестов с одним аргументом командной строки.
Например, рассмотрим набор тестов test_example
, который реализует тесты foo-1
, foo-2
, foomatic
, bar-1
и bar-10
:
$ ./test_example bar-1 # Runs only the test 'bar-1' (exact match)
$ ./test_example foo # Runs 'foo-1' and 'foo-2' (word match)
$ ./test_example oo # Runs 'foo-1', 'foo-2' and 'foomatic' (substring match)
$ ./test_example 1 # Runs 'foo-1' and 'bar-1' (word match)
Вы можете использовать --list
или -l
, чтобы просто перечислить все модульные тесты, реализованные данным набором тестов:
$ ./test_example --list
Чтобы просмотреть описание всех поддерживаемых параметров командной строки, запустите двоичный файл с параметром --help
:
$ ./test_example --help
Вопрос: Разве этот проект не назывался «CUTest»?
А: Да. Он был переименован, поскольку исходное название оказалось слишком перегруженным.
В: Нужно ли мне распространять файлы README.md
и/или LICENSE.md
?
О: Нет. Заголовок acutest.h
содержит URL-адрес нашего репозитория, примечание об авторских правах и условия лицензии MIT. Пока вы оставляете их нетронутыми, мы вполне согласны, если вы добавите в свой проект только заголовок. В конце концов, простота использования и универсальный заголовок — наша основная цель.
Acutest распространяется под лицензией MIT, полный текст см. в файле LICENSE.md
или в начале acutest.h
.
Проект находится на github:
Там вы можете найти последнюю версию Acutest, внести свой вклад в улучшения или сообщить об ошибках.