mimic++, ein modernes und (fast) makro-freies mocking-framework



  • Hallo,

    ich habe heute den zweiten Release für mein mocking-framework mimic++ veröffentlicht.

    github: https://github.com/DNKpp/mimicpp
    Doku: https://dnkpp.github.io/mimicpp/
    godbolt-toy-project: https://godbolt.org/z/nfhT9xa4E

    mimic++ ist stark inspiriert vom weit verbreiteten trompeloeil, verzichtet dabei allerdings komplett auf verpflichtende Makros. Die Code-Base baut einzig und alleine auf templates und concepts auf, was einige Vorteile, sowie natürlich auch Nachteile mit sich bringt (je nach Blickwinkel). Allerdings werden auch ein paar wenige Makros zur optionalen Verwendung angeboten, sofern dadurch die Nutzbarkeit vereinfacht wird. Jedoch habe ich viel Wert darauf gelegt, dass diese Makros lediglich eine dünne Schicht bilden und ggf. später (z.B. wenn reflection mit c++26 kommen sollte), ohne Probleme ersetzt werden können.

    Generell kommen Formatierungs- und Darstellungs-Tools meiner Erfahrung nach besser mit echten Sprach-Features zurecht. Dafür ist trompeloeils Syntax teils etwas "kompakter".

    Das Wie und Was

    mimic++ bedient sich hier zwei grundlegenden Konzepten: Mocks und Expectations
    Mocks imitiere hierbei Interfaces, während Expectations eben Erwartungen des jeweiligen Tests sind, wie, wann oder wie oft ein Mock von zu testenden Implementierungen genutzt wird.
    Im Endeffekt ist das Verfahren immer relativ ähnlich:

    1. ein Test erzeugt ein Mock Objekt
    2. dieser Test erzeugt Expectations, was mit diesem Mock passieren soll
    3. der Mock wird an die zu testende Implementierung übergeben
    4. die Implementierung benutzt das Objekt
    5. der Test kann nun von außen nachvollziehen, ob die Expectations erfüllt oder verletzt wurden

    Um sich das besser vorzustellen, zitiere ich hier einmal das erste Beispiel aus meiner Readme:

    mimicpp::Mock<int(std::string, std::optional<int>)> mock{};     // actually enables just `int operator ()(std::string, std::optional<int>)`
    SCOPED_EXP mock.expect_call("Hello, World", _)                  // requires the first argument to match the string "Hello, World"; the second has no restrictions
                    and expect::at_least(1)                             // controls, how often the whole expectation must be matched
                    and expect::arg<0>(!matches::range::is_empty())     // addtionally requires the first argument to be not empty (note the preceeding !)
                    and expect::arg<1>(matches::ne(std::nullopt))       // requires the second argument to compare unequal to "std::nullopt"
                    and expect::arg<1>(matches::lt(1337))               // and eventually to be less than 1337
                    and then::apply_arg<0>(                             // That's a side-effect, which get's executed, when a match has been made.
                        [](std::string_view str) { std::cout << str; }) //     This one writes the content of the first argument to std::cout.
                    and finally::returns(42);                           // And, when matches, returns 42
    
    int result = mock("Hello, World", 1336);                        // matches
    REQUIRE(42 == result);
    

    In Zeile 1 wird ein Mock erzeugt (Punkt 1), das denn int operator ()(std::string, std::optional<int>) als Funktionalität anbietet.
    Zeile 2 - 9 erzeugen dann eine, relativ komplexe, Expectation (Punkt 2), die irgendwo im derzeitigen (oder tieferen) Scope erfüllt werden muss.
    In Zeile 11 lösen wir dann die Erwartung explizit selbst ein. Das würde in realen Tests natürlich innerhalb der zu testenden Implementierung passieren (Punkt 3 und 4).
    Punkt 5 ist dann durch mimic++ gratis dabei, da jede Expectation beim verlassen des Scopes überprüft und ggf. eine Verletzung reported wird, die dann über das benutzte Test-Framework automatisch angezeigt wird (und die tests failen lässt).

    Angenommen, die Expectation wäre nicht erfüllt worden, dann würde beispielsweise der Catch2-Adapter folgendes an den User melden:

    <mimic++InstallPfad>/mimic++/adapters/Catch2.hpp(29): FAILED:
    explicitly with message:
      Unfulfilled expectation:
      Expectation report:
      from: <UnitTestPfad>[89:4], void __cdecl
      CATCH2_INTERNAL_TEST_0(void)
      times: matched never - between 1 and 2147483647 times is expected
      expects:
            expect: arg[0] == "Hello, World",
            expect: arg[1] has no constraints,
            expect: from any category overload,
            expect: from mutable qualified overload,
            expect: arg[0] is not an empty range,
            expect: arg[1] != {?},
            expect: arg[1] < 1337,
    

    Würden wir beispielsweise, anstatt des geforderten "Hello, World", ein anderen String übergeben, hätten wir ebenfalls eine Verletzung und erhielten folgendes:

    <mimic++InstallPfad>/mimic++/adapters/Catch2.hpp(29): FAILED:
    explicitly with message:
      No match for call from <UnitTestPfad>[98:8], void
      __cdecl CATCH2_INTERNAL_TEST_0(void)
      constness: mutable
      value category: any
      return type: int
      args:
            arg[0]: {
                    type: class std::basic_string<char,struct std::char_traits<char>,class std:
      :allocator<char> >,
                    value: "Test"
            },
            arg[1]: {
                    type: class std::optional<int>,
                    value: {?}
            },
    
    1 available expectation(s):
      Unmatched expectation: {
      from: <UnitTestPfad>[89:4], void __cdecl
      CATCH2_INTERNAL_TEST_0(void)
      failed:
            expect: arg[0] == "Hello, World",
      passed:
            expect: arg[1] has no constraints,
            expect: from any category overload,
            expect: from mutable qualified overload,
            expect: arg[0] is not an empty range,
            expect: arg[1] != {?},
            expect: arg[1] < 1337,
      }
    

    Würde mich über Feedback freuen 🙂


  • Gesperrt

    Dieser Beitrag wurde gelöscht!


  • Werde ich für künftige Projekte verwenden 👍🏻



  • @Zhavok sagte in mimic++, ein modernes und (fast) makro-freies mocking-framework:

    Werde ich für künftige Projekte verwenden 👍🏻

    Ja, cool. Das freut mich zu hören 🙂

    In der Zwischenzeit gab es nun auch noch einen neuen Release mit einigen größeren und kleineren Features:

    Erweiterten print-Support

    Im Eingangspost ist zu sehen, dass std::optional bisher nur als {?} geprintet wurde. Das wurde nun erweitert. Sofern ein optional einen printbaren Wert enthält, wird dieser nun korrekt dargestellt.
    Gleiches gilt auch für tuple-like typen (also die, die mittels std::tuple_size und indiziertem std::get ansprechbar sind).

    String-Matcher

    Es gibt nun eine ganze Reihe an String-Matchern, die auf beliebigen String-Typen operieren können und dabei in Case-Sensitiven und -Insensitiven Varianten zur Verfügung stehen:

    • matches::str::eq
    • matches::str::starts_with
    • matches::str::ends_with
    • matches::str::contains

    Die Case-Insensitiven Varianten stehen von Haus aus nur für reine char-Strings zur Verfügung. Allerdings lässt sich das mittels der Option MIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER auf Unicode Typen erweitern, bedeutet allerdings auch, dass dann die leichtgewichtige uni_algo Library nachgezogen werden muss.

    (Experimentelle) Native Catch2-Matcher Integration

    mimic++ bietet zwar bereits eine ganze Reihe an fertigen Matchern an, jedoch fehlen auch noch ein paar nicht ganz unwichtige (z.B. floating-point Matcher). Um diese Lücke zu schließen, gibt es nun die Möglichkeit, alle Matcher aus dem Catch2 Framework direkt in mimic++ zu nutzen. Jedoch ist dies ein experimentelles Feature, das explizit mit der Option MIMICPP_CONFIG_EXPERIMENTAL_CATCH2_MATCHER_INTEGRATION eingeschaltet werden muss.

    Dies steht natürlich nur dann zur Verfügung, sofern Catch2 als Unit-Test-Framework genutzt wird und der entsprechende Adapter aus mimic++ zum Einsatz kommt.

    Object-Watcher

    Es wurden zwei Watcher Typen hinzugefügt, die bestimmte Operationen auf Objekt-Instanzen erkennen und mit bestehenden Expectations vergleichen. Im Prinzip handelt es sich hierbei um einen Weg, Destruktor und Move-Constructor/Assignment zu mocken.
    Als kleinen Teaser:

    #include <mimic++/mimic++.hpp>
    
    namespace expect = mimicpp::expect;
    namespace then = mimicpp::then;
    
    TEST_CASE("LifetimeWatcher and RelocationWatcher can trace object instances.")
    {
        mimicpp::Watched<
            mimicpp::Mock<void()>,
            mimicpp::LifetimeWatcher,
            mimicpp::RelocationWatcher> watched{};
    
        SCOPED_EXP watched.expect_destruct();
        int relocationCounter{};
        SCOPED_EXP watched.expect_relocate()
                    and then::invoke([&] { ++relocationCounter; })
                    and expect::at_least(1);
    
        std::optional wrapped{std::move(watched)};  // satisfies one relocate-expectation
        std::optional other{std::move(wrapped)};    // satisfies a second relocate-expectation
        wrapped.reset();                            // won't require a destruct-expectation, as moved-from objects are considered dead
        other.reset();                              // fulfills the destruct-expectation
        REQUIRE(2 == relocationCounter);            // let's see, how often the instance has been relocated
    }
    

    Würde mich weiterhin über (ernsthaftes) Feedback freuen 🙂



  • Release V4

    Dies ist ein kleiner release, der ein paar zusätzliche Features bringt:

    • print Support für Pointer typen
    • zusätzlichen Adapter für das Test-Framework Doctest
    • und drei zusätzliche Matcher
      • matches::instance
      • matches::range::each_element
      • matches::range::any_element

    Zusätzlich ist mimic++ nun auch offiziell auf godbolt.org verfügbar.



  • Release v5

    Über die letzten Tage hinweg habe ich einige Verbesserungen integriert; darunter auch 4 kleinere Bug-fixes.

    Änderungen

    • Verwenden von standardisiertem clang-format File, anstatt von resharper-eigenem Format.
    • ExpectationBuilder wurde etwas compiler-freundlicher
    • is_overload_set-trait ist jetzt effizienter

    Neue Features

    • Genereller Support für call-conventions (z.B. __stdcall für Microsoft COM)
      • Dazu das notwendige MIMICPP_REGISTER_CALL_CONVENTION macro
    • Neue signatur- bzw. funktionsbezogenen traits und concepts:
      • signature_call_convention(_t)-trait
      • signature_remove_call_convention(_t)-trait
      • call_convention_traits-trait
      • signature_remove_ref_qualifier(_t)-trait
      • signature_remove_const_qualifier(_t)-trait
      • signature_const_qualification(_v)-trait
      • signature_ref_qualification(_v)-trait
      • signature_is_noexcept(_v)-trait
      • has_default_call_convention-concept
    • Hinzufügen von einigen 32bit Konfigurationen für die Build-Pipeline (und damit offizielle 32bit Unterstützung)

    Fixes

    • Verwenden der korrekten Darstellungsbreite, wenn Pointer in 32bit Build über mimicpp::print ausgegeben werden.
    • MIMICPP_MOCK_METHOD behandelt nun geklammerte return types korrekt (z.B. (std::tuple<int, int>)).
    • MIMICPP_MOCK_METHOD nutzt nun die korrekte Referenz-Kategorie, um an die internen Mock-Objekte weiterzuleiten.
    • MIMICPP_MOCK_METHOD behandelt nun alle Arten von Parameter-Packs korrekt.

    Für den letzten Punkt habe ich sogar einen ausführlichen Post auf meinem neuen Blog geschrieben, da dies doch durchaus interessant zu lösen war.
    Schaut doch gerne mal vorbei: dnkpp.github.io

    Wie immer würde ich mich über Feedback sehr freuen.


Anmelden zum Antworten