Last active
October 19, 2020 18:36
-
-
Save TeaDrivenDev/5992922 to your computer and use it in GitHub Desktop.
Warum sehen AutoFixture-Tests so seltsam aus, und welche Vorteile hat das?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* v1.0.1 | |
* | |
* TL;DR: AutoFixture rockt. | |
* | |
* | |
* Folgendes ist der Versuch einer Erklärung, warum Unit Tests mit AutoFixture so seltsam aussehen | |
* und welche Vorteile das hat. | |
* | |
* | |
* Der Code ist so am Stück lauffähig, wenn folgende externen Referenzen eingebunden sind: | |
* | |
* Xunit.Extensions (bindet per NuGet Xunit mit ein) | |
* Moq | |
* AutoFixture.Xunit (bindet per NuGet AutoFixture mit ein) | |
* AutoFixture.AutoMoq | |
* | |
*/ | |
using Moq; | |
using Ploeh.AutoFixture; | |
using Ploeh.AutoFixture.AutoMoq; | |
using Ploeh.AutoFixture.Xunit; | |
using System; | |
using Xunit; | |
using Xunit.Extensions; | |
/* | |
Gegeben sei folgende Klasse, nicht notwendigerweise sinnvoll oder vollständig (und schon gar | |
nicht vollständig sinnvoll): | |
*/ | |
public class WieselFlink | |
{ | |
#region Dependencies | |
private readonly ICanHasCheezburger _cheezburger; | |
private readonly IRoflCopter _copter; | |
#endregion Dependencies | |
#region Constructors | |
public WieselFlink(ICanHasCheezburger cheezburger, IRoflCopter copter) | |
{ | |
if (cheezburger == null) throw new ArgumentNullException("cheezburger"); | |
if (copter == null) throw new ArgumentNullException("copter"); | |
this._cheezburger = cheezburger; | |
this._copter = copter; | |
} | |
#endregion Constructors | |
#region Public interface | |
public string Value | |
{ | |
get { return this._cheezburger.Name + " (weazelized)"; } | |
} | |
#endregion Public interface | |
} | |
// Die Interfaces dazu sehen der Einfachheit halber so aus: | |
public interface ICanHasCheezburger | |
{ | |
string Name { get; } | |
} | |
public interface IRoflCopter { } | |
// Ein ganz normaler xUnit.net-Test wäre dann dieser: | |
public class WieselFlinkTraditionalTests | |
{ | |
[Fact] | |
public void Value_ReturnsWeazelizedCheezburgerName() | |
{ | |
// Arrange | |
var cheezburgerName = "l0lrofl"; | |
var cheezburgerStub = new Mock<ICanHasCheezburger>(); | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
var copterStub = new Mock<IRoflCopter>(); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = new WieselFlink(cheezburgerStub.Object, copterStub.Object); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Das ist ein ziemlich einfacher Test, aber schon hier betreibe ich im Arrange einigen Aufwand: | |
- Ich muß mir einen String ausdenken, von dem beim Lesen des Tests auch nicht unmittelbar klar | |
wird, ob sein Inhalt eine Rolle spielt. | |
- Ich muß ein Mock erzeugen und aufsetzen, ein zweites nur erzeugen, um den Null Guard im SUT | |
nicht auszulösen. | |
- Ich muß das SUT erzeugen, und das mit einem Konstruktoraufruf, der kaputtgeht, wenn sich am | |
Konstruktor mal was ändert, und dem ich einen Parameter übergebe, der für den Test komplett | |
irrelevant ist. | |
Act und Assert dagegen sind minimal einfach. | |
Der Zweck von AutoFixture ist nun, die Arrange-Phase zu vereinfachen und mir möglichst viel von | |
der Arbeit abzunehmen, die keinen.... "Value addet". | |
Mark Seemann spricht in dem Zusammenhang von "Low-friction TDD". | |
Dabei setzt AutoFixture zwei Schwerpunkte: | |
- die Erzeugung "anonymer" Testdaten (hier schreit dann Roy Osherove: "Zufallswerte in Unit Tests | |
gehen ja mal überhaupt nicht!"; Mark Seemann antwortet: "Keine Panik; das ist 'constrained | |
nondeterminism' - wir wissen, was wir tun.") | |
- die Rolle einer "sut factory", die sich selbst um Abhängigkeiten kümmert und explizite | |
Konstruktoraufrufe aus den Tests raushält | |
Im einfachsten Fall lasse ich mir zunächst mal den String erzeugen: | |
*/ | |
public class WieselFlinkSimplestAutoFixtureTests | |
{ | |
[Fact] | |
public void Value_ReturnsWeazelizedCheezburgerName() | |
{ | |
// Arrange | |
var fixture = new Fixture(); | |
var cheezburgerName = fixture.Create<string>(); | |
var cheezburgerStub = new Mock<ICanHasCheezburger>(); | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
var copterStub = new Mock<IRoflCopter>(); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = new WieselFlink(cheezburgerStub.Object, copterStub.Object); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Wie ich nun genau zu dem String komme, überlasse ich der Fixture; die wird schon wissen, was sie | |
treibt. Hiermit ist nun aber zum einen beim Lesen sofort klar, daß der konkrete Inhalt des Strings | |
komplett egal ist, und zum anderen habe ich nicht mehr die Möglichkeit, das SUT (auch | |
unabsichtlich) so zu implementieren, daß es genau für den einen String im Test funktioniert und | |
sonst falsch arbeitet, ohne daß der Test rot wird. | |
Dreh- und Angelpunkt des ganzen ist ebenjene Fixture, die sich, wie es die Art und Weise der | |
Create<T>-Methode schon andeutet, einem IoC-Container nicht unähnlich verhält - mit dem | |
Unterschied, daß sie sich im Zweifelsfall einfach was ausdenkt, wenn ihr keine konkreten Regeln an | |
die Hand gegeben wurden. | |
Eine nackte Fixture kann dabei zunächst mal nur konkrete Typen erzeugen und hangelt sich dabei | |
auch rekursiv durch Konstruktorparameter, wird aber bei einem Interface aufgeben, weil sie dafür | |
keine Regel kennt. (Standardmäßig werden nach dem Erzeugen auch beschreibbare Properties noch mit | |
"anonymen" Werten bestückt, was aber gern mal nachteilige Nebeneffekte hat - unter anderem deshalb | |
ist das ganz oder selektiv ab- und zuschaltbar.) | |
Als nächstes möchte ich jetzt den empfindlichen Konstruktoraufruf loswerden; dazu benutze ich die | |
Fixture noch etwas mehr wie einen IoC-Container, indem ich für die Auflösung der Dependencies | |
konkrete Instanzen für die Interfaces angebe, und sie anschließend eine Instanz der zu testenden | |
Klasse erzeugen lasse: | |
*/ | |
public class WieselFlinkAutoFixtureSutFactoryTests | |
{ | |
[Fact] | |
public void Value_ReturnsWeazelizedCheezburgerName() | |
{ | |
// Arrange | |
var fixture = new Fixture(); | |
var cheezburgerName = fixture.Create<string>(); | |
var cheezburgerStub = new Mock<ICanHasCheezburger>(); | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
fixture.Inject<ICanHasCheezburger>(cheezburgerStub.Object); | |
var copterStub = new Mock<IRoflCopter>(); | |
fixture.Inject<IRoflCopter>(copterStub.Object); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = fixture.Create<WieselFlink>(); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Jetzt könnte ich jederzeit einen weiteren Konstruktorparameter hinzufügen, solange die Fixture den | |
dann erzeugen kann - und wenn nicht, wird zwar mein Test rot, aber der Code kompiliert nach wie | |
vor. Genauso könnte ich den nicht benutzen RoflCopter rausnehmen, ohne den existierenden Test | |
kaputtzumachen - der hängt ja ohnehin nur vom Cheezburger ab. | |
Soweit, so nett, aber etwas begrenzt ist das schon noch, da die Fixture halt nur Dinge rausgeben | |
kann, die sie so in der Natur der konkreten Typen findet oder die ich ihr vorher in die Hand | |
gedrückt habe. | |
Um selbst Regeln für die Erzeugung von Objektinstanzen festzulegen, gibt es beispielsweise eine | |
Register<T>-Methode, über die sich Factory-Delegates angeben lassen, die dann für Anfragen nach | |
dem jeweiligen Typ herangezogen werden. Ich könnte also etwa (auch wenn es hier zunächst nicht so | |
furchtbar viel beiträgt) die eigentliche Erzeugung der Stubs der Fixture überlassen: | |
*/ | |
public class WieselFlinkAutoFixtureWithRegisteredFactoriesTests | |
{ | |
[Fact] | |
public void Value_ReturnsWeazelizedCheezburgerName() | |
{ | |
// Arrange | |
var fixture = new Fixture(); | |
var cheezburgerName = fixture.Create<string>(); | |
fixture.Register<ICanHasCheezburger>(() => | |
{ | |
var stub = new Mock<ICanHasCheezburger>(); | |
stub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
return stub.Object; | |
}); | |
fixture.Register<IRoflCopter>(() => new Mock<IRoflCopter>().Object); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = fixture.Create<WieselFlink>(); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Hier beginnt es spannend zu werden (auch wenn das noch kein generell greifender Ansatz ist, weil | |
ich selbst pro Abstraktion die Regel vorgeben muß): Ich kann die Fixture selbst Instanzen von | |
Abstraktionen erzeugen lassen, die sie dann - wie ein IoC-Container halt - rausreicht, wenn sie | |
danach gefragt wird. | |
Nun ist es nicht unwahrscheinlich, daß ich an mehreren Stellen innerhalb eines Projekts | |
Abhängigkeiten auf die gleichen Abstraktionen habe; natürlich will ich die Regeln dafür möglichst | |
nicht mehrfach pflegen, und überhaupt wäre es vielleicht nett, die zusammen an einer etwas | |
konkreteren Stelle zu halten, insbesondere wenn das mal mehr werden. Hierfür hat AutoFixture das | |
Konzept der Customizations, das auch wieder eine Parallele zu IoC-Containern darstellt - es | |
entspricht im wesentlichen dem Gedanken eines Moduls bei AutoFac (Castle Windsor kennt meines | |
Wissens ein analoges Konstrukt). | |
Eine Customization implementiert das Interface ICustomization und macht das, was ich oben noch | |
selbst direkt im Test getan habe - sie nimmt Registrierungen auf der Fixture vor: | |
*/ | |
public class PrimitiveWieselFlinkCustomization : ICustomization | |
{ | |
private readonly string _cheezburgerName; | |
public PrimitiveWieselFlinkCustomization(string cheezburgerName) | |
{ | |
this._cheezburgerName = cheezburgerName; | |
} | |
#region ICustomization Members | |
public void Customize(IFixture fixture) | |
{ | |
fixture.Register<ICanHasCheezburger>(() => | |
{ | |
var stub = new Mock<ICanHasCheezburger>(); | |
stub.SetupGet(c => c.Name).Returns(this._cheezburgerName); | |
return stub.Object; | |
}); | |
fixture.Register<IRoflCopter>(() => new Mock<IRoflCopter>().Object); | |
} | |
#endregion ICustomization Members | |
} | |
/* | |
Wenn ich die Customization nun auf die Fixture anwende, verhält sich diese logischerweise wie | |
bisher mit den manuell registrierten Factories: | |
*/ | |
public class WieselFlinkWithPrimitiveCustomizationTests | |
{ | |
[Fact] | |
public void Value_ReturnsWeazelizedCheezburgerName() | |
{ | |
// Arrange | |
var fixture = new Fixture(); | |
var cheezburgerName = fixture.Create<string>(); | |
fixture.Customize(new PrimitiveWieselFlinkCustomization(cheezburgerName)); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = fixture.Create<WieselFlink>(); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Mit den Customizations (von denen man auf eine Fixture beliebig viele anwenden kann) gewinnt das | |
ganze deutlich an Flexibilität und Skalierbarkeit, weil die Fixture-Konfiguration damit | |
organisierbar und wiederverwendbar wird und man sich so etwa auch immer wieder einsetzbare | |
domänenspezifische Testframeworks bauen kann. | |
Sehr wichtig ist in dem Zusammenhang, daß die Fixture nicht einfach das Ergebnis der Detailinhalte | |
aller angewendeten Customizations ist, sondern sich AutoFixture die Reihenfolge der Customizations | |
merkt und genau nach dieser vorgeht, wenn ein Typ aufzulösen ist. Erst wenn mit den Regeln einer | |
Customization kein Erfolg erzielt wurde, wird die nächste "angefangen", und nur wenn keine | |
Customization gegriffen hat, versucht sich die Fixture mit der Standard-Funktionalität an der | |
Erzeugung einer Instanz. Dadurch könnte ich beispielsweise die Regel zum Generieren von Strings | |
überschreiben, wenn mir der vorgegebene GUID-basierte Ansatz nicht genügt und ich sicherstellen | |
will, daß mein zu testender Code auch immer ein paar Sonderzeichen gefüttert kriegt. | |
Natürlich lassen sich Customizations ihrerseits auch wieder gruppieren; es gibt dafür die | |
vorgefertigte Klasse CompositeCustomization, von der nur noch abgeleitet werden muß. | |
Das nächste wichtige Konzept ist das Einfrieren von Typen, das in IoC-Terminologie einer | |
Singleton-Registrierung entspricht. Beim Aufruf der Freeze<T>-Methode wird zuerst mit Create<T> | |
eine Instanz erzeugt, die dann sofort mit Inject<T> wieder an die Fixture übergeben und dort für | |
zukünftige Auflösungen festgehalten wird - eventuelle dynamische Regeln wie Factories sind damit | |
außer Kraft gesetzt. | |
Damit kann ich jetzt auch das Erzeugen meines Test-Strings in die Customization stecken (den an | |
den Konstruktor der Customization zu zu übergeben, war sowieso eine Krücke, die in der Praxis sehr | |
unhandlich würde) und dadurch auch das Erzeugen und Anpassen der Fixture knapper schreiben: | |
*/ | |
public class StringGeneratingWieselFlinkCustomization : ICustomization | |
{ | |
#region ICustomization Members | |
public void Customize(IFixture fixture) | |
{ | |
fixture.Freeze<string>(); | |
fixture.Register<ICanHasCheezburger>(() => | |
{ | |
var stub = new Mock<ICanHasCheezburger>(); | |
stub.SetupGet(c => c.Name).Returns(fixture.Create<string>()); | |
return stub.Object; | |
}); | |
fixture.Register<IRoflCopter>(() => new Mock<IRoflCopter>().Object); | |
} | |
#endregion ICustomization Members | |
} | |
public class WieselFlinkWithStringGeneratingCustomizationTests | |
{ | |
[Fact] | |
public void Value_ReturnsWeazelizedCheezburgerName() | |
{ | |
// Arrange | |
var fixture = new Fixture().Customize(new StringGeneratingWieselFlinkCustomization()); | |
var cheezburgerName = fixture.Create<string>(); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = fixture.Create<WieselFlink>(); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Nach dem Einfrieren des Typs string gibt jeder folgende Aufruf von Create<string> wieder genau den | |
selben Wert zurück. (Anzumerken ist zu diesem Beispiel, daß das Einfrieren ausgerechnet von | |
Strings in der Regel eine blöde Idee ist, weil man davon nicht selten mehrere in einem Test | |
braucht.) | |
Bevor wir gleich auf den absurden Part zugehen, nochmal kurz Customizations, und zwar wie | |
angerissen vorgefertigte, immer wieder einsetzbare: Beim traditionellen Unit Testing/TDD quasi | |
unumgänglich sind Mocks/Stubs/Dummies/Fakes, bevorzugt aus dynamischen Mock-Bibliotheken, damit | |
man nicht jede Implementation selbst stricken muß. Den Umgang damit muß man natürlich der Fixture | |
erst mal beibringen, so wie ich das ja in meiner Customization schon punktuell getan habe. | |
Da sowas natürlich außerhalb eines so primitiven Beispiels nicht mehr individuell pro Abstraktion | |
machbar ist, sondern generisch funktionieren sollte, gibt es als Erweiterungen zum eigentlichen | |
AutoFixture "glue libraries" für gängige Mock-Frameworks (derzeit Moq, RhinoMocks, FakeItEasy und | |
NSubstitute), deren Beitrag im wesentlichen jeweils aus einer Customization besteht, die für die | |
korrekte Erzeugung von Mock-Objekten für Interfaces und nicht versiegelte Klassen sorgt. | |
Bei der Verwendung ist es wichtig, sich bewußt zu machen, wie dieses Konstrukt tickt: Kommt an der | |
Customization eine Anfrage zur Auflösung etwa eines Interfaces an (was bedeutet, daß sich davor | |
keine Customization zuständig zeigte), wird ein Mock erzeugt und die gemockte Instanz | |
zurückgegeben (beim von mir verwendeten Moq also die .Object-Property des Mocks). Anfragen nach | |
Mock<T> selbst kommen natürlich auch hier an und werden mit einer erzeugten Mock-Instanz selbst | |
beantwortet. | |
Wenden wir das auf unser Beispiel an und ersetzen unsere eigene etwas naive Customization durch | |
eine AutoMoqCustomization: | |
*/ | |
public class WieselFlinkWithAutoMoqCustomizationTests | |
{ | |
[Fact] | |
public void Value_ReturnsWeazelizedCheezburgerName() | |
{ | |
// Arrange | |
var fixture = new Fixture().Customize(new AutoMoqCustomization()); | |
var cheezburgerName = fixture.Create<string>(); | |
var cheezburgerStub = new Mock<ICanHasCheezburger>(); | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
fixture.Inject<ICanHasCheezburger>(cheezburgerStub.Object); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = fixture.Create<WieselFlink>(); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Im wesentlichen fallen hier drei Dinge auf: | |
- Ich habe den String nicht mehr eingefroren sondern kümmere mich selbst drum; damit habe ich | |
Flexibilität zurückgewonnen, sowohl was Strings angeht, als auch hinsichtlich der Konfiguration | |
von Mock-Objekten für den Typ ICanHasCheezburger. | |
- Ich muß dran denken, der Fixture die konkrete Instanz von ICanHasCheezburger mitzuteilen, die | |
zukünftig verwendet werden soll. | |
- Der WieselFlink-Konstruktor braucht immer noch eine Instanz von IRoflCopter, aber ich muß mich | |
darum überhaupt nicht mehr kümmern; die Fixture wird schon irgendwas passendes erzeugen, jetzt, | |
da sie weiß wie. | |
Dem angekündigten absurde Part etwas näher kommen wir, wenn man das nun mit dem Konzept des | |
Einfrierens zusammenwirft: Friere ich ein Mock<T> ein, greift die AutoMoqCustomization in der | |
Folge auch auf diese Instanz zurück, wenn der Typ T selbst angefragt wird - T ist damit also | |
effektiv mit eingefroren. | |
Das Gewöhnen daran und die selbstverständliche Hinnahme bzw. das Ausnutzen der Implikationen waren | |
zumindest für mein begrenztes kognitives Vermögen nicht unbedingt trivial - aber wenn man's dann | |
mal hat, ist es die Grundlage dafür, AutoFixture "richtig" zu nutzen. | |
Machen wir also das Mock kalt: | |
*/ | |
public class WieselFlinkWithFrozenMockTests | |
{ | |
[Fact] | |
public void Value_ReturnsWeazelizedCheezburgerName() | |
{ | |
// Arrange | |
var fixture = new Fixture().Customize(new AutoMoqCustomization()); | |
var cheezburgerName = fixture.Create<string>(); | |
var cheezburgerStub = fixture.Freeze<Mock<ICanHasCheezburger>>(); | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = fixture.Create<WieselFlink>(); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Das ist recht unspektakulär und demonstriert in der Form nur das oben Geschilderte. Gleichzeitig | |
markiert es aber das Ende der (im wesentlichen) rein imperativen Test-Unschuld, und wir beginnen | |
mit der WTF-Phase. | |
Dazu müssen wir uns zuerst parametrisierte Tests in xUnit.net angucken; das Konzept an sich gibt's | |
ja in jedem Test-Framework. In xUnit.net konkret etwas irritierend ist, daß einfache Testmethoden | |
das Attribut [Fact] bekommen, parametrisierte aber [Theory] heißen (und das TheoryAttribute noch | |
dazu in einer separaten Assembly namens xunit.extensions wohnt, was erst mit xUnit 2 geändert | |
wird). Dazu braucht's dann noch ein oder mehrere [InlineData]-Attribute, deren | |
Konstruktorparameter zur Signatur der Testmethode passen. | |
*/ | |
public class WieselFlinkWithStringParameterTests | |
{ | |
[Theory] | |
[InlineData("l0lrofl")] | |
[InlineData("iksdee <3")] | |
public void Value_ReturnsWeazelizedCheezburgerName(string cheezburgerName) | |
{ | |
// Arrange | |
var fixture = new Fixture().Customize(new AutoMoqCustomization()); | |
var cheezburgerStub = fixture.Freeze<Mock<ICanHasCheezburger>>(); | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = fixture.Create<WieselFlink>(); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Das ist eigentlich auch noch harmlos und unschuldig - Sinn der Sache ist schlicht, den selben | |
Sachverhalt für verschiedene Eingangswerte zu testen; daher werde ich in aller Regel auch mehr als | |
ein InlineDataAttribute haben. Das allerdings nur so lange, bis AutoFixture kommt und das Konzept | |
kidnappt. | |
Diese Zweckentfremdung besteht darin, daß das vom Programmierer explizit mit einem Wert pro | |
Test-Parameter gefütterte harmlose InlineDataAttribute durch ein deutlich gerisseneres ersetzt | |
wird, das mit Reflection auf die Parameter der Testmethode losgeht und dann in Zusammenarbeit mit | |
einer Fixture Falschgeld für die jeweiligen Typen druckt. | |
AutoFixture kennt dabei von sich aus zunächst mal das AutoDataAttribute, das | |
nachvollziehbarerweise mit einer unmodifizierten Fixture arbeitet und daher nur konkrete Typen | |
erzeugen kann. Aber für 'nen String reicht das ja auch schon: | |
*/ | |
public class WieselFlinkAutoDataAttributeTests | |
{ | |
[Theory, AutoData] | |
public void Value_ReturnsWeazelizedCheezburgerName(string cheezburgerName) | |
{ | |
// Arrange | |
var cheezburgerStub = new Mock<ICanHasCheezburger>(); | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
var roflCopter = new Mock<IRoflCopter>().Object; | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = new WieselFlink(cheezburgerStub.Object, roflCopter); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Ich habe an der Stelle innerhalb der Testmethode mal einen gewissen Rückbau vorgenommen und auf | |
die Fixture verzichtet - mit dem AutoDataAttribute hatten wir ja jetzt im Prinzip zwei, und das | |
kann irgendwie nicht Sinn der Angelengeheit sein; wir versuchen lieber, der einen, die wie jetzt | |
oben in dem Attribut haben, noch mehr beizubringen. | |
Dafür müssen wir uns allerdings jetzt unser eigenes Attribut bauen, das mit seiner internen | |
Fixture genau das gleiche macht wie wir vorhin mit unserer selbst erzeugten. Das geht | |
folgendermaßen: | |
*/ | |
public class AutoMoqDataAttribute : AutoDataAttribute | |
{ | |
public AutoMoqDataAttribute() | |
: base(new Fixture().Customize(new AutoMoqCustomization())) | |
{ | |
} | |
} | |
/* | |
Damit haben wir ein Attribut, das das gleiche kann wie das AutoDataAttribute, zusätzlich aber auch | |
noch in der gleichen Weise Mocks erzeugt wie vorhin unsere Fixture mit der AutoMoqCustomization - | |
weil genau die ja jetzt in unserem Attribut tickt. | |
Wir haben nun also wieder eine Fixture, die wir als SUT Factory benutzen könnten - aber wir haben | |
sie nicht in der Hand für die Einfrierei und den Create<WieselFlink>-Aufruf. Glücklicherweise löst | |
sie aber IFixture auf sich selbst auf, so daß wir sie uns einfach mit in den Test geben lassen | |
können: | |
*/ | |
public class WieselFlinkAutoMoqDataAttributeTests | |
{ | |
[Theory, AutoMoqData] | |
public void Value_ReturnsWeazelizedCheezburgerName(string cheezburgerName, IFixture fixture) | |
{ | |
// Arrange | |
var cheezburgerStub = fixture.Freeze<Mock<ICanHasCheezburger>>(); | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = fixture.Create<WieselFlink>(); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Naja, das bringt uns irgendwie so stückchenweise vorwärts und spart beispielsweise drei Zeilen im | |
Arrange gegenüber der Version in WieselFlinkWithFrozenMockTests, wirkt aber schon noch ein wenig | |
bemüht und holprig. Ja, wir müssen uns nicht mehr um den RoflCopter kümmern und haben uns des | |
expliziten Konstruktor-Aufrufs entledigt, aber dafür hantieren wir jetzt die ganze Zeit mit der | |
Fixture und haben im wesentlichen nur deren Setup in dem Attribut versteckt. So richtig | |
befriedigend mag das aber einfach nicht sein. | |
Und weil das so ist, hat AutoFixture dafür auch eine Lösung: Das Attribut [Frozen] für | |
Test-Parameter. Das sorgt dafür, daß die für einen Parameter der Testmethode erzeugte Instanz auch | |
für den jeweiligen Typ in der Fixture eingefroren wird, was wir ja bisher selbst machen mußten. | |
Das bedeutet also, daß wir uns das Mock jetzt gleich eingefroren geben lasen können, weil damit ja | |
auch die ICanHasCheezburger-Instanz für das spätere Erzeugen des SUT festgenagelt ist: | |
*/ | |
public class WieselFlinkWithFrozenMockParameterTests | |
{ | |
[Theory, AutoMoqData] | |
public void Value_ReturnsWeazelizedCheezburgerName( | |
string cheezburgerName, | |
[Frozen] Mock<ICanHasCheezburger> cheezburgerStub, | |
IFixture fixture) | |
{ | |
// Arrange | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
var sut = fixture.Create<WieselFlink>(); | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
So, jetzt kann man aber wirklich sehen, daß wir was erreicht haben. Unser allererster Test hatte | |
sechs Zeilen Code im Arrange-Teil; wir sind runter bis auf drei - die wir aber auch einfach alle | |
brauchen -, und das ganze liest sich auch irgendwie noch etwas deklarativer, finde ich. | |
Rekapitulieren wir, was hier passiert: | |
- xUnit findet eine Theory, sucht ein DataAttribute, findet das AutoMoqDataAttribute und fordert | |
von diesem die notwendigen Objekte für die Methodenparameter an. | |
- Das Attribut seinerseits hat sich eine Fixture erzeugt, ihr eine AutoMoqCustomization gegeben | |
und läßt sich nun von ihr die Objektinstanzen erzeugen - einen String, ein | |
Mock<ICanHasCheezburger>, das gleich eingefroren wird, eine Fixture, die *die* Fixture ist. | |
- Der Testrunner ruft die Testmethode mit den erzeugten Parameter-Instanzen auf. | |
- Das Mock wird so konfiguriert, daß die .Name-Property unseren "anonym" erzeugten String | |
zurückgibt. | |
- Unser erwartetes Ergebnis wird festgelegt. | |
- Die Fixture wird um das SUT gebeten. Der Konstruktor verlangt ein ICanHasCheezburger, bekommt | |
dafür das .Object des eingefrorenen Mocks, außerdem ein IRoflCopter, für das sich die Fixture | |
einfach ausdenkt; wird wohl auch ein Mock sein. | |
....Sekunde mal. Warum fragen wir die Fixture eigentlich noch irgendwas, wenn wir doch | |
festgestellt haben, daß das Attribut das sehr wohl selbst kann? Nix da: | |
*/ | |
public class WieselFlinkPreConstructedSutTests | |
{ | |
[Theory, AutoMoqData] | |
public void Value_ReturnsWeazelizedCheezburgerName( | |
string cheezburgerName, | |
[Frozen] Mock<ICanHasCheezburger> cheezburgerStub, | |
WieselFlink sut) | |
{ | |
// Arrange | |
cheezburgerStub.SetupGet(c => c.Name).Returns(cheezburgerName); | |
var expectedResult = cheezburgerName + " (weazelized)"; | |
// Act | |
var result = sut.Value; | |
// Assert | |
Assert.Equal(expectedResult, result); | |
} | |
} | |
/* | |
Damit haben wir effektiv die Zeile "var sut = fixture.Create<WieselFlink>();" in die | |
Methodensignatur verschoben (ich sagte ja, daß es absurd wird), und das ist nun wirklich unser | |
minimal notwendiger Test. Die zwei Zeilen im Arrange sind genau noch die spezifische | |
Testkonfiguration, während die benötigten Objektinstanzen deklarativ als Methodenparameter | |
angegeben sind und wir zwar im Prinzip wissen, wie sie erzeugt werden, uns aber nicht mehr konkret | |
dafür interessieren. Wir haben unsere Testumgebung einmal dafür präpariert, uns die Testdaten so | |
zu generieren, wie wir sie brauchen, und nehmen jetzt einfach nur noch hin, daß sie halt so | |
funktioniert. | |
*/ | |
// | |
// | |
// | |
/* | |
Ergänzende Anmerkungen dazu: | |
- AutoFixture-Tests sind natürlich bei weitem nicht immer so simpel, und es läßt sich auch oft | |
genug nicht vermeiden, im Test selbst doch noch mit der Fixture zu hantieren - aber auch in | |
komplexeren Tests nimmt einem AutoFixture in der Regel noch einiges an dumpfer Setup-Arbeit ab. | |
- Ein sinnvolles eigenes AutoDataAttribute wird in der Regel mehr als nur die AutoMoqCustomization | |
haben, beispielsweise eine, die wie erwähnt automatische Setzen von Properties unterbindet, | |
oder solche, die für bestimmte Abstraktionen Default-Implementationen vorgeben; alle zusammen | |
werden dann in eine CompositeCustomization gerollt. | |
- Das war nur ein schneller Durchstioh bis hin zur quasi "im Äther" existierenden SUT Factory. | |
AutoFixture ist äußerst komplex in seinen Verwendungsmöglichkeiten, von den wildesten | |
Konfigurations-Optionen für die Objekt-Erzeugung bis zu Typparametern für das FrozenAttribute. | |
- Man muß bei der Verwendung eins AutoData-Attributs auch keinesfalls auf parametrisierte Tests | |
verzichten, sondern kann genauso eigene InlineAutoData-Attribute definieren, bei denen die | |
Fixture dann alle die Werte erzeugt, die das Attribut nicht aus der eigenen Parameterliste | |
bedienen kann. | |
- Die Beschränkung der Attribut-Integration auf xUnit.net hat ihre Ursache offenbar nicht zuletzt | |
in dem Prinzip, das den xUnit-Attributen intern zugrunde liegt. Wäre vergleichbares etwa mit | |
NUnit möglich, hätte das wohl schon mal jemand in Angriff genommen. | |
- Aus meiner Sicht wird TDD erst dadurch wirklich vollständig, daß man nicht nur die | |
Funktionalität, sondern auch den Konstruktor explorativ entstehen lassen kann. Meine Klassen | |
beginnen stets ohne Konstruktor, ich setze in den Tests die Dependencies auf, deren Verwendung | |
maßgeblich ist, und sowie ich den Konstruktor entsprechend erweitere, fallen die Abhängigkeiten | |
automatisch dort rein, weil die Fixture sie schon bereithält. Für jeden neuen | |
Konstruktorparameter alle bereits existierenden Tests anpassen (und die entsprechende | |
Abhängigkeit erzeugen) zu müssen, macht einen solchen Vorgang doch erheblich mühseliger. | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment