System-Wrapper für Produktion und Test
-
Hi zusammen
ich möchte mir für meine Unittests ( die Timing-abhängig sind ) einen Wrapper für die SystemZeit bauen, der in Produktion ( im Einsatz ) anders funktioniert als bei meinen Unitests.
Das sieht aktuell ungefähr so aus:
// Abstrakte Basisklasse fuer die Zeit-Wrapper class AbstractSystemTime { public: AbstractSystemTime() = default; virtual ~AbstractSystemTime() = default; virtual uint64_t currentTime() const = 0; }; // Zeit-Wrapper fuer Produktion class RealSystemTime : public AbstractSystemTime, private NonCopyable { public: virtual uint64_t currentTime() const override { auto now = std::chrono::system_clock::now(); auto duration = now.time_since_epoch(); return std::chrono::duration_cast<std::chrono::milliseconds>( duration ).count(); } }; // Zeit-Wrapper fuer Tests class FakeSystemTime : public AbstractSystemTime { public: FakeSystemTime() = default; FakeSystemTime( uint64_t time_ ) : Time( time_ ) {} virtual ~FakeSystemTime() = default; // Kopieren FakeSystemTime( const FakeSystemTime &other ) : Time( other.Time ) {} FakeSystemTime &operator=( const FakeSystemTime &other ) { Time = other.Time; return *this; } // Bewegen FakeSystemTime( FakeSystemTime && ) = delete; FakeSystemTime &operator=( FakeSystemTime && ) = delete; virtual uint64_t currentTime() const override { return Time; } // Millisekunden addieren FakeSystemTime &operator+=( uint64_t milliseconds_ ) { Time += milliseconds_; return *this; } // Zuweisung von Zahl FakeSystemTime &operator=( uint64_t milliseconds_ ) { Time = milliseconds_; return *this; } // Vergleich bool operator==( const FakeSystemTime &other ) { return currentTime() == other.currentTime(); } bool operator==( uint64_t othertime ) { return currentTime() == othertime; } // Zeit in Millisekunden uint64_t Time = 0; };
Nun ist aber so, dass ich nicht ständig überall Objekte für die Zeitabfrage mitschleppen möchte. Daher wäre der naheliegende Ansatz eine static-Instanz der jeweiligen Zeitklasse zu erzeugen.
Z.b. im SingleTon-Pattern. Ich habe dann ein Modul, welches auf dieses Singleton zugreifen kann und dort die Systemzeit ( egal ob Fake oder Real ) transparent abfragen kann. Die Blackbox-Module kennen dann quasi nur die "AbstractSystemTime" und wissen nicht ob sie gerade eine RealTime oder FakeTime abfragen.Nun stellt sich mir die Frage wie ich ein Singleton in unterschiedlichen Varianten ( im Unittest ein Singleton mit FakeSystemTime, in Produktion/im Einsatz ein Singleton mit RealSystemTime ) implementieren müsste. Ich stehe da etwas auf dem Schlauch Vielleicht ist auch das Singleton nicht der richtige Ansatz. Es handelt sich um eine MultiThreaded-Umgebung.
Jemand ne Idee?
-
Eine möglichkeit wäre es dass kein Singleton im klassischen sinne ist sondern es nur einen statischen Member gibt welche einen pointer auf die Instanz hält.
So ähnlich wie das Qt mit ihren QApplication klassen macht.class AbstractSystemTime { public: static AbstractSystemTime* Instance = nullptr; AbstractSystemTime() { Instance = this; // Eventuell mit nem assert um zu prüfen dass immer nur eine instanz existiert } virtual ~AbstractSystemTime() = default; virtual uint64_t currentTime() const = 0; }; class RealSystemTime : public AbstractSystemTime, private NonCopyable { .... }; class FakeSystemTime : public AbstractSystemTime { .... } // FÜr unit tests int main () { FakeSystemTime timInstance; ... return 0; } // FÜr produktiv code int main () { RealSystemTime timInstance; ... return 0; }
-
Hast du denn in deinem Projekt nicht eine Konfigurationsklasse o.ä., in welcher du dieses
AbstractSystemTime
-Objekt unterbringen kannst (selbstverständlich als Zeiger bzw. Smartpointer) und dann passend zuweist.Ansonsten eine globale Variable in einem eigenen Namensbereich (oder aber alternativ als
static
in derAbstractSystemTime
-Klasse).Ein Singleton funktioniert ja direkt nicht, da dieses ja immer über den Klassennamen angesprochen wird.
-
Ich bin kein großer Freund vom Singleton. Zum einen, weil man Abhängigkeiten nicht mehr von außen sieht und eben wegen der Testbarkeit von abhängigem Code.
Du schreibst zwar, das du keine Lust hast, überall die Objekte mitzuschleppen, aber genau das (aka Dependencie Injection) würde ich wahrscheinlich machen. Kann ja auch überall das selbe Objekt via Referenz sein, was da rum gereicht wird. Dann gibt es auch nur eine Instanz, nur das die nicht global Verfügbar ist.Die, von der Zeit abhängigen, Teile bekommen dann eine Referenz auf
AbstractSystemTime
. Im Produktionscode schiebst du dannRealSystemTime
da rein und im UnittestFakeSystemTime
.
-
@Schlangenmensch sagte in System-Wrapper für Produktion und Test:
Die, von der Zeit abhängigen, Teile bekommen dann eine Referenz auf
AbstractSystemTime
. Im Produktionscode schiebst du dannRealSystemTime
da rein und im UnittestFakeSystemTime
.Das wäre auch aus meiner Sicht die "sauberste" Lösung, aber es sind eben recht viele einzelne Klasse, die einzeln abgetestet werden und es wäre eben ziemlich nervig überall eine Referenz mitzuschleppen. Daher wollte ich eben prüfen ob es Alternativen gibt. Aber grundsätzlich bin ich bei dir: die Abhängigkeiten sollte immer offensichtlich sein.
@Th69 sagte in System-Wrapper für Produktion und Test:
Hast du denn in deinem Projekt nicht eine Konfigurationsklasse o.ä., in welcher du dieses
AbstractSystemTime
-Objekt unterbringen kannst (selbstverständlich als Zeiger bzw. Smartpointer) und dann passend zuweist.Ansonsten eine globale Variable in einem eigenen Namensbereich (oder aber alternativ als
static
in derAbstractSystemTime
-Klasse).Ein Singleton funktioniert ja direkt nicht, da dieses ja immer über den Klassennamen angesprochen wird.
Eine Konfigurationsklasse habe ich schon. Nur eben nicht für jede Klasse. Manche Klasse sind so klein, dass sie dann eben nur eine Eigenschaft haben, so dass eine PropertiesKlasse dort totaler Overhead wäre.
Ich probiere mal den Vorschlag von @firefly.
-
Hier mal der relevanten parts wie das in Qt implementiert ist adaptiert an AbstractSystemTime
Quelle:
https://code.qt.io/cgit/qt/qtbase.git/tree/src/corelib/kernel/qcoreapplication.h
https://code.qt.io/cgit/qt/qtbase.git/tree/src/corelib/kernel/qcoreapplication.cppHeader
class AbstractSystemTime { public: AbstractSystemTime(); ~AbstractSystemTime(); static AbstractSystemTime* instance(){return self;} private: static AbstractSystemTime *self; };
cpp
AbstractSystemTime* AbstractSystemTime::self = nullptr; AbstractSystemTime::AbstractSystemTime() { assert(!self, "there should only one SystemTime object") self = this; } AbstractSystemTime::~AbstractSystemTime() { self = nullptr; }
-
@firefly
Bei einer ähnlichen Variante bin ich jetzt auch rausgekommen. Die FakeSystemTime musste ich entsprechend dann auch noch "NonCopyAble" machen. Die Initialisierung der static war mir noch nicht ganz klar. Danke dafürMorgen teste ich das mal intensiv und berichte dann noch, wie sich das anfühlt.
-
Sollte es nicht reichen AbstractSystemTime als "NonCopyAble" zu machen?
Oder soll es auch mal SystemTime varianten geben die klopierbar sein sollen?
-
Nur um noch eine mögliche Alternative in den Raum zu werfen: C++ ist Multi-paradigm. Nicht jedes Problem muss unbedingt mit einer mit einer (polymorphen) Klasse erschlagen werden.
FakeSystemTime
ist ja schon einiges an Boilerplate um einen einzigenstd:uint64_t
und auchRealSystemTime
kapselt lediglich einen System-Call. Es mag durchaus gute Gründe geben, das in ein Klassenobjekt zu kapseln, aber wenn die eh nur im Singleton-Kontext verwendet wird, dann reicht eventuell auch bereits sowas banales hier:namespace { std::atomic<std::uint64_t> fake_time; } auto get_system_time() -> std::uint64_t; auto get_fake_time() -> std::uint64_t; void advance_fake_time(std::uint64_t); auto get_time() -> std::uint64_t { if (use_fake) // [[unlikely]] (C++20), oder auch 'if constexpr' return get_fake_time(); else return get_system_time(); }
... nur so als Anregung. Ob das Sinn macht, musst du selbst entscheiden und hängt natürlich stark davon ab, wie die Zeit in deinem Code verwendet wird - z.B. auch ob irgenwelcher (evtl. externer) Code unbedingt solche Zeitobjekte braucht.
-
@Finnegan sagte in System-Wrapper für Produktion und Test:
... nur so als Anregung. Ob das Sinn macht, musst du selbst entscheiden und hängt natürlich stark davon ab, wie die Zeit in deinem Code verwendet wird - z.B. auch ob irgenwelcher (evtl. externer) Code unbedingt solche Zeitobjekte braucht.
An sowas in der Richtung hatte ich auch schon gedacht. Einfach eine Weiche die entweder die reale Zeit liefert oder meine manipulierbare Test-Zeit.
-
@firefly sagte in System-Wrapper für Produktion und Test:
Sollte es nicht reichen AbstractSystemTime als "NonCopyAble" zu machen?
Oder soll es auch mal SystemTime varianten geben die klopierbar sein sollen?nein, soll es nicht geben. Du hast vollkommen recht.
Vielen Dank an euch alle für eure guten Vorschläge
-
Mit dem Vorschlag von @Finnegan hat man die Weiche fest im Produktionscode. Damit kann man vlt. leben, finde ich aber auch nicht so schön
Über die Ableitung kann man das besser Trennen, der eine Teil im Testcode und der andere in Produktion.
-
@Schlangenmensch sagte in System-Wrapper für Produktion und Test:
Mit dem Vorschlag von @Finnegan hat man die Weiche fest im Produktionscode. Damit kann man vlt. leben, finde ich aber auch nicht so schön
Über die Ableitung kann man das besser Trennen, der eine Teil im Testcode und der andere in Produktion.Die Weiche ist dann tatsächlich fest im Produktionscode, aber das Risiko ist überschaubar. Ich würde die Weiche dann default auch auf "Produktion" stellen und nur im Test anders.
-
Man könnte das über nen funktionszeiger lösen, wodurch der fake code nicht mehr teil des produktion code wäre.
Verändern des funktionszeigers wäre dann über eine methode möglich, welche im falle eines nullptrs dann den internen funktionszeiger wieder auf den poduktion version umstellen kann
-
Man kann auch sowas hier machen, wenn man das strikt trennen will:
get_time.hpp:
auto get_time() -> std::uint64_t;
get_time_production.cpp:
auto get_time() -> std::uint64_t { ... system_time ... }
get_time_test.cpp:
namespace { std::atomic<std::uint64_t> fake_time; } void advance_fake_time(std::uint64_t ms) { fake_time.fetch_add(ms); } auto get_time() -> std::uint64_t { return fake_time.load(); }
... und dann für Produktionscode
get_time_production.o
/.obj
und für Test-Codeget_time_test.o
/.obj
linken. Da muss man dann für den Testcode auch nicht die Quellen neu kompileren, dieget_time
verwenden. Header deklariertget_time
und ein nur in Testcode eingebundener anderer Header zusätzlichadvance_fake_time
(oder man haut die Deklaration direkt in die Test-.cpp
).
-
Ich habe derweil ein wenig sinniert über die Sinnhaftigkeit meines uint64_t ... Ist das als Timestamp in der heutigen Zeit von Chrono überhaupt noch sinnvoll? Mit Chrono kann man ja auch Zeitintervalle (std::chrono::duration) sehr gut lesbar ( mit Literalen oder std::chrono::seconds ) im Code verwenden. Würde es da Sinn machen, die komplette Zeitsteuerung in der Applikation ( z.B. Überwachung von TimeOuts ) mit Chrono zu machen? Oder wäre das aus eurer Sicht Overhead?
Andererseits ist so ein uint64_t natürlich schon einfach und pflegeleicht... Der Nachteil: Wenn man z.B. ein Delay setzen möchte, muss man die im Server verwendete Zeiteinheit kennen. ( also z.B. Millisekunden ). bei Chrono ist sowas eindeutig.
Man findet irgendwie auf Anhieb nicht sonderlich viel über Chrono im Netz. Die meisten scheinen Chrono für die Laufzeitmessung zu nutzen. now() - now() = duration...
-
Ich kenne die chrono-Implementierungen nicht wirklich im Detail, würde mich aber sehr wundern, wenn das für Zeiträume/Zeitpunkte unter der Haube was großartig anderes als lediglich ein einziger (u)int64_t wäre. Hat halt ein schöneres Interface.
Ich halte es für unwahrscheinlich, dass es da irgendeinen Overhead gibt. Zumindest solange du damit nicht "mehr" machst, als du es mit dem
uint64_t
eh schon tust.Ich glaube mich hat da immer unterbewusst die Textlänge der voll qualifizierten Typen abgeschreckt. Ich kenne keinen wirklich guten Grund, warum ich selbst chrono auch so selten verwende