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/nfhT9xa4Emimic++
ist stark inspiriert vom weit verbreitetentrompeloeil
, 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:- ein Test erzeugt ein Mock Objekt
- dieser Test erzeugt Expectations, was mit diesem Mock passieren soll
- der Mock wird an die zu testende Implementierung übergeben
- die Implementierung benutzt das Objekt
- 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 durchmimic++
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
-
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 einoptional
einen printbaren Wert enthält, wird dieser nun korrekt dargestellt.
Gleiches gilt auch fürtuple-like
typen (also die, die mittelsstd::tuple_size
und indiziertemstd::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 OptionMIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER
auf Unicode Typen erweitern, bedeutet allerdings auch, dass dann die leichtgewichtigeuni_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 inmimic++
zu nutzen. Jedoch ist dies ein experimentelles Feature, das explizit mit der OptionMIMICPP_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.