Wzorzec projektowy ”fluent interface” (polski odpowiednik… płynny interfejs?) przyjął się w środowisku .NET-owym bardzo dobrze. I słusznie! “Płynna syntaktyka” znacznie poprawia czytelność pisanego kodu.
Jednym z sztandarowych przykładów jej użycia są asercje w testach. Pytanie który z poniższych zapisów jest bardziej naturalny?
Zwykły proszek, ekhm, asercja:
Assert.AreEqual("Jan", firstName);
czy odpowiednik “fluent”:
firstName.ShouldBe("Jan");
Dla C# pojawiło się kilka bibliotek wspierających zapis asercji w sposób “płynny”, m.in. Fluent Assertions, Shouldly, Should Assertion Library. Wszystkie biblioteki są do siebie podobne, polecam zaznajomić się z każdą by wyrobić sobie opinię i samemu wybrać najodpowiedniejszą. Najczęściej korzystam z dwóch pierwszych, natomiast w tym wpisie chciałbym przyjrzeć się bibliotece Fluent Assertions.
Nawiązując do pierwszego przykładu, zapis w Fluent Assertions będzie wyglądać następująco:
firstName.Should().Be("Jan");
Co więcej, nasze asercje możemy łączyć za pomocą koniunkcji:
firstName.Should().StartWith("Ag").And.EndWith("a").And.Contain("szk");
Dokumentacja Fluent Assertions przedstawia sporo przykładów zastosowania biblioteki w praktyce. Poniżej wkleiłem proste i co ciekawsze przykłady, aby pokazać w jaki sposób biblioteka radzi sobie z asercjami dla obiektów, kolekcji, liczb, dat, typów Booleanowych:
string firstName = "Jan";
firstName.Should().Be("Jan");
firstName = "Agnieszka";
firstName.Should().StartWith("Ag").And.EndWith("a").And.Contain("szk");
// Obiekty:
string theObject = null;
theObject.Should().BeNull();
theObject = "sth";
theObject.Should().NotBeNull();
theObject.Should().BeOfType();
int? theInt = 5;
theInt.Should().HaveValue(); // Nullable type
// Boolean:
bool theBoolean = true;
theBoolean.Should().BeTrue();
theBoolean = false;
theBoolean.Should().BeFalse();
// Stringi:
string theString = " ";
theString.Should().BeNullOrWhiteSpace();
theString = "This is a string";
theString.Should().Contain("is a");
theString.Should().NotContain("is an");
theString.Should().BeEquivalentTo("THIS IS A STRING"); // case insensitive
theString.Should().StartWith("This");
string emailAddress = "someone@somewhere.com";
emailAddress.Should().Match("*@*.com"); // wildcards
emailAddress.Should().MatchEquivalentOf("*@*.COM"); // case insensitive
string someString = "hello world";
someString.Should().MatchRegex("h.*world");
// Typy numeryczne:
int number = 6;
number.Should().BeGreaterOrEqualTo(5);
number.Should().BeGreaterThan(4);
number.Should().BeLessOrEqualTo(7);
number.Should().BeLessThan(68);
number.Should().BePositive();
number.Should().Be(6);
number.Should().NotBe(10);
number.Should().BeInRange(1, 10);
// Daty:
DateTime theDatetime =
new DateTime(year: 2010, month: 3, day: 1, hour: 22, minute: 15, second: 0);
theDatetime.Should().BeAfter(1.February(2010));
theDatetime.Should().BeBefore(2.March(2010));
theDatetime.Should().BeOnOrAfter(1.March(2010));
theDatetime.Should().Be(1.March(2010).At(22, 15));
theDatetime.Should().NotBe(1.March(2010).At(22, 16));
theDatetime.Should().HaveDay(1);
theDatetime.Should().HaveMonth(3);
theDatetime.Should().HaveYear(2010);
theDatetime.Should().HaveHour(22);
theDatetime.Should().HaveMinute(15);
theDatetime.Should().HaveSecond(0);
// Kolekcje:
var collection = new [] {2, 5, 3};
collection.Should().NotBeEmpty()
.And.HaveCount(3)
.And.ContainInOrder(new[] {2, 5});
collection.Should().HaveCount(3);
collection.Should().ContainSingle(x => x == 3);
collection.Should().NotContain(x => x > 10);
collection.Should().NotContainNulls();
collection.Should().NotBeEmpty();
collection.Should().NotBeNullOrEmpty();
collection.Should().IntersectWith(new[] {5});
Aby wykorzystać FluentAssertions w łapaniu wyjątków należy zadeklarować metodę, która rzuca wyjątkiem do zmiennej typu Action, a następnie wywołać metodę ShouldThrow. Wygląda to następująco:
Action act = () => throw new NotImplementedException();
act.Should().Throw<NotImplementedException>();
Bajecznie proste i czytelne.
Jednym z kluczowych czynników oceny bibliotek do testowania jest sposób komunikacji w przypadku testów czerwonych. Fluent Assertions radzi sobie na tym polu bardzo dobrze.
Przykładowy komunikat dla kolekcji, która ma mieć 4 elementy, a w rzeczywistości (wirtualnej) ma 3:
Expected 4 items because we thought we put three items in the collection, but found 3.
Fluent Assertions działa z bibliotekami:
Jest to kwestia czysto-subiektywna, ale mi osobiście bardziej odpowiada pod względem czytelności zapis asercji w wersji fluent niż standardowej. Przyznam szczerze, że odzwyczaiłem się już od pisania zwykłych asercji (Assert.That
, Assert.AreEqual
, itd.) na rzecz Fluent Assertions. Polecam zaznajomić się z tą biblioteką, a jak już będziecie w jej obsłudze “fluent”, to polecam poeksperymentować z konkurencją – Shouldly, Should Assertion Library, itp.