После прочтения книги Test Driven Development for Embedded C я начал знакомство с миром юнит-тестирования с фреймворка cppUtest. Не в последнюю очередь потому, что в нем свеженаписанный тест регистрируется и запускается самостоятельно. За это приходится платить — использованием C++, динамическим выделением памяти где-то в глубинах фреймворка. Может быть, можно как-то попроще?
Совсем недавно я узнал о минималистичном фреймворке minUnit, который умещается всего в 4 строчки.
Я приведу их здесь для наглядности:
Просто и красиво. При этом написание теста выглядит вот так:
К сожалению, когда я попытался этим фреймворком воспользоваться, то очень быстро понял, что мне ужасно лень руками регистрировать каждый тест. Это ведь нужно заводить заголовочный файл для файла с тестами, каждому тесту в этот файл прописывать объявление, потом идти в main и прописывать вызов!
Посмотрел я на другие фреймворки, написанные на чистом С: почти везде тоже самое. В качестве альтернативы предлагаются отдельные программы, сканирующие исходники с тестами и генерирующими код для запуска.
Но может быть, можно проще?
Уверенность в меня вселил вот этот пост, где для регистрации тестов используется линкер. Но мне привязываться к линкеру и специфичным для компилятора атрибутам не хотелось.
Насколько мне известно, на чистом С невозможно сделать полноценную регистрацию теста. А как насчет полуавтоматической?
Идея оформилась следующим образом. Для каждого модуля пишется файл module_tests.c, в нем пишутся все тесты для данного модуля. Эти тесты образуют группу. В этом же файле пишется магическая функция запуска всех тестов в группе.
А в main’е нужно руками прописывать только запуск группы, а не каждого теста в отдельности.
Это сводится к следующей задаче: нужно как-то получить список всех функций в файле. В С это можно сделать только с помощью препроцессора. Но как? Например, если функции будут называться как-то однообразно.
«Служебные» имена тестов вполне могут быть какими угодно, лишь бы заголовок у теста был внятный!
Значит, нужно с помощью препроцессора генерировать имена для функций-тестов, причем однообразно и по единому шаблону. Например, вот так:
Признаюсь честно, boost я использовал в первый раз в жизни и был до глубины души поражен мощью препроцессора С!
Теперь можно писать тесты следующим образом:
После этого инклуда счетчик проинкрементируется и имя для следующего теста будет сгенерировано имя static char * umba_test_1_(void).
Осталось только сгенерировать функцию, которая будет запускать все тесты в файле. Для этого создается массив указателей на функции и заполняется указателями на тесты. Потом функция просто в цикле вызывает каждый тест из массива.
Эту функцию нужно будет обязательно писать в конце файла с тестами, чтобы значение UMBA_TEST_COUNTER равнялось номеру последнего теста.
Для генерирования массива указателей я сперва пошел по простому пути и написал helper-файл вот такого вида:
В принципе, вполне можно обойтись и этим, сгенерировав объявления для нескольких сотен тестов. Тогда от boost'a нужен будет только один файл — boost/preprocessor/slot/counter.hpp.
Но, раз уж я начал использовать boost, почему бы не продолжить?
Всего две строчки, но какое могущество за ними скрыто!
Добавляем тривиальный код для самой функции запуска группы:
И для ее запуска из main:
Вуаля. Теперь запуск группы с любым количеством тестов выглядит одинаково:
Я вполне доволен этим результатом. Механических действий при написании теста теперь заметно меньше.
Все упомянутые макросы умещаются в 40-50 строк, что, к сожалению, несколько больше, чем minUnit (и гораздо менее очевидно).
Весь код целиком.
Да, здесь отсутствует уйма функционала из больших фреймворков, но, честно признаюсь, мне ни разу не довелось воспользоваться в тесте чем-то помимо простой проверки вида CHECK(if true).
Description для теста просто выкидывается, но сделать с ним что-то полезное вроде бы несложно, если вдруг захочется.
Что мне хотелось бы выяснить:
Совсем недавно я узнал о минималистичном фреймворке minUnit, который умещается всего в 4 строчки.
Я приведу их здесь для наглядности:
#define mu_assert(message, test) do { if (!(test)) return message; } while (0)
#define mu_run_test(test) do { char *message = test(); tests_run++; \
if (message) return message; } while (0)
extern int tests_run;
Просто и красиво. При этом написание теста выглядит вот так:
static char * test_foo() {
mu_assert("error, foo != 7", foo == 7);
return 0;
}
К сожалению, когда я попытался этим фреймворком воспользоваться, то очень быстро понял, что мне ужасно лень руками регистрировать каждый тест. Это ведь нужно заводить заголовочный файл для файла с тестами, каждому тесту в этот файл прописывать объявление, потом идти в main и прописывать вызов!
Посмотрел я на другие фреймворки, написанные на чистом С: почти везде тоже самое. В качестве альтернативы предлагаются отдельные программы, сканирующие исходники с тестами и генерирующими код для запуска.
Но может быть, можно проще?
Уверенность в меня вселил вот этот пост, где для регистрации тестов используется линкер. Но мне привязываться к линкеру и специфичным для компилятора атрибутам не хотелось.
Насколько мне известно, на чистом С невозможно сделать полноценную регистрацию теста. А как насчет полуавтоматической?
Идея оформилась следующим образом. Для каждого модуля пишется файл module_tests.c, в нем пишутся все тесты для данного модуля. Эти тесты образуют группу. В этом же файле пишется магическая функция запуска всех тестов в группе.
А в main’е нужно руками прописывать только запуск группы, а не каждого теста в отдельности.
Это сводится к следующей задаче: нужно как-то получить список всех функций в файле. В С это можно сделать только с помощью препроцессора. Но как? Например, если функции будут называться как-то однообразно.
«Служебные» имена тестов вполне могут быть какими угодно, лишь бы заголовок у теста был внятный!
Значит, нужно с помощью препроцессора генерировать имена для функций-тестов, причем однообразно и по единому шаблону. Например, вот так:
#define UMBA_TEST_COUNTER BOOST_PP_COUNTER
#define UMBA_TEST_INCREMENT() BOOST_PP_UPDATE_COUNTER()
#define UMBA_TOKEN(x, y, z) x ## y ## z
#define UMBA_TOKEN2(x, y, z) UMBA_TOKEN(x,y,z)
#define UMBA_TEST( description ) static char * UMBA_TOKEN2(umba_test_, UMBA_TEST_COUNTER, _(void) )
Признаюсь честно, boost я использовал в первый раз в жизни и был до глубины души поражен мощью препроцессора С!
Теперь можно писать тесты следующим образом:
UMBA_TEST("Simple Test") // получается static char * umba_test_0_(void)
{
uint8_t a = 1;
uint8_t b = 2;
UMBA_CHECK(a == b, "MATHS BROKE");
return 0;
}
#include UMBA_TEST_INCREMENT()
После этого инклуда счетчик проинкрементируется и имя для следующего теста будет сгенерировано имя static char * umba_test_1_(void).
Осталось только сгенерировать функцию, которая будет запускать все тесты в файле. Для этого создается массив указателей на функции и заполняется указателями на тесты. Потом функция просто в цикле вызывает каждый тест из массива.
Эту функцию нужно будет обязательно писать в конце файла с тестами, чтобы значение UMBA_TEST_COUNTER равнялось номеру последнего теста.
Для генерирования массива указателей я сперва пошел по простому пути и написал helper-файл вот такого вида:
#if UMBA_TEST_COUNTER == 1
#define UMBA_LOCAL_TEST_ARRAY UmbaTest umba_local_test_array[ UMBA_TEST_COUNTER ] = {umba_test_0_};
#elif UMBA_TEST_COUNTER == 2
#define UMBA_LOCAL_TEST_ARRAY UmbaTest umba_local_test_array[ UMBA_TEST_COUNTER ] = {umba_test_0_, umba_test_1_};
…
В принципе, вполне можно обойтись и этим, сгенерировав объявления для нескольких сотен тестов. Тогда от boost'a нужен будет только один файл — boost/preprocessor/slot/counter.hpp.
Но, раз уж я начал использовать boost, почему бы не продолжить?
#define UMBA_DECL(z, n, text) text ## n ## _,
#define UMBA_LOCAL_TEST_ARRAY UmbaTest umba_local_test_array[ UMBA_TEST_COUNTER ] = { BOOST_PP_REPEAT( UMBA_TEST_COUNTER, UMBA_DECL, umba_test_ ) }
Всего две строчки, но какое могущество за ними скрыто!
Добавляем тривиальный код для самой функции запуска группы:
#define UMBA_RUN_LOCAL_TEST_GROUP( groupName ) UMBA_LOCAL_TEST_ARRAY; \
char * umba_run_test_group_ ## groupName ## _(void) \
{ \
for(uint32_t i=0; i < UMBA_TEST_COUNTER; i++) \
{ \
tests_run++; \
char * message = umba_local_test_array[i](); \
if(message) \
return message; \
} \
return 0; \
} \
И для ее запуска из main:
#define UMBA_EXTERN_TEST_GROUP( groupName ) char * umba_run_test_group_ ## groupName ## _(void);
#define UMBA_RUN_GROUP( groupName ) do { \
char *message = umba_run_test_group_ ## groupName ## _(); \
tests_run++; \
if (message) return message; \
} while (0)
Вуаля. Теперь запуск группы с любым количеством тестов выглядит одинаково:
UMBA_EXTERN_TEST_GROUP( SimpleGroup )
static char * run_all_tests(void)
{
UMBA_RUN_GROUP( SimpleGroup );
return 0;
}
int main(void)
{
char *result = run_all_tests();
if (result != 0 )
{
printf("!!!!!!!!!!!!!!!!!!!\n");
printf("%s\n", result);
}
else
{
printf("ALL TESTS PASSED\n");
}
printf("Tests run: %d\n", tests_run-1);
return 0;
}
Я вполне доволен этим результатом. Механических действий при написании теста теперь заметно меньше.
Все упомянутые макросы умещаются в 40-50 строк, что, к сожалению, несколько больше, чем minUnit (и гораздо менее очевидно).
Весь код целиком.
Да, здесь отсутствует уйма функционала из больших фреймворков, но, честно признаюсь, мне ни разу не довелось воспользоваться в тесте чем-то помимо простой проверки вида CHECK(if true).
Description для теста просто выкидывается, но сделать с ним что-то полезное вроде бы несложно, если вдруг захочется.
Что мне хотелось бы выяснить:
- Изобрел ли я что-то новое или подобным трюком уже пользуются много лет?
- Можно ли сие как-то улучшить? Мне не очень нравится необходимость писать какой-то странный инклуд после каждого теста, но других реализаций счетчика на препроцессоре я не нашел (__COUNT__ мой компилятор не поддерживает).
- Стоит ли использовать самодельный фреймворк в продакшене?
- Как, черт побери, работает BOOST_PP_COUNTER?! Даже на stackoverflow ответом на соответствующий вопрос является «magic».