Unit Testen einer Funktion, die im Fehlerfall nicht stoppt



  • Hallo zusammen,

    ich habe eine Funktion bei der ich mir nicht wirklich sicher bin wie ich sie testen soll bzw. ob ich sie richtig teste. Sehr vereinfacht sieht das ganze so aus:

    void getInput() {
       do {
          std::cout << "Gebe einen validen input ein"; 
          std::string input {};
          std::cin >> input;
       } while (input != "valid");
    }
    

    Hier möchte ich also gerne testen, dass bei einem nicht validen Input, der User nochmal gefragt wird.
    Um den Code zu testen, schreibe ich ihn so um, dass ich den User Input kontrollieren kann. Ich sorge also dafür, dass der Input invalide ist.
    Jetzt ist aber das Problem: Dann befinde ich mich ja in einer Endlosschleife, da muss ich also irgendwie auch wieder raus.

    Mein Lösungsansatz sieht also grob so aus:

    • Schreibe Code so um, dass ich statt std::cin einen istringstream nutzen kann und statt std::cout einen ostringstream
    • Konfiguriere den instringstream, sodass er beim ersten auslesen einen invaliden, beim zweiten einen validen Input liefert
    • Überprüfe, ob der "Gebe einen validen Input ein" 2x im ostringstream gelandet ist

    Es funktioniert soweit, aber der Ansatz erscheint mir irgendwie etwas komisch, so fragil. Erst dafür sorgen, dass er intern failed und dann alles wieder klappt, um der Endlosschleife zu entkommen.
    Würdet ihr das auch so machen? Gibt es Alternativen?

    Danke für euren Input! 🙂



  • Man könnte die Eingabe und Ausgabe in eine Hilfsklasse mit virtuellen Funktionen auslagern.

    class ConsoleIO {
    public:
        virtual ~ConsoleIO();
        virtual std::string readLine();
        virtual void writeLine(std::string_view line);
    };
    
    void getInput(ConsoleIO& io) {
       do {
          io.writeLine("Gebe einen validen input ein");
          std::string input = io.readLine();
       } while (input != "valid");
    }
    

    Dann kannst du einen Test-Mock für ConsoleIO machen. Darin kannst du dann nicht nur kontrollieren was "gelesen" werden soll, sondern z.B. auch falls die readLine Funktion nach dem gültigen Input noch einmal aufgerufen wird einen Fehler ausgeben und den Prozess beenden. Und so verhindern dass der Unit-Test Prozess hängen bleibt/ewig läuft.

    Ob sich das auszahlt ist die Frage. Eine Alternative wäre z.B. nur einen higher level Test zu machen der das testet. z.B. indem man den Prozess von aussen startet und entsprechenden Input reinfüttert. Dabei braucht man dann halt ein Tool das Timeouts erkennen kann und den Prozess abbrechen falls er hängen bleibt.



  • Danke für deine Antwort!

    Ja ich denke mit einem Mock könnte ich das in der Tat hübscher und robuster machen. Im Grunde kann ich dazu ja auch das Interface direkt von iostream nehmen, oder nicht?

    Jetzt will ich überprüfen, dass er in eine Endlossschleife geht, wenn ein fehlerhafter Input kommt:

    1. Mock setzen, sodass er fehlerhaften Input liefert
    2. Mock exspectations setzen, der input / output muss minimum 2x passieren. Wenn er also nicht in eine Endlosschleife geht, failed der Test auf jeden Fall
    3. Wie komme ich, vorrausgesetzt der Fehlerfall klappt so wie geplant, wieder aus der Endlosschleife raus? Ich sehe hier als beste und irgendwie auch einzige Möglichkeit dem Mock zu sagen: Beim 2. mal oder 3. mal Input anfordern, gebe einen validen Input zurück. Oder gibt es noch andere Möglichkeiten?


  • Gegebenfalls könnte ich mit:
    https://github.com/rollbear/trompeloeil/blob/master/docs/CookBook.md#throw

    beim 2. Aufruf eine spezielle Exception thrown und dann in meinem sagen: Die muss geworfen werden. Solle mit https://github.com/rollbear/trompeloeil/blob/master/docs/CookBook.md#sequences auch machbar sein.

    Das hört sich sogar eig. ganz elegant für mich an. Damit verwandle ich die Endloss Schleife quasi in eine Exception. Das klingt auch gar nicht so unlogisch: Wenn der Input fehlschlägt, wird ne Exception geschmissen 😃

    Auf der anderen Seite prüft die andere Methode vermutlich besser was passieren soll. Wäre ja z.B. möglich, dass zwar in die Endlosschleife gegangen wird, aber ein korrekter Input nach dem ersten Fehlversuch nicht mehr akzeptiert wird. Das wird natürlich in meinem ersten Vorschlag direkt mitgeprüft.



  • @Leon0402 sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Im Grunde kann ich dazu ja auch das Interface direkt von iostream nehmen, oder nicht?

    Was meinst du mit "das Interface direkt von iostream nehmen"? Meinst du einen std::iostream übergeben und dann einen eigenen Stream/Stream-Buffer implementieren für den Mock? Das würde ich nicht machen wollen.

    Oder meinst du das Interface von std::iostream nachbilden? Das würde ich noch weniger machen wollen 🙂 Ein Problem dabei ist dann nämlich dass die ganzen Stream-Insertion Operatoren nicht mehr funktionieren. Also wenn Klasse Foo einen std::ostream& operator <<(std::ostream&, Foo const&) hat, dann geht der mit std::ostream aber nicht mit deinem Nachahmungs-Ding.

    Nach meiner Erfahrung ist es gut Interfaces zwischen Klassen so einfach wie möglich zu halten. Ein Swiss Army Knife wie std::iostream ist da nicht die richtige Wahl. Egal ob man die originale std::iostream Klasse nimmt oder es nachbaut.



  • Naja ich dachte daran einfach eine Mocking Library zu nehmen (oben verlinkte). Also im wesentlichen eine Klasse MyIOStreamMock, welche von std::ostream / std::istream erbt. Ich denke die Verwirrung bestand vlt. darin grade, dass ich std::iostream gesagt habe? Ich meine natürlich konkret die Interfaces ostream und istream (für std::cout und std::cin)

    class Mock : public Interface
    {
    public:
      MAKE_MOCK2(foo, bool(int, std::string&),override);
      MAKE_MOCK1(bar, bool(int),override);
      MAKE_MOCK1(bar, bool(std::string),override);
      MAKE_MOCK0(baz, void()); // not from Interface
    };
    

    Also entsprechend hier halt von ostream / istream erben, die benötigten Funktionen angeben (Da C++ keine Reflection hat) und die Implementierung übernimmt dann ja die Mocking Lib.

    Dann kann ich ja die Funktion einfach mit meinem Mock aufrufen und alles sollte doch soweit erstmal funktionieren. Letzendes nutze ich ja in meiner aktuellen Lib auch bereits eine Art Mock, den ich nicht selbst geschrieben habe ... std::stringsteam. Nur das man halt mit dem richtigen Mock entsprechend mehr Möglichkeiten hätte.

    Dein Ansatz mit einer eigenen Klasse, um ein klareres Interface zu haben, finde ich an sich auch gut. Aber irgendwie mag ich den Gedanken nicht (übermäßig) Dinge an meinem Code zu ändern nur damit ich ihn gut testen kann. So eine Klasse würde man ja normalerweise ja nicht einfach so einbauen, oder doch? Das sieht dann immer so nach overengineeren aus 😃

    @hustbaer sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Ein Problem dabei ist dann nämlich dass die ganzen Stream-Insertion Operatoren nicht mehr funktionieren. Also wenn Klasse Foo einen std::ostream& operator <<(std::ostream&, Foo const&) hat, dann geht der mit std::ostream aber nicht mit deinem Nachahmungs-Ding.

    Ich vermute, dass du vermutest hast, dass ich was anderes vorhabe als ich tatsächlich vorhabe? Wenn das Problem auch mit meinem Ansatz besteht, müsstest du mir das nochmal erklären. Da mein MockOStream ja vin std::ostream erbt, sollte das doch kein Thema sein?
    Also meine Funktion, die ich testen will würde dann auch so aussehen:

    void getInput(std::ostream ostream, std::istream istream) {
       do {
          std::ostream<< "Gebe einen validen input ein"; 
          std::string input {};
          std::istream>> input;
       } while (input != "valid");
    }
    

    Ich würde also 2 Mocks bauen, einmal für ostream und einmal für istream halt 🙂



  • @Leon0402 sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Ich denke die Verwirrung bestand vlt. darin grade, dass ich std::iostream gesagt habe? Ich meine natürlich konkret die Interfaces ostream und istream (für std::cout und std::cin)

    Nein. Die Verwirrung besteht vermutlich darin dass dir nicht klar ist dass std::istream und std::ostream keine Interfaces sind. Das sind ziemlich konkrete Dinger.
    Wenn du da irgendwas mocken willst, dann musst du vermutlich eigene Stream-Buffer implementieren.

    IMO viel zu fummelig, ich würde das nicht machen wollen.

    Wenn das Problem auch mit meinem Ansatz besteht, müsstest du mir das nochmal erklären. Da mein MockOStream ja vin std::ostream erbt, sollte das doch kein Thema sein?

    Hast du "deinen Ansatz" denn schon ausprobiert? Ich wüsste nämlich nicht wie der funktionieren soll.

    Ich würde also 2 Mocks bauen, einmal für ostream und einmal für istream halt

    Probier einfach es zu machen, dann merkst du vermutlich was ich meine.



  • @hustbaer sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Nein. Die Verwirrung besteht vermutlich darin dass dir nicht klar ist dass std::istream und std::ostream keine Interfaces sind.

    Hmm in der Tat. Ja aus irgendeinem Grund dachte ich tatsächlich, dass das Interfaces sind. cout ist ja direkt ein ostream.
    Liegt das daran, dass die entsprechenden Methoden nicht virtual sind? Es gibt ja auch den std::ostringstream, der ja von ostream erbt (oder nicht?). Wie macht der das denn, wenn die Methoden nicht virtual sind?

    Oder bin ich immer noch auf dem Holzweg 😃 Werde es ansonsten auch mal ausprobieren am Wochende, wenn ich etwas Zeit habe.



  • @Leon0402 Das was du die ganze Zeit unter dem Schlagwort Interface verbuchst ist prinzipiell einfach eine Base-Class. ostream und istream haben sogar recht viele virtuelle Funktionen und sind sogar Teil einer virtual inheritance. https://en.cppreference.com/w/cpp/io/basic_ostream
    Im Prinzip meinst du schon das Richtige, nennst es nur falsch 😉

    EDIT: Scheinbar muss ich mich revidieren. Ich finde auf Anhieb nur virtual DTors. Daher frag bloß nicht mich, sobald irgendwo nen virtual inheritance im Spiel ist, ist mir das viel zu suspekt 😃



  • @Leon0402 sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Hmm in der Tat. Ja aus irgendeinem Grund dachte ich tatsächlich, dass das Interfaces sind. cout ist ja direkt ein ostream.
    Liegt das daran, dass die entsprechenden Methoden nicht virtual sind? Es gibt ja auch den std::ostringstream, der ja von ostream erbt (oder nicht?). Wie macht der das denn, wenn die Methoden nicht virtual sind?

    Naja die iostreams unterstützen schon Polymorphie. Aber halt nicht indem man einfach ein paar virtuelle Methoden direkt in den Stream-Klassen überschreibt. Man muss dazu wie schon gesagt einen eigenen Stream-Buffer (std::streambuf) implementieren und dann einen Stream mit diesem Stream-Buffer erzeugen.
    Beispiel wie das geht siehst du z.B. hier: https://stackoverflow.com/questions/14086417/how-to-write-custom-input-stream-in-c

    Das ist jetzt grundsätzlich erstmal nicht so schwer man nur Daten aus einer Stream-artigen Quelle lesen bzw. in ein Stream-artiges Ziel schreiben will. Aber in den std::streambuf Funktionen jetzt Test-Assertions einzubauen stelle ich mir fummelig vor.



  • @DNKpp Ja unter einem Interface verstehe ich per se erstmal nur abstrakte Klassen bzw. pure virtual Methoden. Weiter gefasst tut es hier auch eine Basisklasse mit virtuellen Funktionen, soweit ich das verstanden habe.
    Zu deinem Ergebnis bin ich auch gekommen. Keine virtuellen methoden außer dtor, aber virtuelle Vererbung. Soweit ich virtuelle Vererbung verstanden ist das aber nur für Multi Vererbung. Wenn eine Klasse A von einer Klasse B quasi 2x erbt durch unterschiedliche Vererbungspfade. Dann aber den ganzen Kram von B nur einmal enthält und eben nicht mehrfach.

    @hustbaer sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Naja die iostreams unterstützen schon Polymorphie. Aber halt nicht indem man einfach ein paar virtuelle Methoden direkt in den Stream-Klassen überschreibt. Man muss dazu wie schon gesagt einen eigenen Stream-Buffer (std::streambuf) implementieren und dann einen Stream mit diesem Stream-Buffer erzeugen.
    Beispiel wie das geht siehst du z.B. hier: https://stackoverflow.com/questions/14086417/how-to-write-custom-input-stream-in-c

    Hieraus lese ich zumindest, dass es sinnvoll ist lieber den streambuff zu überschreiben und diesen dann mit einem "normalen" istream / ostream zu nutzen. Das taugt für meinen Anwendungsfall eher nicht so. Dann müsste ich, wie du ja sagst, den Mock selbst schreiben mit entsprechenden Assertionen Funktionen und das erscheint mir auch fummelig.

    Die erste Frage, die sich mir aber direkt stellt ist: Wie macht denn ostringstream /istringstream das? Da wurde ja nicht (nur?) der Buffer ausgetauscht, sondern tatsächlich richtig eine Subklasse von istream/ostream erstellt. Mit dem klappt auch alles wunderbar. Daraus schließe ich ja, dass man schon doch irgendwie richtig subclassen kann.

    Ich habe schon grob mitbekommen, dass das nicht unbedingt einfach geht. Nicht zuletzt weil da ja auch ganz viel template im Spiel ist und das ja nur typedefs sind. Aber mich würde schon grob interessieren wie das überhaupt gehen kann.
    Ich liege doch hier richtig, dass keine Methode in ostream virtual ist? Wie kann also doch Polymorphie funktionieren?

    Ich vermute mal, wenn du sagst, dass ist ne bescheuerte Idee, dann wirst du damit recht haben. Du kennst dich ja schon ganz gut aus 😃 Aber würde gerne noch etwas mehr verstehen, warum es ne bescheuerte Idee ist ostream zu subclassen.

    Vlt. möchte ich das Szenario auch mal vereinfachen. Ich möchte nicht mehr, dass irgendwelche Assertions gemacht werden oder in irgendeine Art von Buffer geschrieben wird, sondern das einfach gar nichts passiert. Rein gar nichts 😃 Also sprich alle Methoden (oder zumindest die relevante Operatoren etc.) sollen komplett leer sein.
    Warum ist das schwierig, was ist da das Problem? (Meine Vermutung ist ja: Weil nicht virtual ... siehe Frage oben)

    Wenn ich eine ultra komplexe Klasse A habe und die eine Methode "foo" hat, die virtual ist. Dann kann ich doch eig. super easy überschreiben, wenn mein eigentliches Ziel nur ist, dass die Funktion gar nichts macht. Die einzigen Probleme, die mir da evtl. einfallen sind:

    • Überladungen
    • Templates

    Damit wird es vlt. etwas kniffliger, aber klingt erstmal noch nach nem lösbaren Problem.



  • @Leon0402 Ich glaube, du solltest dich mit Polymorphie in C++ nochmal auseinander setzen. Wenn du von einer Klasse "public" ableitest, hast du Zugriff auf alle public und protected Methoden der Parent Klasse.
    Wenn es keine virtuellen Funktionen gib, ist halt kein "Interface" vorgesehen um das Verhalten der Parent Klasse zu verändern.
    Den virtuellen Dtor brauchst du dafür, dass die Parent Klasse sauber abgeräumt wird, wenn die abgeleitete Klasse zerstört wird.



  • @Schlangenmensch sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    @Leon0402 Ich glaube, du solltest dich mit Polymorphie in C++ nochmal auseinander setzen. Wenn du von einer Klasse "public" ableitest, hast du Zugriff auf alle public und protected Methoden der Parent Klasse.
    Wenn es keine virtuellen Funktionen gib, ist halt kein "Interface" vorgesehen um das Verhalten der Parent Klasse zu verändern.
    Den virtuellen Dtor brauchst du dafür, dass die Parent Klasse sauber abgeräumt wird, wenn die abgeleitete Klasse zerstört wird.

    Ich sehe jetzt nicht direkt wie das im Widerspruch zu meinen Aussagen steht? Wenn die Klasse bei ihren Methoden kein "virtual" verwendet , funktioniert Polymorphie nicht. Oder zumindest nicht für eben jene Methoden (um es ganz genau zu nehmen).
    Deswegen ja auch meine Verwirrung wieso std:ostringstream, eine Subklasse von std::ostream, Polymorphie unterstützt, wenn doch alle Methoden in std::ostream nicht virtual sind. Irgendwas übersehe ich hier doch?

    Mit "virtueller Verbung" oben, falls du dich darauf bezogen hattest, meinte ich konkret das hier

    class A: virtual public B
    

    und das eben nur bei Mehrfachverbung eine Rolle spielt, so wie von mir erläutert.

    Das mit dem virtuellen DTor ergibt sich ja daraus, dass sonst eben Polymorphie beim Destruktor nicht greifen würde. Sprich es würde immer nur der Base Destruktor aufgerufen werden und eben nicht der von der Subklasse.



  • @Leon0402 sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Wie macht denn ostringstream /istringstream das? Da wurde ja nicht (nur?) der Buffer ausgetauscht, sondern tatsächlich richtig eine Subklasse von istream/ostream erstellt. Mit dem klappt auch alles wunderbar. Daraus schließe ich ja, dass man schon doch irgendwie richtig subclassen kann.

    Abzuleiten ist für die String-Streams sinnvoll schonmal deswegen weil man nicht überall wo man sowas braucht einen Stream-Buffer und einen Stream erstellen will. Wenn das mit einem Objekt geht ist das schonmal besser. Und natürlich kann man dann auch weitere Memberfunktionen dazumachen - wie z.B. die str Funktionen.

    Und wenn du wissen willst wie die das machen, wieso schaust du dir den Code nicht einfach an?
    Jede vernünftige IDE hat eine "go to definition" Funktion. Mit der ist es super einfach den Code von Standard-Library Klassen zu finden.
    (Ich kenne die Antwort, aber es geht darum dass du dir das erstmal selbst ansehen solltest statt sofort im Forum zu fragen.)



  • @hustbaer sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Und wenn du wissen willst wie die das machen, wieso schaust du dir den Code nicht einfach an?

    Weil ich den Code immer etwas überfordernd finde oder zumindest fand. Es geht immer besser 😃

    Also soweit ich das verstehe ist wohl die Realisierung so, dass gar keine Methoden überschrieben werden. Wenn ich also den operator<< aufrufe auf einen ostringstream wird tatsächlich die Methode in ostream genutzt. Stattdessen tauscht die Methode den Buffer entsprechend aus, sodass die Methoden in der Base Klasse das richtige machen. Korrekt?

    In dem Fall hast du auf jeden Fall recht, so wird das nichts mit dem mocken. Also kann ich im Grunde nur eine Abstraktion drüber bauen, damit es testbar wird. Oder halt ostringstream nehmen und mich damit begnügen, dass es halt nicht alles bietet, was ein richtiger Mock bietet.

    Hier bin ich etwas unentschlossen. Habe schon öfteres gelesen, dass man grundsätzlich auch eh nichts mocken sollte, was man nicht geschrieben hat. Stattdessen lieber Abstraktionen definieren und Libaries über ein kontrolliertes Interface nutzen.
    Nur macht man damit halt irgendwie alles viel komplexer 😞 Und wirklich mit dem C++ Spirit lässt sich das ja auch nicht vereinen. Ständig um jeden Funken Performance kämpfen, aber dann zusätzliche Schichten mit virtual verwenden?



  • @Leon0402 sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Also soweit ich das verstehe ist wohl die Realisierung so, dass gar keine Methoden überschrieben werden. Wenn ich also den operator<< aufrufe auf einen ostringstream wird tatsächlich die Methode in ostream genutzt. Stattdessen tauscht die Methode den Buffer entsprechend aus, sodass die Methoden in der Base Klasse das richtige machen. Korrekt?

    Korrekt.

    In dem Fall hast du auf jeden Fall recht, so wird das nichts mit dem mocken. Also kann ich im Grunde nur eine Abstraktion drüber bauen, damit es testbar wird. Oder halt ostringstream nehmen und mich damit begnügen, dass es halt nicht alles bietet, was ein richtiger Mock bietet.

    Oder einen manuell implementierten, sehr fummeligen Stream-Buffer verwenden. Also gehen müsste das schon. Will man aber wie gesagt vermutlich eher nicht machen.

    Hier bin ich etwas unentschlossen. Habe schon öfteres gelesen, dass man grundsätzlich auch eh nichts mocken sollte, was man nicht geschrieben hat.

    So grundsätzlich würde ich das nicht sagen.

    Stattdessen lieber Abstraktionen definieren und Libaries über ein kontrolliertes Interface nutzen.

    In diesem Fall halte ich das für einen guten Rat.

    Nur macht man damit halt irgendwie alles viel komplexer 😞

    Ja, wird "komplexer". In dem Sinn dass mehr Code da ist. Allerdings hast du in grösseren Programmen schnell ein Problem wenn du keine sauberen Abstraktionen verwendest. Das Einziehen dieser sauberen Abstraktionen macht das Programm dann zwar oft auch grösser. Manchmal aber auch kleiner. Und wenn man es richtig macht immer besser wartbar.

    Und wirklich mit dem C++ Spirit lässt sich das ja auch nicht vereinen. Ständig um jeden Funken Performance kämpfen, aber dann zusätzliche Schichten mit virtual verwenden?

    Naja...
    Ich würde behaupten dass Performance hier keine Rolle spielt. In diesem konkreten Fall. Und dass die Sorge um die Performance auch allgemein oft völlig überzogen ist. "Premature optimization is the root of all evil" ist schon nicht ganz falsch.

    Und wenn Performance wirklich mal eine Rolle spielt, dann kann man auch mit statischer Polymorphie arbeiten (aka. Templates). Das macht's aber auch nicht schöner. Aber du kannst dir ja aussuchen was du weniger hässlich/unpassend findest.



  • @hustbaer sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Ja, wird "komplexer". In dem Sinn dass mehr Code da ist. Allerdings hast du in grösseren Programmen schnell ein Problem wenn du keine sauberen Abstraktionen verwendest. Das Einziehen dieser sauberen Abstraktionen macht das Programm dann zwar oft auch grösser. Manchmal aber auch kleiner. Und wenn man es richtig macht immer besser wartbar.

    Woran machst du fest, ob sich eine Abstraktion lohnt?
    Hättest du die Abstraktion für std::iostream auch geschrieben, wenn du es nicht für die Testbarkeit bräuchtest?
    Schreibst du solche Abstraktionen auch für andere Libs? Zum Beispiel für eine Log Lib oder für eine Datenbank Lib? Falls nein, warum dann nicht hier auch?

    Eventuell hast du da ja auch irgendwie weitergehende Lektüre oder so. Mit Software Design wollte ich mich schon länger mal beschäftigen.

    @hustbaer sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Ich würde behaupten dass Performance hier keine Rolle spielt. In diesem konkreten Fall. Und dass die Sorge um die Performance auch allgemein oft völlig überzogen ist. "Premature optimization is the root of all evil" ist schon nicht ganz falsch.

    Tut sie auch nicht. Aber ich möchte mir natürlich auch nicht Sachen angewöhnen, wo jeder C++ Programmierer die Hände über dem Kopf zusammenschlägt 😃 Und ich habe irgendwie immer das Gefühl die ganzen Sachen zur Testbarkeits (Abstraktionen, Interfaces etc.) machen eig. das Software Design eher besser ... aber jeder C++ Profi würde sich denken: Was nen Anfänger 😃



  • @Leon0402 sagte in Unit Testen einer Funktion, die im Fehlerfall nicht stoppt:

    Woran machst du fest, ob sich eine Abstraktion lohnt?

    Ich hab da kein fixes Regelwerk. Wenn es mir sinnvoll erscheint halt 🙂

    Also z.B. wenn ich unterschiedliche Implementierungen brauche, und das vorhandene Interface aus einer Third-Party Library viel mehr kann als ich brauche.

    Oder wenn ich merke dass eine Klasse mehr als nur eine Aufgabe erfüllt. Also angenommen du sollst Input von irgendwo lesen und verarbeiten. Den Input muss man jetzt normalerweise erstmal parsen. Lesen, Parsen und Verarbeiten sind 3 verschiedene Aufgaben. Also macht man im Idealfall 3 verschiedene Klassen. Sagen wir zum Lesen können wir was fertiges nehmen, std::istream oder was auch immer. Bleibt noch Parsen und Verarbeiten. D.h. ich mache eine Parser-Klasse und eine die dann die Daten verarbeitet. Ich gebe nicht den std::istream an eine Klasse und mache dann beides dort. Damit kann ich beide Teile unabhängig testen. Und ich kann auch beiden Teile unabhängig lesen um Fehler zu suchen, unabhängig ändern wenn Erweiterungen benötigt werden usw.
    Die Teile die man sich ansehen muss werden kleiner und dadurch übersichtlicher. Dafür hat man mehr Teile - das ist der Tradeoff. Wenn man es gut macht stört es aber kaum dass es mehr Teile werden, da jeder Teil immer nur mit wenigen anderen Teilen zusammenarbeitet und das über klar definierte, einfache Schnittstellen.

    Wenn man es schlecht macht, macht man damit allerdings alles nur noch schlimmer. Denn dann hat man viele kleine bis mittelgrosse Teile, die so miteinander verschlungen sind dass man den Überblick verliert.

    Hättest du die Abstraktion für std::iostream auch geschrieben, wenn du es nicht für die Testbarkeit bräuchtest?

    In diesem super-einfachen Beispiel so wie du es gezeigt hast vermutlich nicht.

    Schreibst du solche Abstraktionen auch für andere Libs? Zum Beispiel für eine Log Lib oder für eine Datenbank Lib? Falls nein, warum dann nicht hier auch?

    Das kommt immer auf den konkreten Fall drauf an. Logging Libs erfüllen zumeist genau den Job den man braucht. Sie stellen ein Interface bereit das praktisch ist zum Loggen. Das nochmal zu abstrahieren macht nur selten Sinn. Es kann in Ausnahmefällen Sinn machen. Aber i.A. eher nicht.

    Was DB Libs angeht: Die Verwendung dieser sollte quasi immer auf einen kleinen Teil der Anwendung limitiert werden. Dabei abstrahiert man nicht direkt den Zugriff auf die DB - die DB Lib ist ja meist schon eine Abstraktion. Der Knackpunkt ist auch hier wieder dass eine Klasse im Idealfall nur eine Aufgabe haben sollte.

    Wenn ich z.B. Messwerte von irgendwo auslese und in eine DB schreiben soll, dann habe ich wieder zwei Teile: ein Teil besorgt die Daten, der andere Teil schreibt sie wo hin. D.h. ich mache eine Klasse die die Daten ausliest und eine zweite wo ich Daten reinstopfen kann die diese dann in die Datenbank schreibt.

    Dadurch ergibt sich normalerweise ganz natürlich dass nur wenige Klassen direkt mit der DB arbeiten. Kommt aber natürlich sehr stark auf die Anwendung drauf an (was macht das Ding und wie gross/komplex ist es).

    Bei grösseren Anwendungen wo viele verschiedene Stellen auf die Datenbank zugreifen müssen, kann es aber wirklich Sinn machen den Zugriff auf die DB nochmal weiter zu abstrahieren. Dazu werden gerne ORM Libraries verwendet - obwohl ich kein grosser Freund dieser bin.

    Eventuell hast du da ja auch irgendwie weitergehende Lektüre oder so. Mit Software Design wollte ich mich schon länger mal beschäftigen.

    Ich kann dir dazu leider kein gutes Buch empfehlen.



  • @hustbaer Danke erstmal für den Input, sehr hilfreich 🙂

    Ich habe mal versucht das ganze in die Tat umzusetzen, bin jedoch über ein Problem gestolpert. Mit meiner eigenen ConsoleIO Klasse gehen natürlich auch die operator<< Überladungen von z.B. meinen eigenen Klassen nicht mehr. (Hinweis: Ich habe bei meiner ConsoleIO Klasse einen virtuellen operator<< geschrieben anstatt einer writeLine Methode bzw. zusätzlich geschrieben)

    Das ist irgendwie eher suboptimal, gibt es dafür einen Lösungansatz? Für meine eigene Klassen könnte ich ja sogar noch den operator entsprechend umschreiben ... aber gefallen tut mir es nicht, dass ich da an so vielen Stellen im Code Sachen abändern muss. Und mit Code, der nicht mir gehört, funktioniert es natürlich auch nicht.

    Grundsätzlich könnte man natürlich einfach ne template Funktion machen, aber die kann halt nicht mehr virtual sein.



  • @Leon0402
    Ich kann immer nur den Code beurteilen den du herzeigst.
    Für diesen macht es keinen Sinn einen operator << zu definieren. Der braucht nur "read line" und "write line".

    Das ist irgendwie eher suboptimal, gibt es dafür einen Lösungansatz? Für meine eigene Klassen könnte ich ja sogar noch den operator entsprechend umschreiben ... aber gefallen tut mir es nicht, dass ich da an so vielen Stellen im Code Sachen abändern muss. Und mit Code, der nicht mir gehört, funktioniert es natürlich auch nicht.

    Und spätestens jetzt hab ich keinen Plan mehr was du eigentlich willst.
    Du hast ein konkretes Problem genannt zu dem ich dir eine konkrete Antwort gegeben habe. Jetzt schreibst du dass du in Code den ich nicht sehe irgendwas umschreiben musst und das lästig ist und was wenn der Code von jmd. anderem ist und...
    Keine Ahnung.

    Stell konkrete Fragen wenn du konkrete Antworten willst.


Log in to reply