"Mock-Objekte" in C++



  • [ Für alle die nichts mit dem Begriff Mock Objekt anfangen können: Wenn ich eine Klasse A testen will (Unit-Test), die mit einer Klasse B kommunizieren muss, aber Klasse B entweder noch nicht implementiert ist oder zu aufwendig ist (z.B. GUI-Sachen, Netzwerk, Datenbank, Dateiparsen usw.), kann ich das Mock Object Pattern anwenden: Ich führe für Klasse B eine abstrakte Basisklasse bzw. Interface ein, implementiere dieses Interface durch ein "Mock-Objekt", was nur so tut als wär es eine richtige Instanz von B, aber tatsächlich das Verhalten zeigt, dass ich in meinem Test für A gerade brauche. z.b. könnte ich zum Testen eines Netzwerkclients einen Mock-Server schreiben, der einfach bestimmte Fehlersituationen produziert. ]

    Das ganze ist beispielsweise in Java kein Problem, da Polymorphie default ist. Was aber mach ich am dümmsten in C++? Da hat man ja des öfteren konkrete Objekte mit nicht-virtuellen Funktionen ... ist es nicht ein bisschen blöd, nur zum Testen alles virtuell und indirekt zu machen, obwohl ich den Polymorphismus eigentlich gar nicht brauche?

    Nach dem, was ich so im Netz gefunden habe, besteht da kein großes Problembewußtsein, dann wird eben eine abstrakte Basisklasse dazuerfunden, was solls ... das kanns doch nicht sein, oder?



  • Hallo,
    spontan würde ich da wohl auf Compilezeit-Polymorphie via #ifdef zurückfallen.
    Für mich stellt sich aber die Frage, ob es soviel sinn macht eine Klasse B zu testen die von einer *konkreten* Klasse A abhängig ist, bevor ich die Klasse A habe. In diesem Fall ist B ja scheinbar nicht nur von einem Protokoll abhängig.
    Da macht es imo schon mehr sinn, beide Klassen parallel, in abhängigkeit meiner aktuellen User-Story, zu entwickeln und zu testen.

    Das ganze ist beispielsweise in Java kein Problem, da Polymorphie default ist

    Auch in Java könnte ich aus performance-Gründen die Methoden der Klasse als final deklariert haben. Das Problem bleibt imo das selbe, nämlich ob oder wie ich für eine konkrete Klasse, die nicht polymorph eingesetzt wird/werden soll, ein Mock-Objekt erstellen kann.

    So wie ich das sehe ergibt sich das Problem aus einer frühzeitigen Optimierung auf Geschwindigkeit bzw. Größe. Man könnte ja auch mit einem Interface beginnen (-> kein Problem mit Mock-Object) und dann, bei Bedarf, dieses durch eine konkrete Klasse ersetzen.

    Letztlich besteht das Problem aber bereits im Design. Wenn ich vor der Implementierung schon weiß, dass A eine konkrete Klasse ist und kein Interface, dann erscheint es mir logisch, dass ein Test, der etwas anderes voraussetzt schwierig wird.

    Hm, irgendwie sind das doch alles nur laute Gedanken 😃



  • HumeSikkins schrieb:

    spontan würde ich da wohl auf Compilezeit-Polymorphie via #ifdef zurückfallen.
    Für mich stellt sich aber die Frage, ob es soviel sinn macht eine Klasse B zu testen die von einer *konkreten* Klasse A abhängig ist, bevor ich die Klasse A habe.

    A ist eine Fassade für eine existierende Library in C. In jedem Testcase den ganzen Lebenszyklus von A durchzuspielen ist ziemlich aufwendig, konkrete Testbedingungen herzustellen ziemlich kompliziert. (BTW ich hatte die Rollen von A und B andersrum definiert, aber ich bleib jetzt bei deiner Betrachtungsweise;))

    Auch in Java könnte ich aus performance-Gründen die Methoden der Klasse als final deklariert haben. Das Problem bleibt imo das selbe, nämlich ob oder wie ich für eine konkrete Klasse, die nicht polymorph eingesetzt wird/werden soll, ein Mock-Objekt erstellen kann.

    Das Problem ist aber wesentlich geringer, weil final eine Optimierung ist, nicht-virtual jedoch idR nicht. Ich hab jedenfalls noch nie die Empfehlung gesehen, dass man alle Memberfunktionen standardmäßig virtual machen soll, und erst wenn man profiled hat, das virtual bei manchen wieder entfernt.

    So wie ich das sehe ergibt sich das Problem aus einer frühzeitigen Optimierung auf Geschwindigkeit bzw. Größe.

    Wenn man so will, könnte man die Entscheidung pro C++ auch in diese Schublade stecken. 🤡



  • Ich verstehe nicht ganz, wozu man denn diese Basisklasse eigentlich braucht. Ich würde in dem Fall einfach eine Klasse mit der selben Schnittstelle erstellen und diese dann später austauschen.



  • Bashar schrieb:

    ist es nicht ein bisschen blöd, nur zum Testen alles virtuell und indirekt zu machen, obwohl ich den Polymorphismus eigentlich gar nicht brauche?

    Du kannst ein virtual zur CompileTime an und abschalten:

    class InterfaceOfB
    {
    public:
      virtual void coole_func() = 0;
    };
    
    struct Empty {};
    
    template<class Base>
    class basic_B : public Base
    {
    public:
      void coole_func();
    };
    
    #ifdef UNIT_TEST //oder was weiss ich
      typedef basic_B<InterfaceOfB> B;
    #else //normaler release code
      typedef basic_B<Empty> B;
    #endif
    

    So kannst du virtual in der 'Debug' Version haben, und kein virtual in der Release Version.



  • Tests will man normalerweise automatisiert auf dem original Quelltext laufen lassen - ohne noch irgendwas veraendern zu muessen.



  • In den Programmen, die ich für die Maschinen geschrieben habe, waren derlei Objekte die ersten, die ich implementiert habe, und sie sind auch bis zum Ende drin geblieben. (Bin schon wieder ohne Worte, dass die Dinger schon wieder einen Namen bekommen haben...)
    Die Umschaltung lief einfach über Selbsterkennung, ein globales Flag, einen Switch in der GUI oder Skripting.



  • Bashar schrieb:

    [ Für alle die nichts mit dem Begriff Mock Objekt anfangen können: ...]

    Das ganze ist beispielsweise in Java kein Problem, da Polymorphie default ist. Was aber mach ich am dümmsten in C++? Da hat man ja des öfteren konkrete Objekte mit nicht-virtuellen Funktionen ... ist es nicht ein bisschen blöd, nur zum Testen alles virtuell und indirekt zu machen, obwohl ich den Polymorphismus eigentlich gar nicht brauche?

    ... besteht da kein großes Problembewußtsein, dann wird eben eine abstrakte Basisklasse dazuerfunden, was solls ... das kanns doch nicht sein, oder?

    HumeSikkins schrieb:

    Hallo,
    ...
    Da macht es imo schon mehr sinn, beide Klassen parallel, in abhängigkeit meiner aktuellen User-Story, zu entwickeln und zu testen.
    ...
    So wie ich das sehe ergibt sich das Problem aus einer frühzeitigen Optimierung auf Geschwindigkeit bzw. Größe. Man könnte ja auch mit einem Interface beginnen (-> kein Problem mit Mock-Object) und dann, bei Bedarf, dieses durch eine konkrete Klasse ersetzen.

    Letztlich besteht das Problem aber bereits im Design. Wenn ich vor der Implementierung schon weiß, dass A eine konkrete Klasse ist und kein Interface, dann erscheint es mir logisch, dass ein Test, der etwas anderes voraussetzt schwierig wird.

    Jo, man kann das Mock Obj wohl als ein abstract factory pattern verstehen, das im Regelfall vom der Testsuite gestellt wird.
    Bei der Entwicklung von Unit-Teatsystemen sollte man schon darauf achten, dass Zielcode und Testsystem orthogonal aufeinander entwickelt werden. Schon das Setzen eines simplen Flags zu Testzwecken im Zielcode ist ein Zeichen dafür, das was schief läuft. Die "dazu erfundene" abstrakte Basisklasse ist ein wesentlicher Bestandteil des Testframeworks, die Komponenten über simple Vererbung zusammenführen - einen konkreten Testvektor und eine konkrete Instanz - ob die nun vom Mock Objekt oder von einer vituellen Zielcodefunktion stammt, ist dabei unerheblich. Deshalb das "fehlende Problembewußtsein " ...

    So wie ich auch TDD verstanden habe und einsetze, wird zuerst die Testsuite mit Testvektor entwickelt und dann erst der eigentlichen Zielcode vorangetrieben.

    Es ist korrekt, dass Performanceuntersuchungen und Laufzeitbetrachtungen gerade in Java bei virtuellen und abstrakten Funktionen im Testframework stark verfälscht werden (Heisenbergische Unschärferelation - Ort und Implus eines Teilchens sind nicht beliebig genau bestimmbar 😃 ) So dass, wie von Hume vorgeschlagen, eine parallele Entwicklung im Zielcode sinnvoll sein mag. Mir pesönlich gefallen mehr die Ideen vom Einsatz Meta-Code-Generatoren, um wieder eine Trennung von Testsystem und Zielcode zu erhalten.



  • Prof84 schrieb:

    Die "dazu erfundene" abstrakte Basisklasse ist ein wesentlicher Bestandteil des Testframeworks, die Komponenten über simple Vererbung zusammenführen - einen konkreten Testvektor und eine konkrete Instanz - ob die nun vom Mock Objekt oder von einer vituellen Zielcodefunktion stammt, ist dabei unerheblich. Deshalb das "fehlende Problembewußtsein " ...

    So wie ich auch TDD verstanden habe und einsetze, wird zuerst die Testsuite mit Testvektor entwickelt und dann erst der eigentlichen Zielcode vorangetrieben.

    Das heißt? Dass die abstrakte Basisklasse schon vom Test her da ist und der Zielcode sich da nur reinhängt? Bin mir nicht sicher, ob ich dich richtig verstehe, weil das meine Frage ja nur streift.



  • Bashar schrieb:

    Das heißt? Dass die abstrakte Basisklasse schon vom Test her da ist und der Zielcode sich da nur reinhängt? Bin mir nicht sicher, ob ich dich richtig verstehe, weil das meine Frage ja nur streift.

    Wie jetzt?? 😕 - Die Seite kennst Du?!
    http://www.xprogramming.com/software.htm

    Die auch?!
    http://cppunit.sourceforge.net/cgi-bin/moin.cgi/FrontPage?action=show&redirect=StartSeite

    Und die?!
    http://www.mockobjects.com/wiki/

    Oder die?!
    http://mockpp.sourceforge.net/



  • Prof84 schrieb:

    Wie jetzt?? 😕

    Ich habe nicht nach Erklärungen, was Mock Objekte sind oder was man damit machen kann, gesucht, sondern nach einer Diskussion, wie sich diese Idee mit einem der Grundgedanken von C++ verträgt, nämlich dass Polymorphie optional ist, und dass ich nur für das bezahle was ich nutze. Vielleicht ist das auch kein Problem, kann sein, weiß ich nicht. Deshalb frag ich ja.



  • @Bashar:
    Ja, irgendwie haben wir Frequenzprobleme....

    1. Für jeden Testcase den Du implementierst wird bezahlt. Das ist auch bei Mock Objekten nicht anders.

    2. Es ist Intention eines Unit Testsystems jede entwickelte Methode durchzutesten. Dabei ist die Gesamt-Performance egal. Deswegen heißt das auch UNIT-Test. Wir haben hier Systeme, die bollern nach Feierabend durch das Testfield 3-4 Stunden durch. Ob daraus später 6,7 oder 8 Stunden werden ist uns ebenfalls schnuppe. Hauptsache - wir haben vor Schichtbeginn unsere Ergebnisse. Ob dies nun in astrophilosphischen ethischen theologischen Sinne einer optionalen C++ Polymorphie ist, tangiert mich und wahrscheinlich die meisten anderen Entwickler perifer ...



  • Prof84 schrieb:

    1. Für jeden Testcase den Du implementierst wird bezahlt. Das ist auch bei Mock Objekten nicht anders.

    Ich meinte nicht monetär, sondern im übertragenen Sinne.

    1. Es ist Intention eines Unit Testsystems jede entwickelte Methode durchzutesten. Dabei ist die Gesamt-Performance egal. Deswegen heißt das auch UNIT-Test. Wir haben hier Systeme, die bollern nach Feierabend durch das Testfield 3-4 Stunden durch.

    Die Performance beim Testen ist mir auch schnuppe, es geht um die Performance der fertigen Applikation. Aber auch um andere Schwierigkeiten, die sich durch Polymorphie ergeben.



  • Bashar schrieb:

    Die Performance beim Testen ist mir auch schnuppe, es geht um die Performance der fertigen Applikation. Aber auch um andere Schwierigkeiten, die sich durch Polymorphie ergeben.

    Mock Objekte sind 100%ige Bestandteile des Testsystems. Welche Schwierigkeiten? Du setzt den Testcase an das Mock Objekt und ratterst die
    Polymorphie durch. Wo siehst Du die Probleme?

    Ups... Nacht! 😮

    P84



  • Prof84 schrieb:

    Bashar schrieb:

    Die Performance beim Testen ist mir auch schnuppe, es geht um die Performance der fertigen Applikation. Aber auch um andere Schwierigkeiten, die sich durch Polymorphie ergeben.

    Mock Objekte sind 100%ige Bestandteile des Testsystems. Welche Schwierigkeiten? Du setzt den Testcase an das Mock Objekt und ratterst die
    Polymorphie durch. Wo siehst Du die Probleme?

    Ich weiß, dass die MockObjekte zum Test gehören. Aber die ihnen übergeordnete Basisklasse, die ohne sie nicht notwendig gewesen wäre, nicht. Beispiel:

    // ohne Testen:
    class A { public: bool bar(); };
    class Testkandidat { public:   bool foo(A&); };
    
    // mit Testen:
    class ABase { public: virtual bool bar() = 0; };
    class A : public ABase { public: bool bar(); };
    class Testkandidat { public bool foo(ABase&); };
    
    class MockA : ABase { public: bool bar(); };
    TEST(Testkandidat, foo)
    {
      Testkandidat k;
      MockA a;
      CHECK(k.foo(a));
    }
    

    Wenn ich das Programm irgendwann als fertig deklariere und die Tests entferne/deaktiviere, hab ich immer noch zusätzlich die ABase-Klasse, ausserdem den Umstand, dass viele ihrer Memberfunktionen virtual sind, dann kann ich die Klasse nicht mehr sorglos kopieren, muss sie evtl. in eine Handle-Klasse einpacken, etc.

    Ups... Nacht! 😮

    Ups, genau. Nacht 🙂



  • Bashar schrieb:

    [

    // ohne Testen:
    class A { public: bool bar(); };
    class Testkandidat { public:   bool foo(A&); };
    
    // mit Testen:
    class ABase { public: virtual bool bar() = 0; };
    class A : public ABase { public: bool bar(); };
    class Testkandidat { public bool foo(ABase&); };
    
    class MockA : ABase { public: bool bar(); };
    TEST(Testkandidat, foo)
    {
      Testkandidat k;
      MockA a;
      CHECK(k.foo(a));
    }
    

    Wenn ich das Programm irgendwann als fertig deklariere und die Tests entferne/deaktiviere, hab ich immer noch zusätzlich die ABase-Klasse, ausserdem den Umstand, dass viele ihrer Memberfunktionen virtual sind, dann kann ich die Klasse nicht mehr sorglos kopieren, muss sie evtl. in eine Handle-Klasse einpacken, etc.

    Ach so, jetzt dämmert´s ...
    a) Inneres isoliertes Testen
    Zielcode + Mock Objekt incl. Testvektor => Testsuite (AOP)
    b) Recycle Testsuite
    Testsuite - Mock Objekt => Zielcode (wie auch immer ...)

    Hmmm ... pupupupuh ... heiß, heiß ....Ok!

    0. Lohnt es sich darüber nachzudenken?
    Während bei Java systembedingt eine Typensicherheit garantiert ist, ist das "Rosinenpoolen" bei C++ nicht ganz ungefährlich. Da sind virtuelle Funktionen und abstrakte Klassen das geringste Problem.Ich setze Mock Objekte auch oft in Bereichen ein, indem ich eine penible Pointer-Sicherheit gewährleisten muss. Dort wäre ein "switch off" der Mock Objekte ohne Revision wie Rauchen im Benzinlager. Thema: Vertauenswürdige Komponenten - Der generierte Zielcode ist nicht getestet!

    Apropos Einsatz:
    0b) Sinn und Unsinn von Mock Objekten
    ... oder vielleicht legen wir erst eine Etage höher an...
    0a) Isoliertes Testen - Einsatz von Stubs, Shunts und Mocks

    " ...
    -Stubs bieten die minimalste Fake Implementierung einer Klasse oder Schnittstelle.Ich verwende sie immer, wenn der Aufruf an sich, noch sein Aktualparameter (RR: Was für ein Wort) für den Test interessant sind.
    -Self-Shunt ist sehr elegant solange die implementierte Schnittstelle schmall bleibt.Bei mir kommen sie zum Einsatz, wenn der Aufruf, d.h. die Interaktion zwischen den Objekten geprüft werden soll.
    -Mocks sind die unbestreitbaren Könige unter den Hochstablern. Ich benötige sie, wenn sie dublizierenten Testcode vermeiden helfen oder etwas mehr als nur ins triviale Verhalten vorzugaukeln ist.
    ...
    Wir wissen, dass wir es mit den Mocks übertrieben haben,
    - wenn der Test der Klasse mehr als drei Mocks verlangt.
    - wenn Mocks noch wieder Gebrauch von Mocks machen.
    - wenn für die Mehrzahl der Enitäten im System eine Schnittstelle, ein Mock und die tatsächliche Anwendungsklasse existiert.
    - wenn Mocks ihrerseits so kompliziert sind, dass sie eigentlich schon wieder eigene Test benötigen.
    ..."
    Quelle: http://frankwestphal.de/TestgetriebeneEntwicklungmitJUnitundFIT.html
    9.23, 9.18

    Also ich setze Mock Objekte oft in der Systementwicklung ein, wenn es mir nicht möglich ist, die Systemserver "unter Feuer zu nehmen". Oder irgend ein Systemmüll generiert wird, der Mitarbeiter und Abteilungen nervt oder irgendeine Auswertung verfälscht, die noch gebraucht wird etc.
    Aber mein Haupteinsatzfeld ist immer noch die "Stecknadelfunktion", weil ich mir als Freiberufler, im Gegensatz zu vielen Angestellten, nicht erlauben kann "für ein Monatsgehalt über ein einziges Problem zu hängen". Ich generiere zb mein Mock und mache erst einmal weiter, ohne dass das Testsystem spinnt, ich die Team-Members aufhalte oder verlangsame oder ich auf irgendeine "Schnachtasse" warten müsste, bis die aus dem Quark kommt (so fuck Strouppy - " ... versuchen Sie niemals soziologische Probleme mit technischen Mitteln zu lösen!" 🤡 ). Wenn ich mich dann länger brässig anstelle, kann ich auch so leichter mein Problem dem "Halbgott" (Systemanalytiker || Senior Developer) servieren. Mit dem Framework wird er direkt an den neuralgischen Punkt - zb Mock - gelotst, ohne das er sich erst per Analyse durch die äußere Kapselung bohren muss.

    1. Wenn nich´ - wat machen wa damit?
    Die Testsuites werden mit Mockobjekten idR ehe refakturiert oder gehen durch die Revision für den Test der nächsten Ebene. D.h. aus der generischen Programmierung werden später auch konkrete Klassen und aus der Testsuite wird Zielcode abgebildet und dieser unabhängig getestet, obwohl Beide sich lediglich durch das Mock Objekt unterscheiden mögen. Was ich persönlich oft mache, ist das Mock zum "System-Ping" zu modifizieren, als "is still alive"-Test und so Testsuite als Zielcode übernehme. Dabei ist C++ ein Vorteil, weil der Preis nicht so hoch ist, wie bei Java (s.o.). Wenn die Leute nur die Rosinen poolen, würde kein Bäcker welche verwenden ... 😋

    2. Wenn doch - wie machen wa dat?
    Mein Fazit: Wir haben bei der Testsuite eben eine AOP! D.h. die Mocks müssen wir unterdücken durch libaries, frameworks, headers, makros etc..
    Dafür benötigen wir eben eine Makierung im Zielcode und das hat zur Folge, dass wir unsere Orthogonalität verlieren. IMHO wird dann der Teufel mit dem Beezebub ausgetrieben und ist nicht Intention eines Unittests.


Anmelden zum Antworten