Pull to refresh

Полуавтоматическая регистрация юнит-тестов на чистом С

Reading time4 min
Views8.7K
После прочтения книги Test Driven Development for Embedded C я начал знакомство с миром юнит-тестирования с фреймворка cppUtest. Не в последнюю очередь потому, что в нем свеженаписанный тест регистрируется и запускается самостоятельно. За это приходится платить — использованием C++, динамическим выделением памяти где-то в глубинах фреймворка. Может быть, можно как-то попроще?
Совсем недавно я узнал о минималистичном фреймворке 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 для теста просто выкидывается, но сделать с ним что-то полезное вроде бы несложно, если вдруг захочется.

Что мне хотелось бы выяснить:
  1. Изобрел ли я что-то новое или подобным трюком уже пользуются много лет?
  2. Можно ли сие как-то улучшить? Мне не очень нравится необходимость писать какой-то странный инклуд после каждого теста, но других реализаций счетчика на препроцессоре я не нашел (__COUNT__ мой компилятор не поддерживает).
  3. Стоит ли использовать самодельный фреймворк в продакшене?
  4. Как, черт побери, работает BOOST_PP_COUNTER?! Даже на stackoverflow ответом на соответствующий вопрос является «magic».
Tags:
Hubs:
+13
Comments15

Articles