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 der AbstractSystemTime-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 dann RealSystemTime da rein und im Unittest FakeSystemTime.



  • @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 dann RealSystemTime da rein und im Unittest FakeSystemTime.

    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 der AbstractSystemTime-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.cpp

    Header

    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ür 🙂

    Morgen 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 einzigen std:uint64_t und auch RealSystemTime 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-Code get_time_test.o/.obj linken. Da muss man dann für den Testcode auch nicht die Quellen neu kompileren, die get_time verwenden. Header deklariert get_time und ein nur in Testcode eingebundener anderer Header zusätzlich advance_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 😉


Log in to reply