Unit Testen einer Funktion, die im Fehlerfall nicht stoppt



  • @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.



  • Da war ich möglicherweise schon etwas weiter im Kopf ohne die entsprechenden Informationen nachzuliefern 🙂

    Die fehlende Information war vermutlich: Jetzt wo ich so eine Abstraktion für std::cout / std::cin habe, möchte ich die auch in anderen Funktionen das Konsolen Interfaces nutzen. Denn auch die profitieren dann ja von den Möglichkeiten zum Mocken.

    Zum Beispiel habe ich Methoden:

    • Um Daten einzugeben
    • Um Daten auszugeben

    Also sprich die Methoden, in denen ich eben unter anderem auch so eine "getInput" Methode verwende. Ist ja erstmal ganz sinnig auch da eben meine ConsoleIO Klasse zu nutzen, damit ich auch für diese Methoden mocken kann.
    Da kannst du dir aber vlt. vorstellen, dass ich mehr als nur eine Zeile Text ausgebe, sondern vlt. eher sowas habe:

    Console consoleIO; 
    consoleIO << "Das sind die Daten: " << someDataObject << "\n";
    

    Also eben entsprechend auch verketten will (deswegen der operator<<) und eben (das ist das Problem) auch nicht nur Strings ausgeben möchte. Sondern z.B. auch integer, floats und alle anderen selbstgeschrieben Objekte oder Objekte einer Lib, die eben den Operator<< für sich definieren.

    Und all das geht eben nicht mehr mit ConsoleIO. Weder andere primitive Datentypen noch eben komplexe Datentypen, die den operator<< mit einem std::iostream definieren (wie es ja quasi Standard ist).

    Jetzt hatte ich mich gefragt: Kann man das ändern? Ich würde eben nicht gerne alles erst in einen String konvertieren (Indem ich z.B. alles erstmal in einen stringstream schiebe).

    Das einfachste dazu, was mir einfällt sind Templates. Mein ConsoleIO Klasse ist ja nur einen wrapper von daher sollte das ja erstmal ohne Probleme funktionieren. Mein operator<< oder auch "writeLine" Methode nimmt jeden Datentypen entgegen und delegiert weiter and std::cout.

    Nur können Templates ja nicht virtual sein. Und das ist ja der Hauptgrund, warum ich diesen Wrapper überhaupt schreibe 😃

    Auf eine Lösungsidee habe ich mich grade selbst während dem Schreiben gebracht! 🙂 Ich könnte es natürlich so machen:

    class ConsoleIO {
    public:
        virtual ~ConsoleIO();
        virtual std::string readLine();
        virtual void writeLine(std::string_view line);
    
       template <typename T>
       ConsoleIO& operator<<(T& data) {
           std::ostringstream stream {}; 
           stream << data; 
           writeLine(data.str()); 
           return *this;
       }
    };
    

    Also, sofern ich das jetzt grade aus dem Kopf einigermaßen syntax mäßig korrekt gemacht habe: Ich schreibe eine Template Methode, die eben alles mit ostringstream in einen string konvertiert. Dann rufe ich damit meine writeLine Metode auf und auch nur die mocke ich letzendes.

    Damit sollte ich eigentlich meine ConsoleIO Klasse mit allen möglichen Datentypen aufrufen können, auch denen die irgendwo einen eigenen operator<< definiert haben. Und trotzdem kann ich das ganze Mocken.
    Beim Mocken muss man halt hier nur aufpassen, da console << "Teil 1" << "Teil2" << "Teil3" natürlich in 3 Aufrufen resultiert und nicht nur in einem 🙂



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

    Beim Mocken muss man halt hier nur aufpassen, da console << "Teil 1" << "Teil2" << "Teil3" natürlich in 3 Aufrufen resultiert und nicht nur in einem

    Ja. Weswegen ich das auch eher vermeiden würde. Also besser writeLine (oder writeMessage oder was auch immer) wird immer nur mit einer fertigen Nachricht aufgerufen, nicht mit Stücken.

    Ansonsten gibt's natürlich noch die Möglichkeit im Mock bei writeLine das Zeug einfach nur in einen Stream zu stopfen (z.B. in eine std::stringstream Membervariable) und die Checks dann alle in readLine zu machen.

    Wobei dein Stream-Insertion Operator ein weiteres Problem hat:

       template <typename T>
       ConsoleIO& operator<<(T& data) {
           std::ostringstream stream {}; 
           stream << data; 
           writeLine(data.str()); 
           return *this;
       }
    

    Du kannst da wunderbar sämtliche "modifier" (std::hex etc.) reinstecken, wird fehlerfrei kompilieren. Nur Auswirkung wird es keine haben.

    Also mMn.: entscheide dich für eine Variante. Entweder gleich direkt mit Streams (und dann halt ohne Unit-Tests oder mit komplizierten Stream-Buffer Mocks für die Unit-Tests). Oder mit einem eigenen Interface - aber dann ohne iostream-artigen Stream-Insertion Operator.

    Beides in Kombination halte ich für Quatsch.

    Falls du dich für eigenes Interface ohne iostream-artigen Stream-Insertion Operator entscheidest: guck dir https://github.com/fmtlib/fmt an. Damit kann man sehr schön (und sehr performant) Text "formatieren".



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

    Du kannst da wunderbar sämtliche "modifier" (std::hex etc.) reinstecken, wird fehlerfrei kompilieren. Nur Auswirkung wird es keine haben.

    Wobei mir die Funktionalität mit dem writeLine ja auch verloren geht, letzenendes. Berechtigt ist aber natürlich auf jeden Fall die Kritik, dass es fehlerfrei kompiliert. Wobei das vermutlich behebar wäre?

    Konkret würden mir da jetzt zwei Lösungsmöglichkeiten einfallen:

    Zum einen eine Template Spezialisierung für Manipulatoren (Wüsste zwar auf Anhieb nicht wie, aber da ich das in dem ostream Code als Überladung gesehen habe, sollte das ja gehen) und darin dann asserten. Okay Manipulatoren gehen immer noch nicht, aber zumindest sollte ne Fehlermeldung geschmissen werden 🙂

    Eine weitere Möglichkeit wäre den ostringstream als Member zu haben und nur in diesen im operator<< zu schreiben:

    template <typename T>
    ConsoleIO& operator<<(T& data) {
           stream << data; 
           return *this;
    }
    

    Dann eine Art eigenen Manipulator zu schreiben. Der Einfachheithalber jetzt vlt. einfach mal ein

    struct flush {};
    inline Flush flush;
    

    und eine entsprechende Template Spezialisierung für den operator

    template <>
    ConsoleIO& operator<<(flush& data) {
           writeLine(stream.str()); 
           stream = ostringstream {};
           return *this;
    }
    

    Die Benutzung erfolgt dann:

    console << "This " << "is " << "my " << "data " << flush; 
    

    Wenn ich mich nicht komplett irre sollten dann auch die modifier alle gehen (Zumindest sowas wie std::hex ... bin mir nicht so 100% sicher, was genau es alles für modifier gibt und ob alle mit nem ostringstream gehen).

    Gleichzeitig ist hiermit auch das Problem der mehreren calls gelöst. Eine Zeile -> Ein Call beim Mocken.

    Der einzige Nachteil den ich hier sehe ist, dass man halt manuell irgendwie flushen muss (Und das man es potentiell vergessen könnte). Könnte man jetzt auch über ne extra Methode machen anstatt da direkt so im Stream.
    Aber aus meiner Sicht könnte man denke ich mit den Nachteilen hier leben.

    Hab ich was übersehen? Gibt es andere Probleme mit dem Lösungsvorschlag?

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

    Also mMn.: entscheide dich für eine Variante.

    Ich möchte nicht deine Meinung in Frage stellen. Ich finde es nur interessant und recht lehrreich meinen Ansatz noch etwas weiterzuverfolgen. Schon einiges in diesem Thread gelernt, vielen Dank 🙂

    Vermutlich ist die Entscheidung, wenn das hier nichts wird mit meinem Ansatz oben, dann eher erstmal ostringstream weiter zu nutzen für Mocks oder gar nicht die Methoden unit zu testen. Aber ich werde mir auf jedenfall die fmt lib mal anschauen. Ist ja jetzt mit std::format auch im C++ Standard 🙂



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

    Eine weitere Möglichkeit wäre den ostringstream als Member zu haben und nur in diesen im operator<< zu schreiben:

    Kann man machen. Man könnte das aber genau so gut über eine unabhängige Hilfsklasse machen. Dann hätte man die Aufgaben "Console IO" und "Text-Formatierung" schöner getrennt.

    Dem "Console IO" Teil kann es schliessliche egal sein wie der Text formatiert wird, und dem Text-Formatierungs Teil kann es egal sein wo die Daten hingehen.



  • Was spricht denn generell gegen eine virtuelle (protected) Methode, die einfach nur eine referenz auf ein std::ostream zurückliefert?
    std::ostream& stream();
    Das ist doch das, was dein ConsoleIO eigentlich wegabstrahiert: Welches konkrete ostream Objekt du nun eigentlich wirklich verwendest.
    Jedenfalls ließe sich auf diese Weise ein templatetisierter op << bauen, in dem man diesen einfach nur für die Basisklasse baut.

    template <class TEgal>
    MyBaseClass& operator << (const TEgal& data)
    {
        stream() << data;
        return *this;
    }
    


  • @DNKpp
    Und wie bekommt die Klasse dann mit dass in diesen ostream eine fertige Nachricht geschrieben wurde?



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

    @DNKpp
    Und wie bekommt die Klasse dann mit dass in diesen ostream eine fertige Nachricht geschrieben wurde?

    Muss sie das denn? Ich habe mir ehrlich gesagt nicht jeden eurer Posts durchgelesen, aber so wie es aus seinem letzten Post scheint, will er ja ein flush "signal" via Manipulator senden. Also würde doch ein std::endl oder std::flush reichen? Man muss hier ja keinen ostringstream rausreichen. Es reicht ja auch der tatsächliche target stream und der lässt sich ja wie gehabt durch die beiden genannte Manipulatoren flushen.


Anmelden zum Antworten