Skip to content
This repository has been archived by the owner on Nov 27, 2023. It is now read-only.

Commit

Permalink
ADD assets/first-spirit-xml/2022-09-09/2022-09-09-mock-frameworks.xml
Browse files Browse the repository at this point in the history
  • Loading branch information
jekyll2cms authored and root committed Sep 9, 2022
1 parent 2e6f37f commit 84eaf7a
Showing 1 changed file with 225 additions and 0 deletions.
225 changes: 225 additions & 0 deletions assets/first-spirit-xml/2022-09-09/2022-09-09-mock-frameworks.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<documents>
<document uid="0eb0f24138b0628b86d4396f40802109">
<field name="title"><![CDATA[Mocking-Frameworks im Vergleich]]></field>
<field name="subline"><![CDATA[]]></field>
<field name="teaser"><![CDATA[<p>Ein Unit-Test soll genau das ausgewählte Stück Code testen.
Doch oft ruft die zu testende Methode externe Services oder Datenbanken auf.
Mocks können solche Abhängigkeiten durch Platzhalter ersetzen.
Die drei in der Microsoft-Welt beliebten Mocking-Frameworks Moq, NSubstitute und FakeItEasy werden hier gegenübergestellt.</p>
]]></field>
<field name="language_multi_keyword"><![CDATA[de]]></field>
<field name="content_type_multi_keyword"><![CDATA[blog]]></field>
<field name="mime_type_multi_keyword"><![CDATA[text/html]]></field>
<field name="category_multi_keyword"><![CDATA[Softwareentwicklung]]></field>
<field name="tag_multi_keyword"><![CDATA[Software]]></field>
<field name="tag_multi_keyword"><![CDATA[Entwicklung]]></field>
<field name="tag_multi_keyword"><![CDATA[Testing]]></field>
<field name="tag_multi_keyword"><![CDATA[Frameworks]]></field>
<field name="date_date"><![CDATA[2022-09-09T11:00:00+02:00]]></field>
<field name="date_l"><![CDATA[1662714000000]]></field>
<field name="change_date"><![CDATA[1662714000000]]></field>

<!--Author Information-->

