Build- und Designproblem: Mocks für Unit-Tests
-
Hallo zusammen. Ich weiß selbst nicht zu 100% ob das hier wirklich eine Standard C++-Frage ist, da es doch ein wenig in den Build-Prozess geht. Da der allerdings auch im Standard mit drin ist und es nicht um einen speziellen Compiler geht passts denke ich.
Folgende Situation: Ich schreibe an einem Framework, das aus mehreren Schichten/Modulen bestehen soll, die aufeinander aufbauen, d.h. Schicht A besitzt/benutzt Objekte von Schicht B usw.
In meinen Unit-Tests sollen sowohl mehrere/alle Module zusammen als auch einzelne Module getestet werden. Grade für die einzelnen Module möchte ich zum Testen Mocks oder Fake-Klassen verwenden, die die Interfaces der anderen Module simulieren und ihre Verwendung prüfen.
Jetzt kommen einige Schwierigkeiten:
- Die Module erzeugen sich gegenseitig und werden nicht "außen" erzeugt und dann zusammengepappt. Das heißt, ich kann Modul A nicht ein B_Mock an Stelle eines B übergeben.
- Der Framewok-Code sollte möglichst keine Änderungen enthalten, die nur durch die Tests nötig sind. Darunter fallen vor allem irgendwelche #ifdefs etc.Folgende Ideen habe ich dazu bisher, vielleicht sieht jemand bessere Möglichkeiten:
-
Ich könnte die Mock-Klassen genauso nennen wie die echten Klassen. D.h. BMock.cpp und B.cpp implementieren beide class B, die in B.h definiert wird. Ich kann so für die Tests, die Modul A mit dem Mock für Modul B testen sollen, einfach A.o, BMock.o und die entsprechenden test.o zusammenlinken. Der Nachteil ist, dass ich für die Tests, die Modul B testen (etl. mit anderen Mocks), separate Testprogramme linken muss, da BMock.o und B.o nicht zusammen gelinkt werden können.
-
Ich könnte die Interface-Klassen der Module (also die, die von anderen Modulen verwendet werden), virtuell machen und zur Erzeugung Factories verwenden. A würde also ein B* haben und BFactory::create() aufrufen. Mittels Methode 1) könnte ich für das Testprogramm die Factories so ersetzen, dass ich sie bei Bedarf umschalten kann, damit sie Mocks statt der echten Modulklassen produzieren. Der Nachteil ist, dass ich an einigen Modulgrenzen nur für die Tests virtuelle Funktionen und Factories einführen müsste.
Hat jemand andere Vorschläge?
-
-
Also Methode 1) finde ich viel zu kompilizert. Unit Tests sollten so einfach und verständlich wie möglich sein. Mit solchen fiesen Tricks zu arbeiten führt dazu, dass irgendwann irgendwer die Tests nicht mehr versteht und nicht mehr traut. Ein Test, dem ich nicht trauen kann hat keinen Wert.
Methode 2 ist wohl eigentlich die übliche Vorgehensweise, blos dass die Factories nicht mit Trick 1) ausgetauscht werden, sondern einfach durch eine entsprechende Schnittstelle. Nach meinen Erfahrungen machen Änderungen an einer Klasse um diese testbar zu machen das Design i.d.R. nicht schlechter, eher im Gegenteil. Was ist falsch an einer A::SetFactory(BFactory*)?Es klingt übrigens eher nach Stubs als nach Mocks

-
brotbernd schrieb:
Also Methode 1) finde ich viel zu kompilizert. Unit Tests sollten so einfach und verständlich wie möglich sein. Mit solchen fiesen Tricks zu arbeiten führt dazu, dass irgendwann irgendwer die Tests nicht mehr versteht und nicht mehr traut. Ein Test, dem ich nicht trauen kann hat keinen Wert.
Ist ein Argument.
Was ist falsch an einer A::SetFactory(BFactory*)?
Das Interface von A ist das äußere Interface des ganzen Frameworks. B ist ein innerer Teil davon. Da hat imho eine Erwähnung von B oder irgendwelchen B-Factories im Interface von A nichts verloren. Da könnte ich genauso gut gleich die Factories sein lassen und direkt die verschiedenen B-Instanzen reinstopfen.
Es klingt übrigens eher nach Stubs als nach Mocks

