Fragen zu GMock / trompeloeil



  • Hallo zusammen,

    ich habe ein bisschen nach Libraries gesucht, um Fake zu schreiben und bin dabei vor allem bei GMock / trompeloeil erstmal hängen geblieben. Eher weniger, weil die mich so überzeugt haben, sondern mehr, weil es keine aktiv entwickelten Alternativen zu geben scheint. Oder kennt jemand noch andere, die er empfehlen würde?

    Mit beiden Frameworks sind mir auf Anhieb 2 Fragen aufgekommen:

    Unterstützen die nur Mocks? Ich habe gelernt, dass es Mocks gibt (gegen die direkt mit asserts geprüft wird für Interaktionen) und Stubs (liefern nur Rückgabewerte, d.h. sind nur Input und es wird nicht direkt gegen diese getestet). In beiden Libraries und im Internet finde ich immer nur Informationen zu Mocks.
    Man kann zwar scheinbar mit ANY_CALL / ALLOW_CALL sowas auch machen, aber ich frage mich, ob das so vorgesehen ist. Es macht mir eher den Anschein als sollte man Stubs selbst schreiben (was vlt. daran liegt, dass einem das Framework beim Schreiben von Stubs nicht wirklich Arbeit abnimmt und man es deswegen selbst machen soll?)

    Das zweite, was mich etwas verwirrt ist, dass das standard "Arrange Act Assert" pattern wohl nicht unterstützt wird. Denn man muss die Expectations ja immer vor dem Aufruf der Methode setzen. Ist das wirklich so? (Gleichzeitig führt das dazu in den Beispielen von gmock die ich gesehen habe, dass öfters mal mehrere Sachen asserted werden ... was ja eig. eher kontraproduktiv ist)



  • @Leon0402 sagte in Fragen zu GMock / trompeloeil:

    Es macht mir eher den Anschein als sollte man Stubs selbst schreiben (was vlt. daran liegt, dass einem das Framework beim Schreiben von Stubs nicht wirklich Arbeit abnimmt und man es deswegen selbst machen soll?)

    Das Framework nimmt dir schon Arbeit ab. z.B. wenn du in einem Test möchtest dass eine Stub-Funktion immer 2 zurückgibt und in einem anderen Test soll sie immer 3 zurückgeben. Mit GMock kannst du das einfach machen, ohne den Code für die Funktion schreiben zu müssen. Du kannst die Funktion sogar so konfigurieren dass sie z.B. beim 1. Aufruf 2 zurückgibt und bei allen folgenden dann 3. Alles ohne Code für die Funktion schreiben zu müssen.

    Assertions darauf zu machen welche Funktionen aufgerufen werden ist auch möglich, aber nicht nötig um GMock zu verwenden.



  • @hustbaer sagte in Fragen zu GMock / trompeloeil:

    Das Framework nimmt dir schon Arbeit ab. z.B. wenn du in einem Test möchtest dass eine Stub-Funktion immer 2 zurückgibt und in einem anderen Test soll sie immer 3 zurückgeben. Mit GMock kannst du das einfach machen, ohne den Code für die Funktion schreiben zu müssen. Du kannst die Funktion sogar so konfigurieren dass sie z.B. beim 1. Aufruf 2 zurückgibt und bei allen folgenden dann 3. Alles ohne Code für die Funktion schreiben zu müssen.
    Assertions darauf zu machen welche Funktionen aufgerufen werden ist auch möglich, aber nicht nötig um GMock zu verwenden.

    Ah super, danke dir! So habe ich das auch letzlich jetzt gemacht. War mir nur erst nicht sicher, ob ich vlt. die Library für andere Zwecke misbrauche. Denn grade in der Dokumentation von gmock wird noch extra erklärt, dass Mock != Stub. Anschließend wird aber immer nur von Mocks geredet (und die lib heißt ja auch gMock). Aber vermutlich wird hier dann doch einfach nur das Wort Mock zu viel verwendet 😃

    Für mein zweites Problem habe ich denke ich auch eine Lösung gefunden. Zumindest bei trompeloeil kann man benannte Erwartungen setzen und dann abprüfen, ob diese eingehalten worden sind.
    https://github.com/rollbear/trompeloeil/blob/master/docs/FAQ.md#query_expectation
    Das ist zwar etwas unschöner als vlt. bei anderen Isolation Frameworks (z.B. NSubstitue), aber ausreichend denke ich.



  • Nach meinen ersten Anläufen mit UnitTests und Mocking habe ich jetzt ein paar Methoden, wo ich nicht so richtig weiß wie ich die teste. Mein Gefühl sagt mir, dass ich sie schlecht geschrieben habe und deswegen nicht weiß wie ich sie testen soll 😃

    Im wesentlichen geht es um freie Funktionen, welche Teil des Command Line Interface sind. Zum Beispiel den User dazu aufzufordern Daten einzugeben. Und mit diesen Daten wird dann irgendwas gemacht

    void createPerson(DatabaseClass database, std::ostream ostream, std::istream istream) {
        auto name = getPersonNameInput(ostream, istream); 
        auto age = getPersonAgeInput(); 
        database.addPerson(Person { name, age }); 
    }
    
    namespace {
    std::string getPersonNameInput(std::ostream ostream, std::istream istream) {
       while(true) {
         ostream << "Enter Person Name";
         std::string name;
         istream >> name; 
    
         if(validName(name)) 
            return name; 
    
         ostream << "Name enthält Nummern oder ist aus irgendeinem Grund nicht valide. Bitte neue Eingabe machen";
       }
    }
    }
    

    Bisschen vereinfacht sieht meine Methode, die wesentlich länger ist so aus. Sie sagt dem user was er eingaben soll, nimmt die Eingabe entgegen, validiert diese, lässt den user ggf. wiederholen. Das macht sie für ein paar Eigenschaften. GGf. sind noch ein bisschen mehr Logik drin "Wollen sie noch ein Eintrag für xyz machen" etc.

    Dinge, die ich also gerne testen will:

    • Landet am Ende in der Datenbank das fertige Objekt (das ist ja eig. die Hauptaufgabe)
    • Funktionieren die Validierungfunktionen
    • Funktioniert die mehrfache Eingabe bei Falscheingabe
    • ...

    Ich habe das ganze schon in verschiedene Funktionen aufgeteilt, die das jeweils machen (und an sich somit eher einfach zu testen sind) und auch die streams als param übergeben (um sie z.B. mit stringstream zu mocken). Ich weiß also per se wie ich die ganzen Unterfunktionen einzelnd Unit Testen könnte und für die Hauptfunktion könnte ich diese dann stubben.

    Das Hauptproblem ist nur: Die einzig freie Methode des "public Interface" (also die auch eine Deklaration in einer .h hat) ist die createPerson Methode. Der ganze Rest sind Funktionen, die ich alle in nem anonymen Namespace definiert habe. Das sind ja quasi nur Hilfsfunktionen, die außerhalb nicht gebraucht werden (also in einer Klasse private Funktionen). Und man testet ja eig. nur das public Interface, die "private" Methoden werden implizit im Zweifelsfall mitgetestet. (Edit: Darüber hinaus ist es auch schwierig bis unmöglich freie Funktionen in einem anonymen Namespace zu testen, wenn dann noch zusätzlich Funktionen darauf gemockt werden müssen).
    Wenn ich hier also nur "createPerson" testen würde und alles andere explizit zusammen mit ... da hätte ich ja zig Sachen zu assserten.

    Sowas ist ja eig. immer ein Hinweis auf ein schlechtes Design z.B. Verletzung des Single Responsibility Prinzips. Habe ich das hier verletzt oder andere Design Fehler? Wie kann ich es besser machen?

    Die einzige "Lösung" die mir einfällt ist eben diese ganze Methoden aus dem anonymen Namespace rauszuholen, dann kann man sie auch testen und mocken. Aber da man die Funktionen nirgendswo sonst braucht, scheint mir das falsch.

    Manchmal habe ich auch "nützliche" Funktionen in meinem Namespace stehen. Z.B. habe ich mir für mein Anwendungsfall ne Hilfsmethode geschrieben, um Input einzulesen / validieren / error handling zu machen.

    template <typename T>
    T getInput(const std::string& prompt, std::istream& istream, std::ostream& ostream,
               std::function<bool(T)> isValid = nullptr) {
        T input;
    
        while(true) {
            ostream << prompt << "\n>> ";
    
            if((istream >> input) && (!isValid || isValid(input)))
                break;
    
            istream.clear();
            istream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            ostream << "Error!\n";
        }
        istream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    
        return input;
    }
    
    getInput<double>("How old are you?", istream, ostream, [](const double input) {
            bool correctInput = (input >= 0);
            if(!correctInput)
                std::cerr << "Age cannot be negative.\n";
            return correctInput;
        });
    

    Diese Funktion ist jetzt im Gegensatz zu anderen Methoden oben eher universeller verwendbar. Praktisch brauche ich sie aber trotzdem nur in meiner CommandLineInterface.cpp ... also ist sie halt in nem anonymen Namespace gelandet 😃
    Vlt. packe ich auch zu viel da rein? ... Ich könnte auch ne komplette Datenbank Library in nen anonymen Namespace packagen mit der Begründung: "Brauche die ja nur in dieser einen Datei" ... aber sowas würde man wohl kaum machen.


Log in to reply