- Neues Projekt
MockingBeispiel
als .Net Core 2.1 erstellen. - xUnit-Testprojekt
MockingBeispiel.Tests
erstellen. - Referenz von Test-Projekt
MockingBeispiel.Tests
zuMockingBeispiel
erstellen. - Wechsel zum Projekt
MockingBeispiel
. - Ordner "Interfaces" in Projekt erstellen.
- Um mit dem System zu interagieren, brauchen wir als erstes einen Interaktor. Dies implementieren wir zunächst als Schnittstelle. Warum wir dies tun, wird später erläutert. Erstellen einer Schnittstelle für
IKundenInteraktor
.public interface IKundenInteraktor { }
- Neue Methode "SucheKunden" zur Schnittstelle
IKundenInteraktor
hinzufügen:KomplettList<Kunde> SucheKunden(string filter);
Der Typpublic interface IKundenInteraktor { List<Kunde> SucheKunden(string filter); }
Kunde
ist unbekannt und muss nun erstellt werden. - Neuen Ordner
Models
unterhalb des ProjektesMockingBeispiel
erstellen. - Neue Klasse
Kunde
unterhalb des OrdnersModels
anlegen.public class Kunde { public int Id { get; set; } public string Name { get; set; } }
- Die Datei für
IKundenInteraktor
sollte nun folgendermaßen aussehen:using System.Collections.Generic; using MockingBeispiel.Models; namespace MockingBeispiel.Interfaces { public interface IKundenInteraktor { List<Kunde> SucheKunden(string filter); } }
- Einen ähnlichen Konstrukt brauchen wir auch für den Zugriff auf den Datenspeicher für die Kunden.
- Neue Schnittstelle
IKundenDatenspeicher
unterhalb des OrdnersInterfaces
erstellen:using System.Collections.Generic; using MockingBeispiel.Models; namespace MockingBeispiel.Interfaces { public interface IKundenDatenspeicher { List<Kunde> SucheKunden(string filter); } }
- Als nächstes brauchen wir noch eine Schnittstelle für das Protokollieren einer Informationsmeldung und eines Fehlers. Hierzu eine neue Schnittstelle namens
IProtokollierer
unterhalb des OrdnersInterfaces
erstellen.namespace MockingBeispiel.Interfaces { public interface IProtokollierer { void ProtokolliereInformation(string meldung); void ProtokolliereFehler(string meldung); } }
- Da wir die "Interaktion" testen wollen, implementieren wir zunächst eine leere Hülle
- Erstellen des Ordners
Implementations
unterhalb des ProjektesMockingBeispiel
- Erstellen der Klasse
KundenInteraktor
im OrdnerImplementations
- Da die Klasse
KundenInteraktor
die SchnittstelleIKundenInteraktor
implementieren soll, muss dies als solches deklariert werden.using System; using System.Collections.Generic; using MockingBeispiel.Interfaces; using MockingBeispiel.Models; namespace MockingBeispiel.Implementations { public class KundenInteraktor : IKundenInteraktor { public List<Kunde> SucheKunden(string filter) { throw new NotImplementedException(); } } }
- Derzeit wirft die Methode
SucheKunden
noch die AusnahmeNotImplementedException
. Dies ist vollkommen okay, da wir derzeit noch keine Logik haben und diese später anhand der Tests ersetzen. - Da wir den Interaktor verwenden wollen, um Daten aus einem Datenspeicher (
IKundenDatenspeicher
) und Meldungen protokollieren wollen (IProtokollierer
), müssen wir diese referenzieren. Dies geschieht über eine Konstruktor-Injektion. In diesem Fall müssen wir beide Schnittstelle dem Konstruktor der KlasseKundenInteraktor
übergeben und speichern.Komplette Datei fürprivate readonly IKundenDatenspeicher _kundenDatenspeicher; private readonly IProtokollierer _protokollierer; public KundenInteraktor( IKundenDatenspeicher kundenDatenspeicher, IProtokollierer protokollierer) { _kundenDatenspeicher = kundenDatenspeicher; _protokollierer = protokollierer; }
KundenInteraktor
using System; using System.Collections.Generic; using MockingBeispiel.Interfaces; using MockingBeispiel.Models; namespace MockingBeispiel.Implementations { public class KundenInteraktor : IKundenInteraktor { private readonly IKundenDatenspeicher _kundenDatenspeicher; private readonly IProtokollierer _protokollierer; public KundenInteraktor( IKundenDatenspeicher kundenDatenspeicher, IProtokollierer protokollierer) { _kundenDatenspeicher = kundenDatenspeicher; _protokollierer = protokollierer; } public List<Kunde> SucheKunden(string filter) { throw new NotImplementedException(); } } }
- Nun wollen wir den ersten Test schreiben, in dem es darum geht, wenn der Datenspeicher 0 Datensätze zurückgibt, dass eine leere Liste von Kunden zurückgegeben wird und die Informationsmeldung "Keine Kunden gefunden" protokolliert wird.
- Um das Verhalten des Datenspeichers und des Protokollierers zu simulieren, benötigen wir zunächst noch ein Mocking-Framework. Hierzu verwnde ich das Moq.
- Moq muss nun als NuGet-Paket in das Test-Projekt
MockingBeispiel.Tests
eingebunden werden. - Dazu in Visual Studio unter "Tools" > "Nuget Package Manager", den Eintrag "Package Manager Console" öffnen und folgenden Befehl ausführen:
Install-Package Moq -ProjectName MockingBeispiel.Tests
- Warten, bis alls abhängigen Pakete installiert sind
- Um die Tests lesbarer zu machen, installieren wir auch das NuGet-Package Shouldly:
Install-Package Shouldly -ProjectName MockingBeispiel.Tests
- Im Test-Projekt
MockingBeispiel.Tests
nun folgende Test-Klasse erstellen:KundenInteraktorTests
. Diese muss nun folgendermaßen aussehen:using System; using Xunit; namespace MockingBeispiel.Tests { public class KundenInteraktorTests { [Fact(DisplayName = "Datenspeicher gibt 0 Datensaetze zurück. Erwarte leere Liste und protokolliere Meldung \"Keine Kunden gefunden\".")] public void Datenspeicher_gibt_0_Datensaetze_zurueck_Erwarte_leere_Liste_und_protokolliere_Meldung() { throw new NotImplementedException(); } } }
- Im nächsten Schritt müssen wir unser "System" (
KundenInteraktor
), das wir testen wollen, für die Simulierung der anderen Systeme (Datenspeicher, Protokollierer) vorbereiten. Dazu müssen wir die SchnittstellenIKundenDatenspeicher
undIProtokollierer
mocken und demKundenInteraktor
übergeben. Dies tun wir im Konstruktor der TestklasseKundenInteraktorTests
. Die Datei muss nun folgendermaßen aussehen:using MockingBeispiel_20181019.Interfaces; using MockingBeispiel_20181019.Implementations; using System; using Moq; using Xunit; namespace MockingBeispiel_20181019.Tests { public class KundenInteraktorTests { private readonly KundenInteraktor _systemUnde 8000 rTest; private readonly Mock<IKundenDatenspeicher> _mockKundenDatenspeicher; private readonly Mock<IProtokollierer> _mockProtokollierer; public KundenInteraktorTests() { _mockKundenDatenspeicher = new Mock<IKundenDatenspeicher>(); _mockProtokollierer = new Mock<IProtokollierer>(); _systemUnderTest = new KundenInteraktor( kundenDatenspeicher: _mockKundenDatenspeicher.Object, protokollierer: _mockProtokollierer.Object); } [Fact(DisplayName = "Datenspeicher gibt 0 Datensaetze zurück. Erwarte leere Liste und protokolliere Informationsmeldung \"Keine Kunden gefunden\".")] public void Datenspeicher_gibt_0_Datensaetze_zurueck_Erwarte_leere_Liste_und_protokolliere_Informationsmeldung() { throw new NotImplementedException(); } } }
- Nun wollen wir den ersten Test mit folgenden Anforderungen implementieren, wenn die Suche 0 Datensätze zurückgibt:
- Gebe leere Liste zurück
- Protokolliere "Keine Kunden gefunden"
- Der Test muss nun folgenermaßen aussehen:
public void Datenspeicher_gibt_0_Datensaetze_zurueck_Erwarte_leere_Liste_und_protokolliere_Informationsmeldung() { // Arrange _mockKundenDatenspeicher .Setup(expression: s => s.SucheKunden(It.IsAny<string>())) .Returns(value: new List<Kunde>()); // Act List<Kunde> actual = _systemUnderTest.SucheKunden(filter: null); // Assert actual.ShouldNotBeNull(); actual.ShouldBeEmpty(); _mockProtokollierer.Verify( expression: v => v.ProtokolliereInformation("Keine Kunden gefunden"), times: Times.Once); }
- Arrange
- Mit der Methode
Setup
kann man einen Ausdruch angeben, der ausgeführt werden soll. Die MethodeReturns
gibt dann an, welcher Wert dafür zurückgegeben werden soll. Dies ist die "Simulierung".
- Mit der Methode
- Act
- Hier die eigentliche Funktion aufgerufen
- Assert
- Als erstes überprüfen wir mit
actual.ShouldNotBeNull();
, dass der Wert inactual
nichtnull
ist. - Danach testen wir mit
actual.ShouldBeEmpty();
, dass die Liste, die zurückgegeben wurde, leer ist. - Zuletzt überprüfen wir, dass die Meldung "Keine Kunden gefunden" protokolliert wurde. Dazu nutzen wir die Methode
Verify
des Mocking-Objektes fürIProtokollierer
.- Der Parameter
expression
gibt an, welches Ausdruck überprüft werden soll. - Der Parameter
times
gibt an, wie oft der Ausdruck gelaufen sein muss. In diesem Fall ein mal.
- Der Parameter
- Als erstes überprüfen wir mit
- Arrange
- Wenn wir den Test im "Test-Explorer" laufen lassen, erscheint in der Meldung des Testes folgender Test: Message: System.NotImplementedException : The method or operation is not implemented.. Dies kommt daher, dass die Methode
KundenInteraktor.SucheKunden(string filter)
noch die AusnahmeNotImplementedException
geworfen wird. Dies wollen wir nun ändern, in dem wir die Implementierung anpassen. Damit der Code grün und "TDD-Konform" ist, sollte die Methode fürKundenInteraktor.SucheKunden(string filter)
folgendermaßen aussehen:public List<Kunde> SucheKunden(string filter) { _protokollierer.ProtokolliereInformation(meldung: "Keine Kunden gefunden"); return _kundenDatenspeicher.SucheKunden(filter: filter); }
- Wenn wir den Test nun noch einmal ausführen, wird er grün.
- Nun wollen wir den nächsten Test implementieren, wenn die suche beispielsweise 2 Datensätze zurückgibt:
- Liste mit 2 Datensätzen
- Protokolliere "2 Kunden gefunden"
- Der Test muss nun folgendermaßen aussehen:
[Fact(DisplayName = "Datenspeicher gibt 2 Datensaetze zurück. Erwarte Liste mit 2 Datensätzen und protokolliere Informationsmeldung \"2 Kunden gefunden\".")] public void Datenspeicher_gibt_2_Datensaetze_zurueck_Erwarte_Liste_mit_2_Datensaetzen_und_protokolliere_Meldung() { // Arrange _mockKundenDatenspeicher .Setup(expression: s => s.SucheKunden(It.IsAny<string>())) .Returns(value: new List<Kunde>() { new Kunde { Id = 1, Name = "Kunde 1" }, new Kunde { Id = 2, Name = "Kunde 2" } }); // Act List<Kunde> actual = _systemUnderTest.SucheKunden(filter: null); // Assert actual.ShouldNotBeNull(); actual.Count.ShouldBe(expected: 2); _mockProtokollierer.Verify( expression: v => v.ProtokolliereInformation("2 Kunden gefunden"), times: Times.Once); }
- Folgende Dinge haben wir nun zum vorherigen Test geändert
- Es werden aus dem KundenDatenspeicher keine Einträge mehr geliefert, sondern jetzt 2 Kunden.
- Die Anzahl der Einträge soll 2 betragen:
actual.Count.ShouldBe(expected: 2);
⚠️ Dies ist natürlich nur ein "weicher" Test. Es empfiehlt sich die Objekte miteinander zu vergleichen. Ist aber nicht Inhalt dieser Anleitung. Siehe Definieren von Wertgleichheit für einen Typ- Es soll jetzt "2 Kunden gefunden" protokolliert werden.
- Wenn wir jetzt alle Tests ausführen, ist der erste Test, den wir geschrieben haben, weiterhin grün. Aber der neu erstellte Test rot.
- Nun müssen wir den Test grün bekommen und die Methode
KundenInteraktor.SucheKunden(string filter)
anpassen. Sie muss nun folgenermaßen aussehen:public List<Kunde> SucheKunden(string filter) { List<Kunde> gefundeneKunden = _kundenDatenspeicher.SucheKunden(filter: filter); if (gefundeneKunden.Any()) { _protokollierer.ProtokolliereInformation(meldung: $"{gefundeneKunden.Count} Kunden gefunden"); } else { _protokollierer.ProtokolliereInformation(meldung: "Keine Kunden gefunden"); } return gefundeneKunden; }
- Wenn wir alle Tests ausführen, sind nun beide Tests grün. Nun können wir in diesem Schritt einmal den Code umgestalten. Dies könnte so aussehen:
public List<Kunde> SucheKunden(string filter) { List<Kunde> gefundeneKunden = _kundenDatenspeicher.SucheKunden(filter: filter); _protokollierer.ProtokolliereInformation(meldung: gefundeneKunden.Any() ? $"{gefundeneKunden.Count} Kunden gefunden" : "Keine Kunden gefunden"); return gefundeneKunden; }
- Nochmaliges ausführen aller Tests bringt die Gewissheit, der Code funktioniert immer noch, wie die Tests es vorgeben.
- Nun zum letzten Test, wenn ein Fehler im KundenDatenspeicher auftritt
- Leere Liste zurückgeben
- Fehlermeldung "Es ist ein Fehler aufgetreten" protokollieren
- Dieser Test muss nun folgendermaßen aussehen:
[Fact(DisplayName = "Datenspeicher wirft einen Fehler. Erwarte leere Liste und protokolliere Fehlermeldung \"Es ist ein Fehler aufgetreten\".")] public void Datenspeicher_wirft_Fehler_Erwarte_leere_Liste_und_protokolliere_Fehlermeldung() { // Arrange _mockKundenDatenspeicher .Setup(expression: s => s.SucheKunden(It.IsAny<string>())) .Throws<Exception>(); // Act List<Kunde> actual = _systemUnderTest.SucheKunden(filter: null); // Assert actual.ShouldNotBeNull(); actual.ShouldBeEmpty(); _mockProtokollierer.Verify( expression: v => v.ProtokolliereFehler("Es ist ein Fehler aufgetreten"), times: Times.Once); }
- Alle Tests ausführen führt dazu, dass der neue Test fehlschlägt. Das heißt, wir müssen wieder die Implementierung von
KundenInteraktor.SucheKunden(string filter)
anpassen. Die Implementierung würde nun folgendermaßen aussehen:public List<Kunde> SucheKunden(string filter) { try { List<Kunde> gefundeneKunden = _kundenDatenspeicher.SucheKunden(filter: filter); _protokollierer.ProtokolliereInformation(meldung: gefundeneKunden.Any() ? $"{gefundeneKunden.Count} Kunden gefunden" : "Keine Kunden gefunden"); return gefundeneKunden; } catch { _protokollierer.ProtokolliereFehler(meldung: "Es ist ein Fehler aufgetreten"); } return new List<Kunde>(); }
- Ein erneutes ausführen aller Tests führt dazu, dass nun alle Tests grün sind.
ℹ️ Vorbereitung:
Als Vorbereitung für die Implementierungen, muss für das Beispiel das NuGet-Package Autofac. Dieses Framework dient zur Umsetzung des Inversion of Control-Prinzipes unter Einsatz von Dependency Injection.
Install-Package Autofac -ProjectName MockingBeispiel
- Im Projekt für
MockingBeispiel
erstellen wir im OrdnerImplementations
die KlasseStammKundenDatenspeicher
und implementieren diese mit der SchnittstelleIKundenDatenspeicher
. Die Datei könnte folgendermaßen aussehen:using System.Collections.Generic; using System.Linq; using MockingBeispiel.Interfaces; using MockingBeispiel.Models; namespace MockingBeispiel.Implementations { public class StammKundenDatenspeicher : IKundenDatenspeicher { public List<Kunde> SucheKunden(string filter) { return new List<Kunde> { new Kunde { Id= 1, Name = "Karla Kolumna" }, new Kunde { Id= 2, Name = "Tierpfleger Karl" }, new Kunde { Id= 3, Name = "Benjamin Blümchen" } } .Where(predicate: p => p.Name.ToLower().Contains(value: filter.ToLower())) .ToList(); } } }
- Als nächstes erstellen wir im Ordner eine Implementierung für
IProtokollierer
mit dem KlassennameKonsolenProtokollierer
. Die Datei könnte folgendermaßen aussehen:using System; using MockingBeispiel.Interfaces; namespace MockingBeispiel.Implementations { public class KonsolenProtokollierer : IProtokollierer { public void ProtokolliereInformation(string meldung) { Console.WriteLine(value: meldung); } public void ProtokolliereFehler(string meldung) { Console.WriteLine(value: meldung); } } }
- Im nächsten Schritt wollen wir die einzelnen Module "zusammenbauen" und mal etwas sehen 😏. Dazu gehen wir zur
Main
-Methode in der DateiProgram.cs
. Der Code könnte folgendermaßen aussehen:Erläuterung:using System; using Autofac; using MockingBeispiel.Implementations; using MockingBeispiel.Interfaces; using MockingBeispiel.Models; namespace MockingBeispiel { public static class Program { public static void Main(string[] args) { ContainerBuilder containerBuilder = new ContainerBuilder(); containerBuilder .RegisterType<KundenInteraktor>() .As<IKundenInteraktor>(); containerBuilder .RegisterType<KonsolenProtokollierer>() .As<IProtokollierer>(); containerBuilder .RegisterType<StammKundenDatenspeicher>() .As<IKundenDatenspeicher>(); IKundenInteraktor kundenInteraktor = containerBuilder .Build() .Resolve<IKundenInteraktor>(); foreach (Kunde kunde in kundenInteraktor.SucheKunden(filter: "")) { Console.WriteLine(value: $"{kunde.Id} - {kunde.Name}"); } Console.WriteLine(value: "Fertsch"); Console.ReadKey(); } } }
- Die Klasse
ContainerBuilder
dient dazu, die einzelnen Schnittstellen und Implementierungen zu registrieren. - Die Methode
RegisterType<TImplementer>
gibt an, welche Implementierung genutzt werden soll. - Die Methode
As<TService>
gibt an, welche Schnittstelle genutzt werden soll. - Die Methode
Build
"baut" die registrierten Komponenten und die MethodeResolve<TService>
löst die Implementierung automatisch auf. - Zuletzt wird die Suche ausgeführt und durch die Liste iteriert und ausgegeben. Die Konsole sollte nun folgendes ausgeben:
3 Kunden gefunden 1 - Karla Kolumna 2 - Tierpfleger Karl 3 - Benjamin Blümchen Fertsch
- Die Klasse
- Nun sind wir in der Lage, weitere Implementierungen für
IKundenDatenspeicher
undIProtokollierer
zu erstellen, ohne die Implementierung vonKundenInteraktor
anzupassen. Die Daten für den Datenspeicher können nun aus einer Datenbank kommen, aus einer JSON-Datei, aus einer externen API, etc. Das selbe gilt für den Protokollierer. Ausgabe in verschiedenen Farben auf der Konsole, Datei-Ausgabe, Windows-Ereignis, Nutzung einer externen Komponente (z.B. NLog, log4net, etc.) ist nun möglich. Zur Wiederholung: Ohne Anpassung der Implementierung vonKundenInteraktor
.
- Im Ordner presentation liegt die PowerPoint-Präsentation
- Das in dieser Anleitung durchgeführte Beispiel liegt im Ordner
/src/Beispiel
- Das Beispiel mit dem Licht liegt im Ordner
/src/BeispielDependencyInversionMitLicht
Wünsche und Anregungen gerne als Issue erfassen. Nichts ist natürlich perfekt. Aber ich hoffe, ich konnt ein wenig zu diesem sehr interessanten Thema beitragen.
Dynamische Grüße
ENDE