Ansatz für Testen / Mocking in diesem Beispiel



  • Hallo zusammen,

    ich habe einen Ausschnitt einer Klasse:

    class IOryApi {
    public:
        virtual ~IOryApi() = default;
    
        [[nodiscard]] virtual Session getSession(const std::string& cookies) const = 0;
    };
    
    class OryApi : public IOryApi {
    public:
        explicit OryApi(std::string baseUrl);
    
        [[nodiscard]] Session getSession(const std::string& cookies) const override;
    
    private:
        std::string mBaseUrl;
    };
    

    mit dem dazugehörigen Ausschnitt der Implementierung

    OryApi::OryApi(std::string baseUrl) : mBaseUrl { std::move(baseUrl) } {}
    
    [[nodiscard]] Session OryApi::getSession(const std::string& cookies) const {
        cpr::Response response =
            cpr::Get(cpr::Url { mBaseUrl + "/sessions/whoami" }, cpr::Header { { "Cookie", cookies } });
    
        try {
            return nlohmann::json::parse(response.text).get<Session>();
        } catch(const JsonValidationError& ex) {
            throw std::runtime_error(ex.what());
        }
    }
    

    Im wesentlichen ist es eine Client Library für die Ory Kratos API (identity management service). Die Idee ist die REST Calls gekapselt zu haben und mit ordentlichen Datenstrukturen für Request & Response zu arbeiten. Außerdem ggf. Error handling etc.

    Meine grundsätzliche Annahme ist erstmal, dass Ory Kratos selber korrekt ist und ich das zumindest im Rahmen von UnitTests nicht testen brauche. Aber den restlichen Code würde ich gerne testen. Also z.B. im obigen Fall, ob das Parsing der json Anwort korrekt funktioniert. Code ist jetzt bewusst etwas trivialer gehalten, um nicht von Thema zu sehr abzulenken.

    Es gibt ja mehrere Möglichkeiten tendenziell, aber im Rahmen von Unit Tests ist wohl die sinnvollste einen Mock für die Rest Requests zu haben. Sowas wie alternative Implementierung von cpr::Get bereitstellen und beim Linken dafür sorgen, dass die genutzt wird, finde ich eher unschön. Eher den Service selbst zu mocken als die Requests scheint mir auch tendenziell sehr aufwendig und nicht lohnenswert.

    Die erste Variante, die mir in den Sin kam, war wie ne RawOryApi zu bauen, die halt nix macht, außer dem Request:

     class IOryInternalApi {
    public:
        virtual ~IOryInternalApi () = default;
    
        [[nodiscard]] virtual cpr::Response getSession(const std::string& cookies) const = 0;
    };
    

    mit quasi nur cpr::Get(cpr::Url { mBaseUrl + "/sessions/whoami" }, cpr::Header { { "Cookie", cookies } }).

    Alternativ hatte ich noch die idee, ob es Sinn ergeben könnte eher einen generischen Wrapper um cpr selber zu bauen. Daher eher:

    class IHttpClient {
    public:
        virtual ~IHttpClient () = default;
    
        virtual cpr::Response get(const cpr::Url& url, const cpr::Header& header) const = 0;
    };
    

    Die Dependency Injection mache ich normalerweise über den Konstruktor. Zum Beispiel:

    class OryApi : public IOryApi {
    public:
        explicit OryApi(IOryInternalApi& internalApi);
    
        [[nodiscard]] Session getSession(const std::string& cookies) const override;
    
    private:
        IOryInternalApi& mInternalApi;
    };
    

    Das sieht imo schon komplett doof hier aus. Es mag an schlechter Benennung liegen, aber etwas Internes per Konstruktor reingeben? Bei anderen Beispielen scheint es irgendwie mehr Sinn zu ergeben, weil es dann tatsächlich eine richtige Dependency ist (z.B. Klasse X bekommt eine Datenbank Connection o.ä.). Hier wirkt es halt völlig konstruiert.
    Mit dem IHttpClient würde es mir schon etwas logischer erscheinen. Das wäre dann ähnlich wie beim Datenbank Beispiel.

    Grundsätzlich bleibt auch das Problem, dass natürlich die aufrufende Klasse nicht nur OryApi konstruieren muss, sondern auch dann die Dependency halten muss. Auch das in dem Fall wieder sehr komisch mit IOryInternalApi. Weniger komisch mit IHttpClient subjektiv, weil das Objekt nicht mehr wie ein Implementationsdetail von OryApi wirkt. Außerdem wäre das Problem wohl in jedem Fall auch mit einem Dependency Injection Framework lösbar.

    Wie sehr ihr das bzw. würdet ihr das lösen? Gerne auch ein paar Kommentare zu den vorgeschlagenen Sachen hier. Mich würde schon interessieren, ob das nur mit völlig konstruiert vor kommt oder ob es das tatsächlich auch ist 😄



  • Ist meine Frage / Problem unverständlich? Oder hat da einfach keiner ne sinnvolle Lösung für? 😅


  • Mod

    @Leon0402 sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    Ist meine Frage / Problem unverständlich? Oder hat da einfach keiner ne sinnvolle Lösung für? 😅

    Deine Frage ist schon gut gestellt. Das Problem dürfte eher sein, dass es eine schwierige Frage zu einem Thema ist, mit dem sich nur wenige auskennen.



  • @SeppJ sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    @Leon0402 sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    Ist meine Frage / Problem unverständlich? Oder hat da einfach keiner ne sinnvolle Lösung für? 😅

    Deine Frage ist schon gut gestellt. Das Problem dürfte eher sein, dass es eine schwierige Frage zu einem Thema ist, mit dem sich nur wenige auskennen.

    Echt? Meine Frage kommt mir jetzt eig. nach einem nicht so ugewöhnlichen Problem im Bereich Testing / Mocking vor. Bin irgendwie davon ausgegangen, dass ihr alle solche Probleme tagtäglich löst 😃

    Oder machen die meisten hier einfach gar keine Tests? 😃 Das habe ich mich schon häufiger gefragt, da Interfaces in C++ den meisten eher wie ein Fremdwort erscheint. Und die sind ja schon relativ notwendig, um testen zu können (gibt natürlich auch andere Wege, aber die finde ich bisher eher schlechter als besser).



  • @Leon0402 sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    Das habe ich mich schon häufiger gefragt, da Interfaces in C++ den meisten eher wie ein Fremdwort erscheint. Und die sind ja schon relativ notwendig, um testen zu können (gibt natürlich auch andere Wege, aber die finde ich bisher eher schlechter als besser).

    Ich komme aus der Embedded-CAD Ecke und Interfaces sind in der Embedded Welt öfters Header Dateien, ähnlich der Header Datei von Windows C DLLs.

    Und das hat sich bei mir etwas nachgezogen. Ich nutze viele Berechnungsfunktionen und sehr viele sind einfache Funktionen in einer einzelnen Datei oder in einem gefühlten C++ Modul. Die Projektgrößen sind halt auch nicht riesig, weniger als 200k LOC. Von daher sind meine Low-Level-Testfunktion auch relativ einfach ohne ein Design. Ich habe es bisher auch nicht benötigt.

    Auf High-Level Ebene nutze ich, sofern möglich, einen "Eingabe -> Program -> Ausgabe(datei)" Test, meistens steuerbar über einen Kommandozeilenparameter.

    Von daher habe ich C++ Interfaces in Tests noch gar nicht benötigt.



  • @Quiche-Lorraine Ah okay das ergibt natürlich Sinn. Ich habe da eher die Interfaces wie vlt. aus Java bekannt im Kopf. Also eher abstrakte Klassen mit pure virtual Methoden in C++. Die nutze ich häufiger als der Durchschnitts C++ Entwickler (aus meiner subjektiven Wahrnehmung heraus), da man damit dann z.B. Klassen Mocken kann kann mit GMock u.ä.

    Allerdings gibt es in C++ ein paar entscheidene Nachteile gegenüber Java in der Hinsicht:

    • Es gibt keine expliziten Interfaces. Das ist dann weniger lesbar insbesondere in Bezug mit Mehrfachvererbung
    • Polymorphie kann standardmäßig nicht immer verwendet werden. Man muss halt explizit Pointer, Referenzen etc. nutzen, was einem (oder zumindest mir) immer komisch vorkommt.
    • Probleme mit Templates etc. (die nicht virtual sein können)
    • Virtual hat Performance Overhead, man kann es also in C++ explizit kontrollieren. An sich natürlich positiv, aber das führt dann natürlich zur Einstellung: "Das ist nicht performant, das mache ich nicht"
      ...

    Schätze mal das sind einige der Gründe, warum das in C++ alles nicht so populär ist. Das C++ häufig embedded etc. eingesetzt wird, ist aber auch wesentlich.



  • Ich würde vermutlich über Konstruktor-Injection den IHttpClient reingeben - aber nicht als Referenz sondern als Smart-Pointer. Also unique_ptr, oder, wenn es Sinn macht dass sich mehrere Objekte den selben IHttpClient teilen, dann als shared_ptr.

    Dadurch entfällt die Notwendigkeit für den Client-Code die Lifetime des IHttpClient zu managen. Einzig das Erzeugen des OryApi Objekts wird dadurch minimal komplizierter. Falls das ein Thema ist, z.B. weil OryApi Objekte an vielen Stellen im Programm erzeugt werden, dann kannst du auch einfach einen alternativen Konstruktor für "nicht-test" Szenarien machen, der sich den IHttpClient selbst erzeugt.



  • @hustbaer sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    Ich würde vermutlich über Konstruktor-Injection den IHttpClient reingeben - aber nicht als Referenz sondern als Smart-Pointer. Also unique_ptr, oder, wenn es Sinn macht dass sich mehrere Objekte den selben IHttpClient teilen, dann als shared_ptr.

    Dadurch entfällt die Notwendigkeit für den Client-Code die Lifetime des IHttpClient zu managen. Einzig das Erzeugen des OryApi Objekts wird dadurch minimal komplizierter. Falls das ein Thema ist, z.B. weil OryApi Objekte an vielen Stellen im Programm erzeugt werden, dann kannst du auch einfach einen alternativen Konstruktor für "nicht-test" Szenarien machen, der sich den IHttpClient selbst erzeugt.

    Den unique_ptr in dem Fall in die Klasse moven? Da hatte ich drüber nachgedacht, ob das sinnvoll sein könnte. In den Tests müsste ich dann ggf. den unique ptr erstellen und mir das Objekt vorher aus dem ptr holen, weil durch das moven der ptr ja zurückgesetzt werden würde:

    std::unique_ptr<IHttpClient> client = std::make_unqique<HttpClientMock>();
    IHttpClient*  clientCopy = client .get();
    OryApi  api {client };
    // client  jetzt nullptr, daher mit clientCopy weiterarbeiten
    

    Hoffe das passt so syntax mäßig, hab es nicht getestet. Sieht schon ein wenig komisch aus das ganze.

    Das schöne an der Referenz ist halt, dass man nicht unbedingt die Objekte auf dem Heap anlegen muss.

    HttpClientMock mock {};
    OryApi  api { mock };
    

    Aber ja mit der Lifetime ist halt nerviger, wenn das api Object jetzt irgendwo gespeichert werden würde.

    Hab da verschiedene Quellen schon zu dem Thema jetzt gelesen mit verschiedenen Meinungen, ob unique ptr, shared ptr, normaler ptr oder Referenz verwendet werden sollte. Irgendwie haben alle Varianten mindestens einen Nachteil.



  • @Leon0402 sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    Den unique_ptr in dem Fall in die Klasse moven?

    Ja, genau.

    Da hatte ich drüber nachgedacht, ob das sinnvoll sein könnte. In den Tests müsste ich dann ggf. den unique ptr erstellen und mir das Objekt vorher aus dem ptr holen, weil durch das moven der ptr ja zurückgesetzt werden würde:

    Wenn du den Mock für Assertions brauchst und unique_ptr verwendest, dann müsstest du es so machen. Ich hab einige Tests die genau das machen. Finde ich nicht schlimm - so lange man sowas nur in Tests macht. Sobald man es auch in Produktivcode braucht, würde ich statt dessen lieber shared_ptr verwenden.

    Das schöne an der Referenz ist halt, dass man nicht unbedingt die Objekte auf dem Heap anlegen muss.

    HttpClientMock mock {};
    OryApi  api { mock };
    

    Ja, klar.

    Aber ja mit der Lifetime ist halt nerviger, wenn das api Object jetzt irgendwo gespeichert werden würde.

    Hab da verschiedene Quellen schon zu dem Thema jetzt gelesen mit verschiedenen Meinungen, ob unique ptr, shared ptr, normaler ptr oder Referenz verwendet werden sollte. Irgendwie haben alle Varianten mindestens einen Nachteil.

    Ja, da gibt's viele verschiedene Meinungen dazu. Ein Vorteil von Smart-Pointern ist halt, dass man Verwendung des Objekts und Erstellen/Konfigurieren des Objekts dadurch nicht unnötig koppelt. Also ich kann irgendwo eine Factory makeFoo bauen die einen Smart-Pointer auf Foo gibt. Und jemand anderes kann den Smart-Pointer auf Foo dann halten und verwenden so lange er will. Dabei muss er nicht wissen wie der erzeugt wurde und was für Dependencies dafür am Leben gehalten werden müssen - und wie.

    Ob das ein nennenswerter Vorteil ist, hängt natürlich von vielen Faktoren ab.


    Wovon ich allerdings universell abraten würde, ist hierfür syntaktisch Referenzen zu verwenden. Meine Regel ist: wenn ein Konstruktor oder auch eine andere Funktion sich die Adresse eines Objekts merkt (bzw. an etwas weiterleitet was sich diese Adresse merkt), so dass das Objekt über die Dauer des Funktionsaufrufs hinaus gültig bleiben muss, dann verwende ich keine Referenzen - sondern Zeiger.

    Also ich verwende den Typ-Unterschied Referenz/Zeiger als Hinweis darauf ob man sich evtl. um die Lifetime des übergebenen Objekts kümmern muss.

    Anders gesagt: wenn eine Referenz übergeben wird, dann sollte es immer OK sein ein Temporary zu übergeben bzw. das übergebene Objekt sofort nach dem Aufruf zu löschen.



  • @hustbaer sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    Wovon ich allerdings universell abraten würde, ist hierfür syntaktisch Referenzen zu verwenden. Meine Regel ist: wenn ein Konstruktor oder auch eine andere Funktion sich die Adresse eines Objekts merkt (bzw. an etwas weiterleitet was sich diese Adresse merkt), so dass das Objekt über die Dauer des Funktionsaufrufs hinaus gültig bleiben muss, dann verwende ich keine Referenzen - sondern Zeiger.
    Also ich verwende den Typ-Unterschied Referenz/Zeiger als Hinweis darauf ob man sich evtl. um die Lifetime des übergebenen Objekts kümmern muss.
    Anders gesagt: wenn eine Referenz übergeben wird, dann sollte es immer OK sein ein Temporary zu übergeben bzw. das übergebene Objekt sofort nach dem Aufruf zu löschen.

    Und du dokumentierst dann einfach, dass kein nullptr übergeben werden darf?

    Verwendest du überhaupt referenzen bei membern dann? Wann würdest du diese einsetzen?



  • @Leon0402 sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    Und du dokumentierst dann einfach, dass kein nullptr übergeben werden darf?

    Sorry für die späte Antwort.

    Jain. Nur wenn es nicht offensichtlich ist dass der Parameter nicht optional ist. Wobei es vermutlich gut wäre es immer zu machen - ein "must not be null" is ja wirklich schnell geschrieben.

    Verwendest du überhaupt referenzen bei membern dann?

    Nein.

    Wann würdest du diese einsetzen?

    Mir fällt gerade kein Fall wo ich Member mit Reference-Type für ne gute Idee halten würde.



  • Hallo Leon

    Ich bin witzerweise auch gerade dabei diese Art von Dependency aus meinen Klassen in einem großen Projekt zu entfernen, weil ich die Unittests verbessern will und autonome Klassen dafür angenehmer sind.

    Szenario:
    MyCoolModule enthält die ganze Logik und tut im Grunde die ganze Arbeit, hat aber selbst nichts mit Netzwerk oder anderen Dingen zu tun, benötigt aber eine Hilfsklasse die diese Netzwerk-Geschichten abdeckt.

    Vorher

    class MyCoolModule
    {
    public:
         MyCoolModule( MyConnection &conn ) : Connection( conn ) {}
         
        void doSomething() { Connection.sendData(); }
    }
    

    Lässt sich blöd testen, denn oft will man im Unittest gar nichts mit MyConnection zu tun haben, aus unterschiedlichen Gründen.

    Nachher:

    class MyCoolModule
    {
    public:
         MyCoolModule() {}
         
        void doSomething() { sendData(); }
    
    protected:
        virtual void sendData() = 0;
    }
    

    Im Unittest dann überladen in der Form:

    class TestMyCoolModule : public MyCoolModule
    {
    protected:
        void sendData() override { SendWasCalledInTest = true; } // Dont really send data here, just make sure it was called
    }
    

    Im Produktionscode:

    class ConnectionAndMyCoolModule : public MyCoolModule
    {
    protected:
        void sendData() override { Connection.sendData() } // Data is sent.
    
    private:
       MyConnection Connection;
    }
    

    Die Vorgehensweise ist nicht für jedes Szenario geeignet, aber in manchen Fällen entsorgt man auf diese Art und Weise eine Menge Dependencies und bekommt Klasse die angenehmer zu testen sind.

    Bin mir nicht sicher ob das genau auf deine Frage passt, wollte das aber trotzdem mal vorschlagen 🙂



  • @It0101 So teste ich teilweise auch meinen Code. Wobei ich das eher zu vermeiden versuche. Habe ich bisher nur verwendet, wenn der Code getestet werden soll, aber gleichzeitig aus Gründen nicht komplett refactored werden darf 😅

    Oben handelt es sich eher um ein Hobby Projekt, da kann ich machen was ich will. Da würde ich das gerne vermeiden. Schon alleine, weil es doof aussieht, wenn man gegen eine Subklasse teste 😃



  • @It0101
    Ich mag es überhaupt nicht Klassen non-final zu machen und ihnen virtuelle Funktionen zu spendieren nur damit man sie testen kann.

    Das von dir gezeigte Beispiel war mMn. definitiv vorher besser. Wenn man da etwas virtuell/abstrakt machen sollte, dann das MyConnection Ding was an MyCoolModule übergeben wird. Dann kann man das auch wieder schön testen:

    // AbstractConnection.h
    class AbstractConnection
    {
    public:
        virtual void sendData() = 0;
    };
    
    // MyConnection.h
    class MyConnection final : public AbstractConnection
    {
    public:
        void sendData() final;
        // ...
    };
    
    // MyCoolModule.h
    class MyCoolModule final
    {
    public:
         MyCoolModule( AbstractConnection &conn ) : Connection( conn ) {}
         
        void doSomething() { Connection.sendData(); }
        // ...
    };
    

    Im Test kann man dann einen Mock erstellen der von AbstractConnection abgeleitet ist. Ist etwas mehr zu tippen, aber nicht wirklich schlimm. Mit Frameworks wie Google-Test ist das Erstellen von Test-Mocks auch relativ einfach. Wobei ich auch handgeschriebenen Mocks nicht abgeneigt bin.

    IMO viel viel besser als die Kapselung von MyCoolModule unnötigerweise aufzuweichen.



  • @hustbaer sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    @It0101
    Ich mag es überhaupt nicht Klassen non-final zu machen und ihnen virtuelle Funktionen zu spendieren nur damit man sie testen kann.

    Das von dir gezeigte Beispiel war mMn. definitiv vorher besser. Wenn man da etwas virtuell/abstrakt machen sollte, dann das MyConnection Ding was an MyCoolModule übergeben wird. Dann kann man das auch wieder schön testen:

    // AbstractConnection.h
    class AbstractConnection
    {
    public:
        virtual void sendData() = 0;
    };
    
    // MyConnection.h
    class MyConnection final : public AbstractConnection
    {
    public:
        void sendData() final;
        // ...
    };
    
    // MyCoolModule.h
    class MyCoolModule final
    {
    public:
         MyCoolModule( AbstractConnection &conn ) : Connection( conn ) {}
         
        void doSomething() { Connection.sendData(); }
        // ...
    };
    

    Im Test kann man dann einen Mock erstellen der von AbstractConnection abgeleitet ist. Ist etwas mehr zu tippen, aber nicht wirklich schlimm. Mit Frameworks wie Google-Test ist das Erstellen von Test-Mocks auch relativ einfach. Wobei ich auch handgeschriebenen Mocks nicht abgeneigt bin.

    IMO viel viel besser als die Kapselung von MyCoolModule unnötigerweise aufzuweichen.

    Nutzt du jetzt doch in dem Beispiel einen Reference Member? 😯

    Aber ja Zustimmung, das ist die grundsätzlich saubere Variante aus meiner Sicht. Allerdings ist dependency injection ja auch nicht unbedingt völlig trivial wie man an dem Thread hier durchaus sieht. Im Zweifelsfall also lieber so testen als gar nicht testen 😃



  • @Leon0402 sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    Nutzt du jetzt doch in dem Beispiel einen Reference Member? 😯

    Ich hab das Beispiel ja nicht geschrieben, ich hab es bloss angepasst. Und ich hab mich darauf beschränkt nur den Teil anzupassen um den es mir gerade ging. Weil wenn man zeigen will wie man eine ganze bestimmte Änderung macht, ist es mMn. sinnvoller sich auf genau diese eine Änderung zu beschränken, anstatt gleichzeitig noch andere Dinge zu ändern.

    Aber ja Zustimmung, das ist die grundsätzlich saubere Variante aus meiner Sicht. Allerdings ist dependency injection ja auch nicht unbedingt völlig trivial wie man an dem Thread hier durchaus sieht. Im Zweifelsfall also lieber so testen als gar nicht testen 😃

    Boah, ja, weiss nicht. Wenn es nur die beiden Möglichkeiten gäbe, vermutlich. Ich wollte halt eine dritte Möglichkeit aufzeigen die mMn. noch besser ist.



  • Ich finde den Ansatz, dass du ein IHttpClient erstellst und diesen per Konstruktor-Injection reingibst am sinnvollsten. Der HttpClient-Mock bekommt dann eine URL als Request und gibt dann eine Json zurück, wo du dann testen kannst, ob es richtig geparst wird.

    Was mir in dem Zusammenhang noch auffällt. Besteht die Aufgabe der zu testenden Klasse daran zu wissen, welche URLs er aufrufen muss und welche Felder er aus der JSON-Antwort parsen muss? Oder hat diese Klasse zwei Aufgaben: URL+Aufruf und JSON-Zu-Session-Objekt-Umwandlung?

    In dein Beispiel erzeugst du aus einer JSON-Antwort ein Session-Objekt und mir ist jetzt nicht klar, ob diese Umwandlung eines JSON-Text in ein Session-Objekt nun Bestandteil von deiner zu testenden Klasse ist oder ob das eine externe Klasse macht.

    Ich will darauf hinaus, das jede Klasse nur eine Aufgabe machen sollte. So eine JSON-To-Session-Umwandlung klingt nach etwas, was raus kann. Wenn du dafür dann eine eigene Klasse hast, kannst du diese dann auch leichter testen und deine Klasse, welche die Http-Requests macht wird dann auch schlanker/leichter testbar.



  • @XMAMan Das parsen in json passiert mehr oder weniger automatisch mit reflection. Also ich habe nur die Zeile:

    nlohmann::json::parse(response.text).get<Session>();
    

    Und die Session sieht z.B. so aus:

    struct Session {
        struct Identity {
            std::string id;
    
            inline static const nlohmann::json schema = R"(
                {
                    "$schema": "http://json-schema.org/draft-07/schema#",
                    "title": "Identity",
                    "type": "object",
                    "properties": {
                        "id": {
                            "type": "string",
                            "format": "uuid"
                        }
                    },
                    "required": ["id"]
                }
            )"_json;
    
            DEFINE_API_TYPE(Identity, id)
        };
    
        std::string id;
        Identity identity;
    
        inline static const nlohmann::json schema = R"(
            {
                "$schema": "http://json-schema.org/draft-07/schema#",
                "title": "Session",
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "format": "uuid"
                    },
                    "identity": {
                        "type": "object"
                    }
                },
                "required": ["id", "identity"]
            }
        )"_json;
    
        DEFINE_API_TYPE(Session, id, identity)
    };
    

    Aus Gründen schlägt hier übrigens immer das Coloring schief, in meiner Entwicklungsumgebung auch 😃 Sieht mir aber eigentlich alles korrekt aus.

    Das Macro DEFINE_API_TYPE definiert dann automatisch diverse methoden zum parsen von json etc. mit validierung, vergleichen von Objekten etc.

    Inwiefern ich die Klasse teste weiß ich noch nicht genau. Theoretich wird zumindest implizit einige Fälle gestetet: Erfolgreiche Umwandlung und ein beliebiger json Fehlerfall. Alle Fälle durchzutesten klingt mehr sehr mühselig. Das wäre vermutlich so ein Fall für so ein Test Framework, die jeglichen beliebigen Input auf dich schmeißen und wo man die Test Fälle nicht direkt so selber definieren muss.

    @XMAMan sagte in Ansatz für Testen / Mocking in diesem Beispiel:

    Ich finde den Ansatz, dass du ein IHttpClient erstellst und diesen per Konstruktor-Injection reingibst am sinnvollsten. Der HttpClient-Mock bekommt dann eine URL als Request und gibt dann eine Json zurück, wo du dann testen kannst, ob es richtig geparst wird.

    Ich fand erstmal auch, dass das am sinnvollsten klingt. Mir ist allerdings dann aufgefallen, dass das auch nur so bedingt Spaß macht. Im Grunde ist die Idee der Library ja, dass man die HTTP Methoden als C++ Methoden machen kann und dann seinen Request beliebig zusammenbauen kann:

    template <typename... Ts>
    Response Get(Ts&&... ts) {
        Session session;
        priv::set_option(session, std::forward<Ts>(ts)...);
        return session.Get();
    }
    

    Siehe: https://github.com/libcpr/cpr/blob/master/include/cpr/api.h#L118

    Hier ein Beispiel aus den docs für nen GET:

    cpr::Response r = cpr::Get(cpr::Url{"https://api.github.com/repos/libcpr/cpr/contributors"},
                          cpr::Authentication{"user", "pass", cpr::AuthMode::BASIC},
                          cpr::Parameters{{"anon", "true"}, {"key", "value"}});
    

    Diese Flexibilität kann ich soweit ich das sehe nicht in meinem IHttpClient bieten. Weil template Methoden dürfen ja nicht virtual sein. Das heißt ich muss ne spezielle Instanzierung in meinem Interface haben. Also z.B:

    virtual cpr::Response get(const cpr::Url& url, const cpr::Header& header) const = 0;
    

    In dem Fall muss man jetzt ne URL übergeben (ok das sollte eh immer gegeben sein), man muss aber auch nen Header übergeben. Man darf nichts anderes wie Parameter übergeben etc. Wenn ich also jetzt ne zweite Klasse habe mit ner anderen API oder andere GET Requests mache ist dann die Frage, ob ich dann 10 verschiedene solcher get wrapper schreiben muss für die verschiedenen Anwendungsfälle.
    Also baue ich doch letzendes einen Wrapper um CPR, aber mit nem deutlich schlechteren Interface 😃 Und trotz schlechterem Interface ist dann CPR nicht mal gekapselt. Weil die Objekte von cpr leaken ja trotzdem nach draußen. Also habe ich gefühlt keine Vorteile und jede Menge Nachteile.

    Aktuell habe ich die erste Lösung mit nem "spezialisierten" HTTP Client, daher mit konkreten Methoden für die Endpoints:

    class IOryHttpClient {
    public:
        virtual ~IOryHttpClient() = default;
    
        [[nodiscard]] virtual cpr::Response getSession(const std::string& cookies) const = 0;
    };
    
    class OryHttpClient : public IOryHttpClient {
    public:
        explicit OryHttpClient(std::string baseUrl);
    
        [[nodiscard]] cpr::Response getSession(const std::string& cookies) const override;
    
    private:
        std::string mBaseUrl;
    };
    

    Das schien mir dann erstmal das beste.

    Aber wenn ihr das weitere oder andere Ideen habt, gerne her damit. Das muss nicht das Endresult bleiben 🙂



  • Das die CPR-Klasse mit ein Template arbeitet und deswegen nicht virtuell sein darf wusste ich nicht. In diesen Falle halte ich die Idee, das man eine Klasse erstellt, die CPR ohne Template in spezialisierter Weise (also dein OryHttpClient) anbietet wirklich aktuell für das Beste.

    Wenn es viele verschiedene CPR-Nutzungen gebe, wo jeder ein anderen Template-Parameter nutzt, dann könnte man ja überlegen, wie man das dann macht, dass man nicht für jede Ausprägung eine eigene Klasse braucht.


Anmelden zum Antworten