Skip to content

Instantly share code, notes, and snippets.

@TeaDrivenDev
Last active October 19, 2020 18:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TeaDrivenDev/5992922 to your computer and use it in GitHub Desktop.
Save TeaDrivenDev/5992922 to your computer and use it in GitHub Desktop.
Warum sehen AutoFixture-Tests so seltsam aus, und welche Vorteile hat das?
/* 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