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

    После прочтения книги 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».
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 15

      0
      В моем фреймворке так:

      #include "TCppUnit.h"
      
      int wmain(int argc, wchar_t* argv[])
      {
          CPPUNIT_TEST_PACKAGE_START;
          {
              if (!CallAllUnits(counters, fnMsg))
              {
                  LogError(L"Could not pass post utility internal tests");
      
                  return -1;
              }
          }
          CPPUNIT_TEST_PACKAGE_END;
      }
      
      


      В группе тестов:

      #include "TCppUnit.h"
      
      static class FourTests : public TCppUnit
      {
      private:
          std::vector<size_t> m_listLocations;
      public:
          REGISTER_CLASS(FourTests, 4){}     // FourTests - название группы тестов.  4  - порядок группы в общем тесте
      
          virtual void TearsUp()
          {
              m_listLocations.clear();
          }
      
          void TestREVC() //  Counting DNA Nucleotides 
          {
              bool result = Test_CompStandOfDNA();
      
              CPPUNIT_ASSERT_EQUAL_MESSAGE( "Could not processed complimentary paires of DNA", true,  result );
          }
      
          void TestHAMM() //  Evolution as a Sequence of Mistakes
          {
              const char* set1 = "GAGCCTACTAACGGGAT";
              const char* set2 = "CATCGTAATGACGGCCT";
      
              size_t result = TDNA::GetHammingDistance( set1, set2 );
      
              CPPUNIT_ASSERT_EQUAL_MESSAGE( "Must process complimentary paires of DNA", 7,  result );
          }
      
      
      };
      
      BEGIN_UNIT_REGISTATION(FourTests)
      
      CPPUNIT_TEST(TestREVC)
      CPPUNIT_TEST(TestHAMM)
      
      END_UNIT_REGISTATION(FourTests);
      
      


      Переменная класса глобальная и при использовании в конструкторе регистрируется в глобальном списке функторов, который запускается в main

      Мо фреймворк конечно не UnitCPP но идея использования вышла оттуда. Его как-то резко остановили поддерживать, а куда деваться с накопленным багажем. Я еще туда и проверку утечек памяти встроил — очень удобно оперативно отслежкосякиивать.

        0
        (__COUNT__ мой компилятор не поддерживает).

        А __LINE__?
          0
          __LINE__ поддерживается, но как это использовать — я не придумал.
          0
          что за компилятор? Может и нам надо?
            0
            Keil ARMCC. Простите, не понял, что вам может быть надо?
            0
            По поводу:
            Как, черт побери, работает BOOST_PP_COUNTER?! Даже на stackoverflow ответом на соответствующий вопрос является «magic».

            Я не использовал BOOST_PP_COUNTER, но мне стало интересно. Насколько я понял, всё сводится к следующему:
            // File counter.hpp
            #ifndef COUNTER_H_
            #define COUNTER_H_
            
            // Начальное значение
            #define COUNTER 0
            
            // Файлик, при включении которого будет происходить
            // прибавление еденицы к текущему значению и, вообще,
            // вся магия
            #define UPDATE_COUNTER() "counter_inc.hpp"
            
            #endif
            


            // File counter_inc.hpp
            
            // Увеличиваем текущее значение,
            // сохраняем во временную переменную
            #define INC_COUNTER COUNTER + 1
            
            // Удаляем старое значение,
            // ниже будет определено новое
            #undef COUNTER
            
            // Грубо говоря, вся суть сводится к этому, но
            // не так просто: текущее значение @INC_COUNTER
            // разбирается на 10 разрядов (смотри 
            // boost\preprocessor\slot\detail\shared.hpp),
            // которые потом собираются вместе в @COUNTER (
            // смотри t_1_56_0\boost\preprocessor\slot\detail\counter.hpp):
            // # if BOOST_PP_COUNTER_DIGIT_10
            // #    define BOOST_PP_COUNTER BOOST_PP_SLOT_CC_10(BOOST_PP_COUNTER_DIGIT_10, /*ещё разряды*/ BOOST_PP_COUNTER_DIGIT_1)
            // # elif BOOST_PP_COUNTER_DIGIT_9
            // ...
            // 
            // 
            // Т.е. максимальное значение @BOOST_PP_COUNTER состоит
            // из 10 разрядов: 999999999.
            #if(INC_COUNTER == 0)
            # define COUNTER 0
            #elif(INC_COUNTER == 1)
            # define COUNTER 1
            // ...
            #endif
            
            // Удаляем временную переменную
            #undef INC_COUNTER
            
            


            // main.cpp
            
            #include <iostream>
            #include "counter.hpp"
            
            #include UPDATE_COUNTER()
            
            int main()
            {
            	std::cout << COUNTER << std::endl;
            }
            
              0
              У меня ощущение, что мы с вами смотрим в разные версии буста. Я вижу вот такой файл counter.hpp:
              # /* **************************************************************************
              #  *                                                                          *
              #  *     (C) Copyright Paul Mensonides 2005.                                  *
              #  *     Distributed under the Boost Software License, Version 1.0. (See      *
              #  *     accompanying file LICENSE_1_0.txt or copy at                         *
              #  *     http://www.boost.org/LICENSE_1_0.txt)                                *
              #  *                                                                          *
              #  ************************************************************************** */
              #
              # /* See http://www.boost.org for most recent version. */
              #
              # ifndef BOOST_PREPROCESSOR_SLOT_COUNTER_HPP
              # define BOOST_PREPROCESSOR_SLOT_COUNTER_HPP
              #
              # include <boost/preprocessor/slot/detail/def.hpp>
              #
              # /* BOOST_PP_COUNTER */
              #
              # define BOOST_PP_COUNTER 0
              #
              # /* BOOST_PP_UPDATE_COUNTER */
              #
              # define BOOST_PP_UPDATE_COUNTER() <boost/preprocessor/slot/detail/counter.hpp>
              #
              # endif
              


              Он инклудит сам себя и файл def.hpp, который явно что-то делает, но вот только что — совершенно неясно
              #  *                                                                          *
              #  *     (C) Copyright Paul Mensonides 2002.
              #  *     Distributed under the Boost Software License, Version 1.0. (See
              #  *     accompanying file LICENSE_1_0.txt or copy at
              #  *     http://www.boost.org/LICENSE_1_0.txt)
              #  *                                                                          *
              #  ************************************************************************** */
              #
              # /* See http://www.boost.org for most recent version. */
              #
              # ifndef BOOST_PREPROCESSOR_SLOT_DETAIL_DEF_HPP
              # define BOOST_PREPROCESSOR_SLOT_DETAIL_DEF_HPP
              #
              # /* BOOST_PP_SLOT_OFFSET_x */
              #
              # define BOOST_PP_SLOT_OFFSET_10(x) (x) % 1000000000UL
              # define BOOST_PP_SLOT_OFFSET_9(x) BOOST_PP_SLOT_OFFSET_10(x) % 100000000UL
              # define BOOST_PP_SLOT_OFFSET_8(x) BOOST_PP_SLOT_OFFSET_9(x) % 10000000UL
              # define BOOST_PP_SLOT_OFFSET_7(x) BOOST_PP_SLOT_OFFSET_8(x) % 1000000UL
              # define BOOST_PP_SLOT_OFFSET_6(x) BOOST_PP_SLOT_OFFSET_7(x) % 100000UL
              # define BOOST_PP_SLOT_OFFSET_5(x) BOOST_PP_SLOT_OFFSET_6(x) % 10000UL
              # define BOOST_PP_SLOT_OFFSET_4(x) BOOST_PP_SLOT_OFFSET_5(x) % 1000UL
              # define BOOST_PP_SLOT_OFFSET_3(x) BOOST_PP_SLOT_OFFSET_4(x) % 100UL
              # define BOOST_PP_SLOT_OFFSET_2(x) BOOST_PP_SLOT_OFFSET_3(x) % 10UL
              #
              # /* BOOST_PP_SLOT_CC_x */
              #
              # define BOOST_PP_SLOT_CC_2(a, b) BOOST_PP_SLOT_CC_2_D(a, b)
              # define BOOST_PP_SLOT_CC_3(a, b, c) BOOST_PP_SLOT_CC_3_D(a, b, c)
              # define BOOST_PP_SLOT_CC_4(a, b, c, d) BOOST_PP_SLOT_CC_4_D(a, b, c, d)
              # define BOOST_PP_SLOT_CC_5(a, b, c, d, e) BOOST_PP_SLOT_CC_5_D(a, b, c, d, e)
              # define BOOST_PP_SLOT_CC_6(a, b, c, d, e, f) BOOST_PP_SLOT_CC_6_D(a, b, c, d, e, f)
              # define BOOST_PP_SLOT_CC_7(a, b, c, d, e, f, g) BOOST_PP_SLOT_CC_7_D(a, b, c, d, e, f, g)
              # define BOOST_PP_SLOT_CC_8(a, b, c, d, e, f, g, h) BOOST_PP_SLOT_CC_8_D(a, b, c, d, e, f, g, h)
              # define BOOST_PP_SLOT_CC_9(a, b, c, d, e, f, g, h, i) BOOST_PP_SLOT_CC_9_D(a, b, c, d, e, f, g, h, i)
              # define BOOST_PP_SLOT_CC_10(a, b, c, d, e, f, g, h, i, j) BOOST_PP_SLOT_CC_10_D(a, b, c, d, e, f, g, h, i, j)
              #
              # define BOOST_PP_SLOT_CC_2_D(a, b) a ## b
              # define BOOST_PP_SLOT_CC_3_D(a, b, c) a ## b ## c
              # define BOOST_PP_SLOT_CC_4_D(a, b, c, d) a ## b ## c ## d
              # define BOOST_PP_SLOT_CC_5_D(a, b, c, d, e) a ## b ## c ## d ## e
              # define BOOST_PP_SLOT_CC_6_D(a, b, c, d, e, f) a ## b ## c ## d ## e ## f
              # define BOOST_PP_SLOT_CC_7_D(a, b, c, d, e, f, g) a ## b ## c ## d ## e ## f ## g
              # define BOOST_PP_SLOT_CC_8_D(a, b, c, d, e, f, g, h) a ## b ## c ## d ## e ## f ## g ## h
              # define BOOST_PP_SLOT_CC_9_D(a, b, c, d, e, f, g, h, i) a ## b ## c ## d ## e ## f ## g ## h ## i
              # define BOOST_PP_SLOT_CC_10_D(a, b, c, d, e, f, g, h, i, j) a ## b ## c ## d ## e ## f ## g ## h ## i ## j
              #
              # endif
              
                0
                Нет, он не инклудит сам себя, Вы смотрите файлы в папке \boost\preprocessor\slot.
                И файл counter.hpp (который у вас в комментарии) инклудит файл detail\counter.hpp, а вот BOOST_PP_SLOT_CC_*, который в def.hpp, как раз «склеивает» разряды числа.
                Файлы (и расположение) одинаковы в 2х версиях буста, которые у меня есть (1.55 и 1.56).
                  0
                  А, действительно. Вы правы, я не заметил, а среда подавилась и второй файл не показала в дереве инклудов.
              +1
              Вообще когда совсем одолевает лень при написании тестов:

              * пишем тесты с выводом в stdout, вроде такого:

              puts("Testing feature 1...");
              printf("%d\n", result);
              

              * запускаем наше хозяйство с выводом в простой текстовый файл, и сохраняем его куда-нибудь в укромное место как «эталон»
              * повторный запуск — сравниваем результат с эталонным файлом чем-то вроде diff
              * обновляем тесты — обновляем эталон

              А когда не лень делают так: github.com/danmar/cppcheck/blob/master/test/testtokenize.cpp
                0
                В embedded с выводом в файл тяжко.
                0
                Посмотрите, как сделано в Cutter. Там вообще отсутствует регистрация тестов, так как эта проблема перекладывается на тест-раннер. Он тупо выдирает все символы, экспортированные из динамической библиотеки, которые начинаются на «test_».

                Для embedded в неизменном виде такой вариант, конечно, не походит. И альтернатива, мне кажется, только во встраивании промежуточного кодогенератора в билд-систему. Но это хороший пример удобства для пользователя ценой сложности для разработчика. Особенно применительно к тестинг-фреймворкам, которые в идеале должны «просто работать» из коробки.
                  0
                  Сторонний раннер тоже вариант, да.
                  А почему мой вариант не подходит для embedded? Что нужно изменить?

                  Я планирую его запускать в отладчике, поэтому printf'ы вообще можно убрать, на самом деле. Просто, если тест не проходит — while(1){;}. И отладчик останавливается как раз на проваленном тесте, это даже удобнее, чем искать по информации из лога.
                    0
                    А почему мой вариант не подходит для embedded?
                    Это было про Cutter, динамические либы и всё такое, а не про идею из поста.
                      0
                      Пардон, туплю.

                Only users with full accounts can post comments. Log in, please.