Fluent Assertions w testach jednostkowych w C#
[[academy-video-youtube({"vid": "TytferBCLOo", "start_time": "0", "title": "Fluent Assertions in Unit Testing in C#", "creator": "Tim Corey", "length": "10m 6s"})]]
Testy jednostkowe są pisane raz i czytane wiele razy, co sprawia, że linia asercji jest jednym z najistotniejszych detali w zestawie testów. Assert.Equal(expected, actual) działa, ale wiadomość o błędzie, która po prostu porównuje dwa ciągi znaków, nie zawsze wyjaśnia, co test sprawdzał. Fluent Assertions przekształca składnię asercji, tak aby linia czytała się jak reguła, którą egzekwuje, a komunikat o błędzie staje się zdaniem, a nie różnicą.
W swoim filmie "Fluent Assertions w testowaniu jednostkowym w C#" Tim Corey bierze projekt xUnit, który już testuje SampleClass happy path, a następnie wprowadza przypadek brzegowy (Eddie Van Halen), który łamie prostą logikę podziału po spacji. Instaluje pakiet Fluent Assertions, przepisuje asercję używając łańcucha .Should().Be(), pokazuje, jak łączyć wiele warunków na jednej wartości, i kończy AssertionScope, dzięki czemu pojedynczy test raportuje każde błędne warunki jednocześnie, zamiast przerywać na pierwszym. Zespoły, których zestaw testów urósł do punktu, w którym same porównania równości nie są już czytelne, znajdą tu ścieżkę aktualizacji.
Punkt startowy xUnit
[0:35 - 2:46] Projekt na ekranie jest mały: biblioteka klas z jednym SampleClass, który przyjmuje pełne imię i nazwisko w konstruktorze i dzieli je na poszczególne FirstName i LastName, oraz projekt testowy xUnit, który testuje happy path. Testy używają teorii z atrybutami InlineData do wykonania tej samej logiki względem dwóch danych wejściowych ("Tim Corey" i "Sue Storm"), a każda asercja to standardowa forma 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);
}Uruchomienie zestawu daje sześć przechodzących testów. Nic w tym nie jest złe, a dla przypadków, które pasują do "pierwsza spacja ostatnia", asercje są wystarczająco czytelne. Interesujące terytorium zaczyna się, gdy imię nie pasuje do tego kształtu.
Gdzie pozytywna ścieżka przestaje działać
[2:46 - 4:32] Przypadek brzegowy, który wybiera Tim to "Eddie Van Halen", trzyelementowe imię, w którym nazwisko to "Van Halen", a nie tylko token po pierwszej spacji. Obecna implementacja pobiera indeks 1 z podziału i nazywa go nazwiskiem, więc klasa zwraca "Van" jako nazwisko, całkowicie pomijając "Halen". Błąd jest prawdziwy, a napisanie testu, który kończy się niepowodzeniem, jest pierwszym krokiem do naprawy.
Pisanie tego testu z Assert.Equal("Van Halen", sample.LastName) działa, ale wiadomość o błędzie wygląda jak porównanie ciągów znaków bez kontekstu. Przejście na Fluent Assertions sprawia, że ten sam test wyraża zasadę w słowach i produkuje komunikat o błędzie, który nazywa testowaną właściwość. Dla małego zestawu różnica wygląda na kosmetyczną; dla zestawu kilku setek asercji różnica w czytelności się zwiększa.
Installing Fluent Assertions
[4:32 - 5:10] Pakiet znajduje się na NuGet pod FluentAssertions. Tim zaznacza go jako jeden z najczęściej pobieranych pakietów w rejestrze, co jest użytecznym kontekstem: każdy nowy współtwórca statystycznie prawdopodobnie już go widział. Projekt testowy otrzymuje dwie dyrektywy using na górze:
using FluentAssertions;
using FluentAssertions.Execution;using FluentAssertions;
using FluentAssertions.Execution;Pierwsza wprowadza metody rozszerzenia asercji, czego potrzebuje większość testów. Drugi istnieje dla typu AssertionScope omówionego później w filmie; testy, które nie grupują asercji, mogą ją pominąć.
Składnia Should.Be
[5:10 - 6:14] Minimalna przepisana wersja Fluent Assertions testu Van Halen:
[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");
}Łańcuch czyta się blisko mówionego języka angielskiego. Rozszerzenie .Should() zwraca obiekt asercji, którego metody opisują porównanie; .Be(...) wykonuje sprawdzenie równości. Wrażliwość na wielkość liter, wiodące i końcowe spacje są częścią tego sprawdzenia, co odpowiada zachowaniu większości porównań łańcuchów w kodzie produkcyjnym.
Uruchomienie tego testu na wadliwym wdrożeniu generuje wiadomość o błędzie, która nazywa właściwość i wyjaśnia różnicę: "Oczekiwano, że sample.LastName będzie 'Van Halen' o długości 9, ale 'Van' ma długość 3." Wiadomość identyfikuje testowaną wartość przez wyrażenie, które ją wygenerowało, co jest rodzajem kontekstu, którego nie zapewni goła forma Assert.Equal.
Łączenie wielu warunków
[6:14 - 8:10] Pojedyncza właściwość często musi spełniać więcej niż jedną zasadę. Fluent Assertions komponuje warunki za pomocą .And, dzięki czemu łańcuch pozostaje jednym wyrażeniem:
sample.LastName.Should()
.StartWith("Van")
.And.EndWith("len")
.And.Contain(" ");sample.LastName.Should()
.StartWith("Van")
.And.EndWith("len")
.And.Contain(" ");Każde ogniwo w łańcuchu jest osobnym warunkiem. Domyślnie łańcuch przerywa raportowanie przy pierwszym błędzie: jeśli StartWith("Van") przechodzi, ale EndWith("len") zawodzi, wiadomość o błędzie nazwie EndWith i asercja się tam kończy. Dla testów, gdzie każdy warunek jest niezależny i chcesz, aby każdy błąd się ujawnił, zakres asercji (następna sekcja) zmienia to zachowanie.
Składnia łącząca skłania się do wyrażania intencji nad enumeracją równości. Test, który mówi "nazwisko powinno zaczynać się od Van, kończyć się na len i zawierać przestrzeń", czyta się jak specyfikacja. Ta sama logika napisana z trzema oddzielnymi wywołaniami Assert.True wyglądałaby jak trzy kontrole logiczne bez żadnej narracji łączącej je.
Raportowanie każdej porażki w ramach asercji
[8:10 - 9:34] Domyślne zachowanie zatrzymywania się przy pierwszej porażce jest odpowiednie dla łańcuchów, gdzie późniejsze warunki zależą od wcześniejszych. Dla łańcuchów, gdzie każdy warunek ma znaczenie niezależnie, owinięcie asercji w AssertionScope sprawia, że test raportuje wszystkie błędy podczas jednego uruchomienia:
[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(" ");
}Linia using-var-discard jest standardowym wzorcem: zakres trwa, dopóki zmienna nie wyjdzie z zakresu na końcu metody, a podkreślenie ignoruje tę zmienną, która nie jest czytana. Gdy test się nie powiedzie, komunikat zawiera każdą niewłaściwą zmienną jako osobną linię, co oznacza, że jedno uruchomienie daje pełną historię zamiast zmuszania do trzech uruchomień, aby ujawnić trzy problemy. Dla testów, które weryfikują kształt zwróconego obiektu w wielu właściwościach, to jest różnica między naprawieniem jednego błędu na cykl testowy a naprawieniem wszystkich naraz.
Podsumowanie: testy, które czytają się jak specyfikacje
[9:34 - 10:06] Fluent Assertions nie zmienia tego, co testy weryfikują; zmienia jak odczytywana jest weryfikacja. Łańcuch .Should(), kompozycja .And i grupowanie AssertionScope wszystkie popychają kod testowy bliżej pisemnej specyfikacji testowanego zachowania. Połączenie ze stosowaniem tego samego rodzaju płynnego stylu, który używany jest przez FluentValidation do walidacji danych wejściowych, wzór produkuje zestawy testów i zasady walidacji, które nowi użytkownicy mogą odczytać bez potrzeby wycieczki po systemie.
Wnioski
[9:34 - 10:06] Dodanie Fluent Assertions do projektu testowego wymaga jednej instalacji NuGet, dwóch dyrektyw using oraz przepisania linii asercji z Assert.Equal(expected, actual) na actual.Should().Be(expected). Łańcuchy wyrażają zasady z wieloma warunkami w jednym oświadczeniu, a AssertionScope sprawia, że pojedynczy test raportuje każdy błąd, zamiast zatrzymywać się na pierwszym. Korzyścią są wiadomości o błędach, które wyjaśniają regułę, która została naruszona, co jest różnicą między trasą stosu a zdaniem.
Przykładowa wskazówka: Przy asercji na kolekcjach preferuj result.Should().BeEquivalentTo(expected) nad porównanie właściwość po właściwości. Przechodzi przez graf obiektu za Ciebie i raportuje pierwszą różniącą się właściwość według ścieżki (na przykład users[2].Address.City), co jest znacznie bardziej użyteczne niż wiadomość "kolekcje nie są równe", gdy 50-elementowa lista różni się w jednym zagnieżdżonym polu.
Obejrzyj pełne wideo na jego kanale YouTube Channel i zdobądź więcej informacji na temat pisania czytelnych, łatwych w utrzymaniu testów C# w serii 10-Minute Training.