Ich überlege, googlemock zu verwenden, weil die tun was ich möchte (Aufrufe zählen, vorher angegebene Werte zurückliefern,...). Die sagen es seien Mocks

-
Aufrufe zählen, -> Mock
vorher angegebene Werte zurückliefern, -> StubSind natürlich nur Namen und diese werden bestimmt auch unterschiedlich angewendet. Aber ich glaube i.A. werden Mocks und Stubs danach unterschieden, an welchem Objekt dir Verifizierung stattfindet. Bei Mocks wird die Übeprüfung am Mock selber vorgenommen, beim Stub am Testobjekt.
sut.Foo(mock); Assert(mock.Calls == 3); sut.Bar(stub); Assert(sut.Baz() == 42);Vielleicht sagen auch einige, dass Mocks aus einem Mockframework stammen und Stubs was anderes sind, oder was weiß ich. Ohne sich auf Frameworks zu beziehen, finde ich die allg. Unterscheidung jedoch recht sinnvoll.
Zu deinem Problem fällt mir jetzt grad auch keine weitere grandiose Lösung ein ausser dass A seine BFactory über irgendeinen weiteren globalen Service (Singleton?) bezieht, der sich umkonfigurieren lässt. Es ist irgendwo ein Kompromiss zwischen Kapselung und Erweiterbarkeit.
-
@pumuckl:
A auf B zu templatisieren wäre keine Lösung (A<B> für produktiv und A<BMock> zum Testen)?
-
hustbaer schrieb:
@pumuckl:
A auf B zu templatisieren wäre keine Lösung (A<B> für produktiv und A<BMock> zum Testen)?Nein, damit wäre gegenüber void A::SetB(B*) nichts gewonnen. Im Gegenteil, ich müsste im A-Header noch die komplette Implementierung stehen haben, weils ja jetzt ein Template ist. Das würde die Kapselung des Ganzen noch mehr aufbröseln.
brotbernd schrieb:
Aufrufe zählen, -> Mock
vorher angegebene Werte zurückliefern, -> StubSind natürlich nur Namen und diese werden bestimmt auch unterschiedlich angewendet. Aber ich glaube i.A. werden Mocks und Stubs danach unterschieden, an welchem Objekt dir Verifizierung stattfindet. Bei Mocks wird die Übeprüfung am Mock selber vorgenommen, beim Stub am Testobjekt.
Dann ists ein Mock. Am Mock-Objekt werden sowohl die Aufrufe gezählt, als auch die Rückgaben für die erwarteten Aufrufe eingestellt. Je nach Framework wird dann das mock-Objekt bei zu vielen/zu wenigen Aufrufen eine Exception werfen oder bei einer späteren Abfrage einen Fehlerwert zurückliefern.
Zu deinem Problem fällt mir jetzt grad auch keine weitere grandiose Lösung ein ausser dass A seine BFactory über irgendeinen weiteren globalen Service (Singleton?) bezieht, der sich umkonfigurieren lässt. Es ist irgendwo ein Kompromiss zwischen Kapselung und Erweiterbarkeit.
Danke, das klingt gut. Ich hatte sowieso vor, für jede Schicht eine Fassadenklasse zu definieren (es sind aktuell genau 7 Schichten/Module und werden kaum mehr werden), und abgesehen von der obersten Schicht gehören alle nicht zur API des Frameworks. Ich könnte also eine allgemeine Fassaden-Factory definieren, die ebenfalls nicht zur API gehört, der ich aber sagen kann "gib beim nächsten GetXYFacade-Aufruf dieses Mock-Objekt statt der echten Fassade zurück" Das wäre dann eine einzige interne Klasse, deren Interface ich für Testzwecke anpasse. Damit könnte ich leben

Nachtrag: zusätzlich hätte die Fassadenfactory die Möglichkeit, einzelne Fassaden nach dem Decorator-Pattern mit Loggern auszustatten, abhängig von einer Konfigurationsdatei.