“Płynne asercje”, czyli jak ułatwić sobie życie korzystając z Fluent Assertions?

Wzorzec projektowy “fluent interface” (polski odpowiednik… płynny interfejs…?) przyjął się w środowisku .NETowym 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");

Wybór pozostawiam Tobie, czytelnikowi, gdyż każdy ma swój gust. Niemniej jednak warto zaznajomić wiedzę na temat płynnych asercji.

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.

Proste asercje

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});

Asercje dla wyjątków

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.

Komunikaty asercji

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.”

Kompatybilność

Fluent Assertions działa z bibliotekami:

  • MSTest (Visual Studio 2010, 2012 Update 2, 2013 and 2015)
  • NUnit
  • XUnit, XUnit2
  • MBUnit
  • Gallio
  • NSpec
  • MSpec

Werdykt

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.

Źródła i linki

5 thoughts on ““Płynne asercje”, czyli jak ułatwić sobie życie korzystając z Fluent Assertions?

  1. Pingback: Kurs TDD cz. 14: Testowanie zależności – atrapy obiektów | DariuszWoźniak .NET
  2. Świetna biblioteka, korzystam w pracy od prawie dwóch lat :)

    P.S. W nowszej wersji FluentAssertions zamieniono act.ShouldThrow() na act.Should().Throw() – warto zupdatować posta :)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.