Fluent Assertions in Unit Testing in C#
[[academy-video-youtube({"vid": "TytferBCLOo", "start_time": "0", "title": "Fluent Assertions in Unit Testing in C#", "creator": "Tim Corey", "length": "10m 6s"})]]
Unittests werden einmal geschrieben und viele Male gelesen, was die Assertionszeile zu einem der wichtigsten Details in einer Testsuite macht. Assert.Equal(expected, actual) funktioniert, aber eine Fehlermeldung, die nur zwei Strings vergleicht, erklärt nicht immer, was der Test überprüfte. Fluent Assertions formt die Assertions-Syntax um, sodass die Zeile wie die Regel liest, die sie durchsetzt, und die Fehlermeldung zu einem Satz wird, anstatt einem Unterschied.
In seinem Video "Fluent Assertions in Unit Testing in C#" nimmt Tim Corey ein xUnit-Projekt, das bereits einen SampleClass Happy Path überprüft, und führt dann einen Randfall (Eddie Van Halen) ein, der die einfache Leerraum-Trenner-Logik bricht. Er installiert das Fluent Assertions-Paket, schreibt die Assertion mit der .Should().Be() Kette um, demonstriert, wie man mehrere Bedingungen an einem einzigen Wert verketten kann, und schließt mit AssertionScope ab, sodass ein einzelner Test alle fehlschlagenden Bedingungen auf einmal meldet, anstatt beim ersten zu stoppen. Teams, deren Test-Suite über den Punkt hinaus gewachsen ist, an dem rudimentäre Gleichheitsunterschiede klar lesbar sind, finden hier den Upgrade-Pfad.
Der xUnit Ausgangspunkt
[0:35 - 2:46] Das Projekt auf dem Bildschirm ist klein: eine Klassenbibliothek mit einer einzigen SampleClass, die einen vollständigen Namen in ihrem Konstruktor entgegennimmt und ihn im Raum in FirstName und LastName aufteilt, sowie ein xUnit-Testprojekt, das den Happy Path überprüft. Die Tests verwenden Theorie mit InlineData-Attributen, um die gleiche Logik gegen zwei Eingaben ("Tim Corey" und "Sue Storm") auszuführen, und jede Assertion hat die Standardform Assert.Equal(expected, actual).
[Theory]
[InlineData("Tim Corey", "Tim")]
[InlineData("Sue Storm", "Sue")]
public void TestFirstNameProperty(string fullName, string expected)
{
var sample = new SampleClass(fullName);
Assert.Equal(expected, sample.FirstName);
}
[Theory]
[InlineData("Tim Corey", "Tim")]
[InlineData("Sue Storm", "Sue")]
public void TestFirstNameProperty(string fullName, string expected)
{
var sample = new SampleClass(fullName);
Assert.Equal(expected, sample.FirstName);
}
Das Ausführen der Suite ergibt sechs bestandene Tests. Nichts daran ist falsch, und für die Fälle, die "Erstes Leerzeichen Letztes" passen, lesen die Assertions klar genug. Das interessante Gebiet beginnt, wenn ein Name nicht dieser Form entspricht.
Wo der Erfolgsweg nicht mehr funktioniert
[2:46 - 4:32] Der Randfall, den Tim auswählt, ist "Eddie Van Halen", ein dreigliedriger Name, bei dem der Nachname "Van Halen" ist, nicht nur das Token nach dem ersten Leerzeichen. Die aktuelle Implementierung nimmt Index 1 des Splits und nennt ihn den Nachnamen, sodass die Klasse "Van" für den Nachnamen zurückgibt und "Halen" vollständig verwirft. Der Fehler ist echt, und einen fehlgeschlagenen Test dafür zu schreiben, ist der erste Schritt zur Behebung.
Das Schreiben dieses Tests mit Assert.Equal("Van Halen", sample.LastName) funktioniert, aber die Fehlermeldung liest sich als Stringvergleich ohne Kontext. Der Wechsel zu Fluent Assertions lässt den gleichen Test wie die Regel in Worten ausdrücken und erzeugt eine Fehlermeldung, die die zu testende Eigenschaft benennt. Für eine kleine Suite sieht der Unterschied kosmetisch aus; für eine Suite mit einigen hundert Assertions summiert sich der Lesbarkeitsunterschied.
Fluent Assertions installieren
[4:32 - 5:10] Das Paket ist auf NuGet unter FluentAssertions verfügbar. Tim hebt es als eines der am häufigsten heruntergeladenen Pakete im Registry hervor, was nützlicher Kontext ist: Jeder neue Beitragende wird statistisch gesehen es wahrscheinlich schon zuvor gesehen haben. Das Testprojekt erhält zwei Using-Direktiven oben:
using FluentAssertions;
using FluentAssertions.Execution;
using FluentAssertions;
using FluentAssertions.Execution;
Die erste bringt die Assertion-Extension-Methoden ein, was die meisten Tests benötigen. Der zweite existiert für den AssertionScope Typ, der später im Video behandelt wird; Tests, die keine Assertions gruppieren, können darauf verzichten.
Die Should.Be-Syntax
[5:10 - 6:14] The minimum Fluent Assertions rewrite of the Van Halen test:
[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
sample.LastName.Should().Be("Van Halen");
}
[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
sample.LastName.Should().Be("Van Halen");
}
Die Kette liest sich nahe an gesprochener englischer Sprache. Die .Should() Erweiterung gibt ein Assertionsobjekt zurück, dessen Methoden den Vergleich beschreiben; .Be(...) führt eine Gleichheitsprüfung durch. Groß- und Kleinschreibungsempfindlichkeit, angeführte Leerzeichen und nachfolgende Leerzeichen sind allesamt Teil dieses Checks, der dem Verhalten der meisten Zeichenfolgenvergleiche in Produktionscode entspricht.
Das Ausführen dieses Tests gegen die fehlerhafte Implementierung erzeugt eine Fehlermeldung, die die Eigenschaft benennt und die Lücke erklärt: "Erwartete sample.LastName, um 'Van Halen' mit einer Länge von 9 zu sein, aber 'Van' hat eine Länge von 3." Die Nachricht identifiziert den Wert im Test durch den Ausdruck, der ihn erzeugte, was die Art von Kontext ist, die die nackte Assert.Equal Form nicht bietet.
Mehrere Bedingungen verketten
[6:14 - 8:10] Eine einzelne Eigenschaft muss oft mehr als eine Regel erfüllen. Fluent Assertions komponiert Bedingungen mit .And, sodass die Kette in einer einzigen Aussage bleibt:
sample.LastName.Should()
.StartWith("Van")
.And.EndWith("len")
.And.Contain(" ");
sample.LastName.Should()
.StartWith("Van")
.And.EndWith("len")
.And.Contain(" ");
Jeder Link in der Kette ist eine separate Bedingung. Standardmäßig stoppt die Kette beim ersten Fehler zu berichten: wenn StartWith("Van") bestanden wird, aber EndWith("len") fehlschlägt, wird die Fehlermeldung EndWith nennen und die Assertion hört dort auf. Für Tests, bei denen jede Bedingung unabhängig ist und Sie möchten, dass jeder Fehler aufgedeckt wird, ändert der Assertion Scope (nächster Abschnitt) dieses Verhalten.
Die Verkettungssyntax neigt dazu, die Absicht über die Aufzählen von Gleichheiten auszudrücken. Ein Test, der sagt "Der Nachname sollte mit Van beginnen, mit len enden und ein Leerzeichen enthalten" liest sich wie eine Spezifikation. Die gleiche Logik, geschrieben mit drei separaten Assert.True Aufrufen, würde als drei boolesche Überprüfungen gelesen werden, ohne dass eine Erzählung sie verbindet.
Jeden Fehler melden mit Assertion Scopes
[8:10 - 9:34] Das Standardverhalten, beim ersten Fehler zu stoppen, ist gut für Ketten, bei denen spätere Bedingungen von früheren abhängen. Für Ketten, bei denen jede Bedingung unabhängig wichtig ist, sorgt das Einwickeln der Assertions in einen AssertionScope dafür, dass der Test bei einem Durchlauf jeden Fehler meldet:
[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
using var _ = new AssertionScope();
sample.LastName.Should().StartWith("Van");
sample.LastName.Should().EndWith("len");
sample.LastName.Should().Contain(" ");
}
[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
using var _ = new AssertionScope();
sample.LastName.Should().StartWith("Van");
sample.LastName.Should().EndWith("len");
sample.LastName.Should().Contain(" ");
}
Die using-var-discard-Zeile ist das Standardmuster: der Scope dauert, bis die Variable am Ende der Methode aus dem Scope fällt, und das Discard-Underscore signalisiert, dass die Variable selbst niemals gelesen wird. Wenn der Test fehlschlägt, enthält die Meldung jede fehlgeschlagene Bedingung als separate Zeile, was bedeutet, dass ein Durchlauf die ganze Geschichte gibt, anstatt drei Durchläufe zu erzwingen, um drei Probleme aufzudecken. Für Tests, die die Form eines zurückgegebenen Objekts über viele Eigenschaften hinweg verifizieren, ist dies der Unterschied zwischen einem Fehler pro Testzyklus zu beheben und alle auf einmal zu beheben.
Abschluss: Tests, die wie Spezifikationen lesen
[9:34 - 10:06] Fluent Assertions ändert nicht, was Tests überprüfen; es ändert, wie die Überprüfung sich liest. Die .Should() Kette, die .And Komposition und die AssertionScope Gruppierung bringen den Testcode näher an eine schriftliche Spezifikation des zu testenden Verhaltens. Kombiniert mit der gleichen Art von fließendem Stil, die von FluentValidation für die Eingabevalidierung verwendet wird, erzeugt das Muster Testsuiten und Validierungsregeln, die Neulinge lesen können, ohne eine Tour zu benötigen.
Abschluss
[9:34 - 10:06] Das Hinzufügen von Fluent Assertions zu einem Testprojekt erfordert eine NuGet-Installation, zwei using-Direktiven und ein Neuschreiben der Assertionszeile von Assert.Equal(expected, actual) zu actual.Should().Be(expected). Ketten drücken Mehrbedingungsregeln in einer einzigen Aussage aus, und AssertionScope macht einen einzigen Test, um jeden Fehler zu melden, anstatt beim ersten zu stoppen. Der Nutzen sind Fehlermeldungen, die die Regel erklären, die gebrochen wurde, was der Unterschied zwischen einem Stack-Trace und einem Satz ist.
Beispieltipp: Beim Assertions auf Sammlungen, bevorzugen Sie result.Should().BeEquivalentTo(expected) gegenüber dem Vergleich von Eigenschaft zu Eigenschaft. Es durchläuft den Objektgraphen für Sie und meldet die erste abweichende Eigenschaft nach Pfad (zum Beispiel users[2].Address.City), was weit nützlicher ist als eine "Sammlungen sind nicht gleich" Nachricht, wenn eine 50-Elemente-Liste bei einem verschachtelten Feld nicht übereinstimmt.
Schauen Sie sich das vollständige Video auf seinem YouTube Kanal an und erhalten Sie weitere Einblicke in das Schreiben lesbarer, wartbarer C#-Tests in der 10-Minuten-Trainingsserie.
