[Modultest] Mit Hilfe von assert() Programmfehler finden. Wie geht man richtig vor?



  • Hi,

    google hat mir den Weg zu diesem Forum gezeigt. Und mein erster Eindurck ist sehr positiv. Vor allem "Links für Neulinge" gefällt mir sehr. Ich habe erst seit etwa einer Woche mit C++ angefangen. Komme auch gut voran. Meine erste große Aufgabe soll ein Modultest sein. Bevor es aber soweit ist, will ich selbst ein kleines Programm mit Klassen, Templates, Funktionen,... eben alles was ich bis jetzt gelernt habe, umsetzen. Und dann möchte ich mit assert() selbst ein Modultest durchführen.
    Es hat ein bissel gedauert, aber jetzt meiner Frage. Wie gehe ich da am besten an die Sache ran?
    Als Erstes schreibe ich wieder eine neue Klasse(n), welche fast die gleichen Namen wie die original Klasee/Modul haben. Also MyClassTest, zum Beispiel.
    Zu Anfang sollte man die Konstruktoren testen, aber wie genau mache ich das. Hat da jemand ein Beispiel? Kleine Dinge zu testen wie z.B. assert(a!=0) ist nicht wirklich eine Herrausforderung. Das habe ich sofort verstanden, aber wie funktioniert dies mit einem Konstruktor.
    Dann natürlich die Ausgabe. Die muss auch getestet werden. Wie sieht das bei der Ausgabe eines Arrays oder Liste aus. Muss ich mir da etwa 30-40 Elemente ausgeben lassen und diese einzeln nach dem Muster assert(a[0]!=0), (a[1]!=0),
    (a[1]!=0), (a[2]!=0),... durchführen?
    Es wäre sehr nett, wenn sich jemand etwas Zeit nehmen könnte und mir etwasHilfestellung gibt und vielleicht eins oder zwei kleine Beispiele zeigen würde (z.B. Test der Konstruktoren).

    mfg Xino

    [Edit]Die Boardsuche habe ich schon benutzt. Leider nichts gefunden.



  • Man testet Dinge, die man über eine Klasse oder Funktion "weiß". Ein Test (ich nehme an, du meinst Unit-Tests?) sagt als immer etwas wie:

    "Ich weiß, dass Funktion x() meine Klasse so und so ändert. Also rufe ich x() mal auf und sehe nach, ob das stimmt."

    Nehmen wir als Beispiel an, du möchtest die Klasse std::string aus der STL testen. Also sagst du: Ein "Default-Konstruktor erstellt einen leeren String". Und testest vielleicht so:

    void testDefaultCtor() {
    
       std::string str;    // Default-Konstruktor. str muss leer sein.
       if(!str.empty()) {
          // Test fehlgeschlagen
       }
       if(str.size() != 0) {
          // Test fehlgeschlagen
       }
    }
    

    Nun hat std::string ja auch noch andere Konstruktoren, zum Beispiel den, der einen C-String kopiert. Du weißt, dass ein Konstruktor, der einen C-String erhält, diesen kopieren soll. Also testest du:

    void testCStringCtor() {
    
       std::string str1("Test");    // str muss jetzt "Test" enthalten
       if(str1 != "Test") {
          // Test fehlgeschlagen
       }
       std::string str2("Teste mich mal", 4);   // str muss jetzt "Test" enthalten
       if(str2 != "Test") {
          // Test fehlgeschlagen
       }
    }
    

    Ein drittes Beispiel. Die Funktion std::string::erase() löscht Zeichen aus einem String. Du weißt, dass ab der angegebenen Position soundsoviele Zeichen gelöscht werden sollen. Also testest du:

    void testErase() {
    
       std::string str("Dies ist ein Test");
       str.erase(0, 5);         // Die ersten fünf Zeichen löschen
       if(str != "ist ein Test") {
          // Test fehlgeschlagen
       }
       str.erase(4, 4)     // ab Index vier vier Zeichen löschen
       if(str != "ist Test") {
          // Test fehlgeschlagen
       }
    }
    

    So testest du deine ganze Schnittstelle durch. Du solltest auch testen, ob Funktionen (etwa bei falschen Parametern) die Exceptions werfen, die sie werfen sollen. Aber das ist vielleicht ein bischen zuviel für den Anfang.

    Beim Test darfst du ruhig Dinge verwenden, die du über die Implementierung der Klasse weißt. Falls du etwa weißt, dass funktion a() über Funktion b() implementiert ist, kannst du nur b() testen.

    Ich persönlich ziehe es vor, nicht assert() für die Prüfungen zu verwenden. Das hat nämlich zwei Nachteile:

    - Erstens ist assert() im Release-Modus deines Programms gar nicht aktiv. Du kannst also nur die Debug-Version testen.

    - Zweitens beendet assert() dein Programm, ohne dass du noch eingreifen könntest, also z.B. den Fehler ignorieren oder irgend was in ein Logfile schreiben. Das finde ich nicht erstrebenswert.

    Also verwende ich lieber Exceptions und schreibe mir eine eigene Klasse, die nur für fehlgeschlagene Tests verwendet wird. Die Exceptions kann ich dann irgendwo fangen und noch etwas sinnvolles damit anfangen.

    Für die eigentlichen Tests verwende ich Makros. Das macht den Code übersichtlicher und verhindert, dass ich mal was vergesse. Ein TEST-Makro könnte so aussehen:

    #define TEST(_expression_)                  \
      do {                                      \
        if(!(_expression_)) {                   \
          TestFailed err(#_expression_);        \
          err.setLocation(__FILE__, __LINE__);  \
          throw err;                            \
        }                                       \
      } while(false)
    

    Hier ist die Klasse TestFailed mal vorausgesetzt, die man mit dem Text des fehlgeschlagenen Ausdrucks erstellt und der man den Ort mitgeben kann, an dem der Test fehlgeschlagen ist ( setLocation() ). Mit diesem Makro kann ich einen der obigen Tests umschreiben:

    void testCStringCtor() {
    
       std::string str1("Test");    // str muss jetzt "Test" enthalten
       TEST(str1 == "Test");
       std::string str2("Teste mich mal", 4);   // str muss jetzt "Test" enthalten
       TEST(str2 == "Test");
    }
    

    Das sieht doch viel übersichtlicher aus!

    Stefan.



  • Hi DStefan,

    vielen herzlichen Dank für deine ausführliche Antwort. Habe nur kurz mitbekommen, dass man es mit assert() macht. Ich werde beide Arten testen. Denke es kann nicht schaden. Vor allem das mit dem Makro finde ich sehr interessant. Werde ich auf jeden Fall testen, denn es ist deutlich übersichtlicher. Falls jemand noch weiter Vorschläger oder Tips, dann her damit 🙂



  • assert setzt man normalerweise für Logikfehler (falsche Programmierung bzw. Anwendung von Klassen/Funktionen ein). Also wie DStefan sagte, kannst du damit Pre- und Postconditions sicherstellen (Bedingungen, die vor und nach einem Algorithmus erfüllt sein müssen). Das dient primär Debugging-Zwecken, damit sich Fehler lokal zeigen und nicht erst viel später, wo kein Zusammenhang mehr ersichtlich ist. Im fertigen Release-Build expandiert das Makro assert nicht und du hast keinen Overhead mehr.

    Exceptions dagegen stellen Ausnahmen dar. Man versucht etwas und geht davon aus, dass das entweder klappt oder eine Exception geworfen wird. Zum Beispiel gibt es beim STL-Container std::vector (ähnlich wie ein Array) eine Methode at() , die eine Exception wirft, wenn ein ungültiger Index angegeben wurde. Im Gegensatz zu Assertions sind Exceptions nicht primär da, Programmierfehler aufzudecken, sondern um Laufzeitfehler zu behandeln, die trotz richtiger Programmierung auftreten können. Ein Beispiel wäre auch der Versuch, eine nicht vorhandene Datei zu öffnen (obwohl das in C++ erst mal nichts wirft).



  • Kleiner Nachtrag zu Nexus:

    Assertions dienen auch der Dokumentation. Wenn ich im Code ein assert(irgendwas) lese, kann ich sofort sehen, von welchen Annahmen/Bedingungen der Autor ausgegangen ist. Diese Funktion von assert() finde ich ziemlich wichtig - oder doch zumindest erwähnenswert.

    Stefan.



  • DStefan schrieb:

    Kleiner Nachtrag zu Nexus:

    Assertions dienen auch der Dokumentation. Wenn ich im Code ein assert(irgendwas) lese, kann ich sofort sehen, von welchen Annahmen/Bedingungen der Autor ausgegangen ist. Diese Funktion von assert() finde ich ziemlich wichtig - oder doch zumindest erwähnenswert.

    Stefan.

    Diese "Annahmen" heissen z.B. Vorbedingungen. Diese sollten als Kommentar ueber der Funktion stehen (gilt auch fuer Nachbedingungen und returns). assert ist nicht dazu gedacht, unit tests oder Quelltextdokumentation zu ersetzen.

    Ein Beispiel wäre auch der Versuch, eine nicht vorhandene Datei zu öffnen (obwohl das in C++ erst mal nichts wirft).

    Das ist ein sehr schlechtes Beispiel, der Weg in C++ geht in diesem Fall nicht ueber Exceptions. Das ist meine Meinung.



  • knivil schrieb:

    Ein Beispiel wäre auch der Versuch, eine nicht vorhandene Datei zu öffnen (obwohl das in C++ erst mal nichts wirft).

    Das ist ein sehr schlechtes Beispiel, der Weg in C++ geht in diesem Fall nicht ueber Exceptions. Das ist meine Meinung.

    Das würde aber ganz gut passen, weil man Dateien im Konstruktor des Dateiobjektes öffnet.



  • DStefan schrieb:

    Assertions dienen auch der Dokumentation. Wenn ich im Code ein assert(irgendwas) lese, kann ich sofort sehen, von welchen Annahmen/Bedingungen der Autor ausgegangen ist. Diese Funktion von assert() finde ich ziemlich wichtig - oder doch zumindest erwähnenswert.

    Naja - hier tendiere ich zu knivils Aussage. Du kennst doch lange nicht immer den Code der Funktionen, die du aufrufst. Selbst wenn er dir zugänglich ist, gehst du ihn nicht immer nachlesen. Oder?

    Dass assert als Dokumentation verstanden werden kann, bedingt aber genau das.

    knivil schrieb:

    Das ist ein sehr schlechtes Beispiel, der Weg in C++ geht in diesem Fall nicht ueber Exceptions. Das ist meine Meinung.

    Ja, deshalb habe ich das auch noch in den Klammern angemerkt. Aber von mir aus gesehen ist ein Fehler beim Laden von Ressourcen ein guter Einsatzbereich für Exceptions. std::fstream ist auch nicht gerade das Mass aller Dinge...



  • @knivil und Nexus:

    Da habe ich mich ungenau ausgedrückt (schon wieder 😉 ) Ich wollte keineswegs assert() anstelle von Dokumentation verwenden, sondern Annahmen im Code dokumentieren. Eine gute Anwendung sind Bestandteile der Klasseninvariante. Und natürlich liest man diese nicht in der Dokumentation zu einer Methode, sondern wenn man diese Methode überarbeitet.

    Nehmen wir als Beispiel eine Klasse File in RAII-Manier, also Datei im Ctor öffnen (und Exception, falls das nicht gelingt, wie volkard vorgeschlagen hat) und erst im Dtor wieder schließen. Eine Klasseninvariante wäre, dass die Datei in jeder Methode geöffnet ist. Also:

    void File::writeData(const void *data, size_t count) {
    
       assert(is_open());    // Sichert die Klasseninvariante zu und dokumentiert sie.
    
       // usw.
    
    }
    

    Die Dokumentation zur Methode ist klar. Sie enthielte aber nicht, wie ich finde, die Klasseninvariante, nämlich, dass die Datei geöffnet ist.

    Stefan.



  • Die Dokumentation zur Methode ist klar. Sie enthielte aber nicht, wie ich finde, die Klasseninvariante, nämlich, dass die Datei geöffnet ist.

    Naja, wenn es eine Invariante ist, dass die Datei geoeffnet ist, so kann es kein Dateiobjekt geben, was eine ungeoeffnete Datei repraesentiert. Dort waere das assert nur zum Debuggen gut. Wenn es keine Invariante der Klasse ist, schreibe ich es dazu, unter welchen Umstaenden die Methode aufgerufen werden darf und welche Exceptions geworfen werden koennen



  • knivil schrieb:

    Die Dokumentation zur Methode ist klar. Sie enthielte aber nicht, wie ich finde, die Klasseninvariante, nämlich, dass die Datei geöffnet ist.

    Naja, wenn es eine Invariante ist, dass die Datei geoeffnet ist, so kann es kein Dateiobjekt geben, was eine ungeoeffnete Datei repraesentiert. Dort waere das assert nur zum Debuggen gut.

    Und genau das sagt die Assertion. Sie verhindert, dass (meist ja beim Überarbeiten der Klasse) die Klasseninvariante versehentlich verletzt wird. Sie zeigt aber auch unmittelbar im Code an, dass ich dieser Invariante vertraue. Das wäre dann der Dokumentationsaspekt von assert() . Und natürlich ist das "nur" für's Debuggen gut. In der Release-Version ist assert() doch gar nicht eingeschaltet.

    Ich nehme also "assert" wörtlich. Für irgendwelche Fehler (etwa falsche Parameter) setze ich es nie ein. Dazu sollte man Exceptions verwenden.

    Stefan.



  • DStefan schrieb:

    Ich nehme also "assert" wörtlich. Für irgendwelche Fehler (etwa falsche Parameter) setze ich es nie ein. Dazu sollte man Exceptions verwenden.

    Ja, und jedesmal wieder erkläre ich Dir, das das totaler Unfug ist.



  • volkard schrieb:

    Ja, und jedesmal wieder erkläre ich Dir, das das totaler Unfug ist.

    👍

    Also ich setze assert intern fuer debugging falscher Parameter ein. Erwarte ich z.B. ein konvexes Polygon, so steht in meiner Funktion assert ( konvex_poly( poly ) ). Wenn aber jemand anderes sich nicht an diese Vorbedingung haelt und mit der realease-Version der Bibliothek arbeitet ... selbst Schuld. Warum soll ich da 'ne Exception werfen?


Anmelden zum Antworten