<field name="author_id"><![CDATA[cjohn]]></field><!--Postcontent-->
<field name="headlines"><![CDATA[Mocking-Frameworks im Vergleich]]></field>
<field name="display_content"><![CDATA[<div class="i2-intro p-t-1">
<p>Ein Unit-Test soll genau das ausgewählte Stück Code testen.
Doch oft ruft die zu testende Methode externe Services oder Datenbanken auf.
Mocks können solche Abhängigkeiten durch Platzhalter ersetzen.
Die drei in der Microsoft-Welt beliebten Mocking-Frameworks Moq, NSubstitute und FakeItEasy werden hier gegenübergestellt.</p>
</div>]]></field>
<field name="content"><![CDATA[<div class="adesso-text-formate">
<div class="row p-t-2">
<div class="adesso-container">
<div class="col-xl-8 adesso-center p-b-1 p-l-0 p-r-0">
<p>Ein Unit-Test soll genau das ausgewählte Stück Code testen.
Doch oft ruft die zu testende Methode externe Services oder Datenbanken auf.
Mocks können solche Abhängigkeiten durch Platzhalter ersetzen.
Die drei in der Microsoft-Welt beliebten Mocking-Frameworks Moq, NSubstitute und FakeItEasy werden hier gegenübergestellt.</p>
<h4 id="was-mocks-können">Was Mocks können</h4>
<p>Ein Mock ersetzt Abhängigkeiten, die in der Testumgebung nicht zur Verfügung stehen, durch Platzhalter.
Er fängt Methodenaufrufe ab, ersetzt sie durch eigenen Code und füttert den Aufrufer mit Testdaten.
In der Theorie unterscheidet man Stubs, die nur feste Eingangsdaten mit festen Testdaten beantworten, von sogenannten Fakes, die tatsächlich eine alternative Logik enthalten.
Mocks sind erweiterte Stubs, die überprüfen können, ob eine abgefangene Methode alle im Testlauf erwarteten Aufrufe erhalten hat.
In der Praxis spielt diese Unterscheidung kaum eine Rolle.</p>
<p>Die drei hier betrachteten Frameworks sind alle in der Lage, gemockte Methoden mit einer sinnvollen Implementierung zu hinterlegen.
Im Alltag ist das zum Beispiel nützlich, um vom Testobjekt erzeugte Daten zu sammeln, die im Betrieb an eine Datenbank gesendet würden.
Ein wohlüberlegt definierter Fake kann flexibel eingesetzt werden, wenn er auf unterschiedliche Eingaben passend reagiert.</p>
<h4 id="das-gleiche-nur-anders">Das Gleiche, nur anders</h4>
<p>Die weit verbreiteten Frameworks <a href="https://github.com/moq">Moq</a>, <a href="https://nsubstitute.github.io">NSubstitute</a> und <a href="https://fakeiteasy.github.io">FakeItEasy</a> stellen sich hier dem Vergleich.
Alle davon verwenden intern <a href="http://www.castleproject.org/projects/dynamicproxy/">DynamicProxy</a> aus dem Castle Project, um von der zu testenden Schnittstelle oder Klasse eine Proxyklasse abzuleiten.
Damit unterliegen alle den gleichen Grenzen:
DynamicProxy erzeugt Proxy-Objekte für Interfaces oder Klassen, indem es eine neue Klasse davon ableitet.</p>
<p>Ein Proxy für eine konkrete Klasse kann naturgemäß nur die virtuellen Methoden abfangen.
Von statischen oder versiegelten Klassen ist kein Proxy ableitbar, sie können also nicht gemockt werden.
Generell empfiehlt sich, nur auf Interface-Ebene zu testen.
Denn ein Mock für eine Klasse ruft zumindest einen echten Konstruktor auf, bei nicht abgefangenen Methoden gegebenenfalls auch deren originale Implementierung, was beim Überbrücken von Schreibzugriffen durchaus Schäden hinterlassen kann.</p>
<h5 id="gemeinsamkeiten">Gemeinsamkeiten</h5>
<p>Alle Frameworks werden als NuGet-Paket installiert.
Das Grundprinzip ist immer gleich:
In der Proxy-Klasse werden für jede einzelne Methode die Rückgabewerte festgelegt.
Das kann eine Konstante sein, ein Wert pro erwartete Argument-Kombination oder auch eine Ersatzimplementierung.</p>
<p>Anschließend kann überprüft werden, ob ein erwarteter Aufruf im Testlauf stattgefunden hat.
Leider sieht kein Framework eine direkte Abfragemöglichkeit vor.
Stattdessen werfen sie bei fehlgeschlagener Verifikation eine jeweils frameworkspezifische Exception.</p>
<h5 id="analyzer">Analyzer</h5>
<p>Damit unzulässige Tests früh auffallen, bieten NSubstitute und FakeItEasy einen Roslyn-Analyzer an.
Beide überprüfen die Unit-Tests auf das Überschreiben eines nicht-virtuellen Members sowie auf den Bezug auf Argumente außerhalb des für die Methode spezifizierten Bereichs.
Der Analyzer von FakeItEasy macht mit seinen nur fünf Prüfregeln zwar einen rudimentären Eindruck gegenüber dem NSubstitute-Analyzer mit 24 Regeln.
Aber praktisch dürfte der Nutzen ohnehin gering sein, schließlich fallen Fehler bei der ersten Testausführung sofort auf.</p>
<h5 id="besonderheiten-von-moq">Besonderheiten von Moq</h5>
<p>Moq ist der Klassiker unter den Mocking-Frameworks, sein Name steht für “Mock you”.
Ein Alleinstellungsmerkmal zeichnet ihn aus:
Beim Testaufbau werden unterschiedliche “syntaktische Geschmacksrichtungen” unterstützt.
So lässt sich, wie gewohnt, jede Funktion einzeln mit <code>mock.Setup().Returns()</code> ersetzen.
Alternativ kann der komplette Mock auch in einem großen LINQ-Ausdruck definiert werden, was bei Fakes mit eigener Logik allerdings auf Kosten der Lesbarkeit geschieht.</p>
<p>Bei etwas umfangreicheren Interfaces oder wiederholten Methodenaufrufen geht die Performance von Moq leider schnell in die Knie.
Auch daran zeigt sich, dass Moq sein Spezialgebiet bei kurzen Wegwerf-Mocks hat, die wenige Methoden mit Konstanten überschreiben und die nach ein paar Aufrufen verworfen werden.</p>
<h5 id="besonderheiten-von-nsubstitute">Besonderheiten von NSubstitute</h5>
<p>NSubstitute verwendet Extensions, um den Testaufbau lesbar zu halten.
Solange der Mock streng als solcher definiert wird, behält der Testcode die schlichte Struktur <code>myObj.MyMethod().Returns()</code>.
Die Eleganz endet, sobald Funktionsargumente in der Fake-Logik benutzt werden sollen.
Denn diese werden in einem <code>CallInfo</code>-Objekt verpackt übergeben und sind zunächst alle vom Typ Object.</p>
<p>Um eine Klasse statt einer Schnittstelle zu ersetzen, müssen bei NSubstitute - genauso wie bei Moq - alle Konstruktor-Parameter angegeben werden.
Sollten sie in der Testumgebung nicht zur Verfügung stehen, muss auch hier für jeden Parameter vorher ein Substitut erzeugt werden.
Zu beachten sind auch die Warnungen in der Dokumentation, dass man im Regelfall nur Schnittstellen substituieren soll und nur im Ausnahmefall eine Klasse.</p>
<p>Vom Konstruktor abgesehen, ruft NSubstitute die echte Implementierung einer substituierten Klasse nie auf.
Das heißt, <code>Substitute.For&lt;T&gt;()</code> erzeugt immer einen strikten Fake.
Wo das nicht gewünscht ist, muss ausdrücklich mit <code>Substitute.ForPartsOf&lt;T&gt;()</code> ein partielles Substitut erzeugt werden.</p>
<h5 id="besonderheiten-von-fakeiteasy">Besonderheiten von FakeItEasy</h5>
<p>FakeItEasy hat das Ziel, besonders leicht verständlich zu sein.
Die Trennung von Stubs, Mocks, Fakes wurde verworfen, alles ist hier ein Fake.
Etwas gewöhnungsbedürftig ist die allgegenwärtige Klasse <code>A</code>, eine Anlehnung an natürliche Sprache.
Man erzeugt <code>A.Fake</code> und dafür <code>A.CallTo</code> mit Argumenten <code>A&lt;T&gt;</code>.</p>
<p>Wenn mit einer Klasse statt einer Schnittstelle gearbeitet wird, muss auch FakeItEasy den echten Konstruktor mit allen geforderten Parametern aufrufen.
Dafür erzeugt das Framework sogenannte Dummies:
Für jeden nicht angegebenen Konstruktorparameter wird automatisch ein Objekt eingesetzt, das den passenden Typ hat und nichts tut.
Auf das Konzept der Dummies kann überall zurückgegriffen werden, wo ein Objekt eines bestimmten Typs benötigt wird, dessen Verhalten egal ist.
Mit <code>A.CollectionOfDummy&lt;T&gt;(count)</code> kann sogar eine Liste mit untätigen Fakes gefüllt werden.</p>
<p>Ob die echte Implementierung einer virtuellen Methode aufgerufen wird, kann bei der Konfiguration mit der Option <code>Strict</code> festgelegt werden.
Ein strikter Mock ruft nur Basismethoden auf, die ausdrücklich mit <code>CallsBaseMethods</code> freigegeben wurden.</p>
<h5 id="performance">Performance</h5>
<p>In der täglichen Arbeit fällt auf, dass Moq bei umfangreichen Tests langsamer wird.
Das Ausmaß des Performance-Einbruchs lässt sich mit einem einfachen Lasttest abschätzen:</p>
<ul>
<li>Ein Interface hat 50 Methoden, jede nimmt einen <code>int</code> an und gibt einen zurück.</li>
<li>Ein Unit-Test erstellt einen Fake für das Interface mit 50 identischen Methoden, die den Parameter wieder ausgeben, dann ruft er jede dieser Methoden 100 Mal auf.</li>
<li>Eine Stopwatch schreibt die Millisekunden für den Aufbau und jeden einzelnen Call mit.</li>
</ul>
<p><img src="/assets/images/posts/mock-frameworks/mock-frameworks.png" alt="Zeit pro Call" /></p>
<p>Bei allen Durchläufen stieg die Dauer pro Call mit der Anzahl vorheriger Calls an.
Die Abbildung veranschaulicht die Verläufe pro Framework.
Bei NSubstitute zeigt die Dauer pro Call einer konstanter Steigungsfaktor von harmlosen 0,008.
FakeItEasy pendelt sich nach einer Aufwärmzeit bei einem Steigungsfaktor von 0,01 ein.
Moq hingegen tanzt aus der Reihe:
Bei sehr wenigen Calls arbeitet das Framework noch so zügig wie NSubstitute, ab ca. 1000 Methodenaufrufen eskaliert die Zeit pro Call jedoch.
Dies zeigt sich in einer Steigerung der Aufrufzeit um den Faktor 0,03.
Das heißt, die Performance skaliert über längere Unit-Tests dreimal schlechter als die anderer Mocking-Frameworks.</p>
<h4 id="beispiele">Beispiele</h4>
<p>Um die theoretischen Betrachtungen abzurunden, folgt hier für jedes Framework ein kurzes Beispiel.
Darin wird jeweils dieselbe Schnittstelle <code>IAddressBook</code> so gemockt, dass die Suche ein zur Eingabe passendes Ergebnis simuliert, ohne dass ein echtes Adressbuch verfügbar sein muss.
Anschließend werden die erwarteten Aufrufe verifiziert.</p>
<h5 id="listing-1-moq">Listing 1: Moq</h5>
<pre><code class="language-csharp">public void Moq_AddressBook_Should_Find_Person()
{
var addressBook = new Mock&lt;IAddressBook&gt;();
var setup = mock.Setup(
x =&gt; x.FindPerson(It.IsAny&lt;string&gt;(), It.IsAny&lt;int?&gt;()))
.Returns((string name, int? age) =&gt;
new Person {
Name = name,
Age = age });
var testResult = addressBook.Object.FindPerson("Kay", 42);
mock.Verify(x =&gt; x.FindPerson("Kay", 42));
}
</code></pre>
<h5 id="lising-2-nsubstitute">Lising 2: NSubstitute</h5>
<pre><code class="language-csharp">public void NSubstitute_AddressBook_Should_Find_Person()
{
var addressBook = Substitute.For&lt;IAddressBook&gt;();
addressBook.FindPerson(Arg.Any&lt;string&gt;(), Arg.Any&lt;int?&gt;())
.Returns(callInfo =&gt;
new Person {
Name = callInfo.ArgAt&lt;string&gt;(0),
Age = callInfo.ArgAt&lt;int?&gt;(1) });
var testResult = addressBook.FindPerson("Kay", 42);
addressBook.Received().FindPerson("Kay", 42);
}
</code></pre>
<h5 id="listing-3-fakeiteasy">Listing 3: FakeItEasy</h5>
<pre><code class="language-csharp">public void FakeItEasy_AddressBook_Should_Find_Person()
{
var addressBook = A.Fake&lt;IAddressBook&gt;();
A.CallTo(() =&gt; addressBook.FindPerson(A&lt;string&gt;.Ignored, A&lt;int?&gt;.Ignored))
.ReturnsLazily((string name, int? age) =&gt;
new Person {
Name = name,
Age = age });
var testResult = addressBook.FindPerson("Kay", 42);
A.CallTo(() =&gt; addressBook.FindPerson("Kay", 42)).MustHaveHappened();
}
</code></pre>
<h4 id="fazit">Fazit</h4>
<p>Insgesamt macht NSubstitute den solidesten Eindruck.
Alle häufig benötigten Features sind vorhanden, der Testaufbau über Extensions ergibt leicht lesbaren Code.
Zur Laufzeit überzeugt die Geschwindigkeit.</p>
<p>Bei FakeItEasy wird der Testaufbau dadurch unleserlich, dass jede Zeile mit “A.CallTo” beginnt.
Im Gegenzug ist der Zugriff auf Funktionsargumente schöner gelöst.
Ein Spezialgebiet von FakeItEasy ist die gute Unterstützung von Fakes konkreter Klassen.
Wenn teils Originalcode getestet werden soll, sind die Dummies eine große Hilfe.</p>
<p>Der Einsatz von Moq erscheint nur in Projekten sinnvoll, in denen jeder Mock nach einem sehr kurzen Test verworfen wird.
Darauf ist auch die alternative LINQ-Syntax abgestimmt:
Kurze Stubs lassen sich damit kompakt definieren.
Da Moq zu den ältesten Mocking-Frameworks gehört, wird es in vielen Legacy-Projekten eingesetzt.
Bei der Überarbeitung alter Tests lohnt sich gegebenenfalls der Austausch gegen ein moderneres Framework.</p>
</div>
</div>
</div>
</div>]]></field>
</document>
</documents>

0 comments on commit 84eaf7a

Please sign in to comment.