Halbautomatische Registrierung von Unit-Tests in reinem C

    Nachdem ich das Buch Test Driven Development für Embedded C gelesen hatte , begann ich meine Einführung in die Welt des Unit-Testens mit dem cppUtest-Framework. Nicht zuletzt, weil darin ein frisch geschriebener Test registriert und selbständig gestartet wird. Sie müssen dafür bezahlen - mit C ++, einer dynamischen Speicherzuweisung irgendwo in den Tiefen des Frameworks. Vielleicht kann es irgendwie einfacher sein?
    Zuletzt habe ich etwas über das minimalistische minUnit- Framework gelernt , das in nur 4 Zeilen passt.

    Ich werde sie hier zur Klarheit geben:

    #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 и прописывать вызов!

    Посмотрел я на другие фреймворки, написанные на чистом С: почти везде тоже самое. В качестве альтернативы предлагаются отдельные программы, сканирующие исходники с тестами и генерирующими код для запуска.
    Но может быть, можно проще?

    Уверенность в меня вселил вот этот пост, где для регистрации тестов используется линкер. Но мне привязываться к линкеру и специфичным для компилятора атрибутам не хотелось.
    Soweit ich weiß, ist es in reinem C unmöglich, eine vollständige Testregistrierung durchzuführen. Was ist mit halbautomatischen?

    Die Idee nahm wie folgt Gestalt an. Für jedes Modul wird die Datei module_tests.c geschrieben, in die alle Tests für dieses Modul geschrieben werden. Diese Tests bilden eine Gruppe. In derselben Datei ist eine magische Funktion geschrieben, mit der alle Tests in einer Gruppe ausgeführt werden.
    Und im Wesentlichen müssen Sie nur den Start der Gruppe und nicht jeden Test einzeln mit Ihren Händen registrieren.
    Dies läuft auf die folgende Aufgabe hinaus: Sie müssen irgendwie eine Liste aller Funktionen in der Datei abrufen. In C ist dies nur mit einem Präprozessor möglich. Aber wie? Zum Beispiel, wenn Funktionen irgendwie eintönig aufgerufen werden.

    Die "Service" -Namen der Tests können durchaus beliebig sein, wenn nur der Titel des Tests verständlich war!
    Dies bedeutet, dass mit Hilfe eines Präprozessors Namen für Testfunktionen einheitlich und nach einer einzigen Vorlage generiert werden müssen. Zum Beispiel so:

    #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) )
    

    Ehrlich gesagt habe ich Boost zum ersten Mal in meinem Leben verwendet und war von der Leistung des C-Präprozessors bis ins Mark erstaunt!
    Jetzt können Sie Tests wie folgt schreiben:

    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()
    

    Nach dieser Einbeziehung wird der Zähler inkrementiert und der Name für den nächsten Test wird generiert. Name static char * umba_test_1_ (void).

    Es bleibt nur eine Funktion zu generieren, die alle Tests in der Datei ausführt. Dazu wird ein Array von Zeigern auf Funktionen erstellt und mit Zeigern auf Tests gefüllt. Dann ruft die Funktion einfach jeden Test aus dem Array in einer Schleife auf.
    Diese Funktion muss am Ende der Testdatei geschrieben werden, damit der Wert von UMBA_TEST_COUNTER der Nummer des letzten Tests entspricht.
    Um ein Array von Zeigern zu generieren, bin ich zunächst einem einfachen Pfad gefolgt und habe eine Hilfedatei wie die folgende geschrieben:

    #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_};
    …
    

    Grundsätzlich ist es durchaus möglich, mit der Erstellung von Anzeigen für mehrere hundert Tests auszukommen. Dann wird nur eine Datei von boost'a - boost / preprocessor / slot / counter.hpp benötigt.
    Aber seit ich Boost benutze, warum nicht weitermachen?

    #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_ ) }
    

    Nur zwei Zeilen, aber welche Kraft steckt dahinter!
    Fügen Sie den Trivialcode für die Gruppenstartfunktion selbst hinzu:

    #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; \
                                                           } \
    													   

    Und um es von Haupt zu starten:

    #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)
    

    Voila. Das Starten einer Gruppe mit einer beliebigen Anzahl von Tests sieht jetzt genauso aus:

    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;
    }
    


    Ich bin sehr zufrieden mit diesem Ergebnis. Mechanische Einwirkungen beim Schreiben eines Tests sind jetzt spürbar geringer.
    Alle genannten Makros passen in 40-50 Zeilen, was leider etwas größer ist als minUnit (und viel weniger offensichtlich).
    Der gesamte Code.

    Ja, große Frameworks bieten nicht viele Funktionen, aber ich hatte ehrlich gesagt nie Zeit, etwas anderes als einen einfachen Check wie CHECK (falls zutreffend) für den Test zu verwenden.
    Die Beschreibung für den Test wird einfach weggeworfen, aber es scheint einfach zu sein, etwas Nützliches damit zu tun, wenn Sie es plötzlich wollen.

    Was ich herausfinden möchte:
    1. Habe ich etwas Neues erfunden oder verwende ich diesen Trick seit vielen Jahren?
    2. Kann das irgendwie verbessert werden? Ich mag es nicht wirklich, nach jedem Test ein seltsames Include zu schreiben, aber ich habe keine anderen Zählerimplementierungen auf dem Präprozessor gefunden (__COUNT__ unterstützt meinen Compiler nicht).
    3. Sollte ich in der Produktion ein provisorisches Framework verwenden?
    4. Wie zum Teufel funktioniert BOOST_PP_COUNTER?! Auch beim Stackoverflow lautet die Antwort auf die entsprechende Frage „Magie“.

    Jetzt auch beliebt: