Testy jednostkowe: expectedObjects oraz Should Assertion Library

W następnym poście mam zamiar napisać o SpecsFor, kolejnym framework’u ułatwiającym pisanie testów BDD. Najpierw jednak chciałbym przedstawić expectedObjects oraz Should Assertion Library, które są składowymi SpecsFlow. Wszystkie z wymienionych bibliotek można zainstalować przez NuGet.

Should Assertion Library to mała biblioteka ułatwiająca asercje danych. W standardowych unit testach zwykle piszemy coś w stylu:

Assert.IsTrue(value);
Assert.IsNull(value);
Assert.AreEqual(actualValue,expectedValue)

Jest to jak najbardziej w porządku, ale dzięki Should, możemy uzyskać dużo czytelniejszy kod, który zwłaszcza jest przydatny w BDD.

Zamiast Assert.IsTrue możemy teraz:

bool value = true;
value.ShouldBeTrue();

Jeśli warunek byłby nieprawdziwy, wtedy zostanie wyrzucony Should.Core.Exceptions.FalseException. Przykład:

bool value = true;
value.ShouldBeFalse();

Tak jak to w przypadku asercji, można również przekazać treść błędu:

bool value = true;
value.ShouldBeFalse("Wartosc nie jest prawdziwa");

Analogicznie, Should dostarcza wiele innych typów asercji:

obj.ShouldBeNull();

obj = new object();
obj.ShouldBeType(typeof(object));
obj.ShouldEqual(obj);
obj.ShouldNotBeNull();
obj.ShouldNotBeSameAs(new object());
obj.ShouldNotBeType(typeof(string));
obj.ShouldNotEqual("foo");

Ciekawym rozszerzeniem jest weryfikacja zasięgu wartości:

obj = "x";
obj.ShouldNotBeInRange("y", "z");
obj.ShouldBeInRange("a", "z");
obj.ShouldBeSameAs("x");

Should posiada również wsparcie dla kolekcji danych:

var list = new List<object>();
list.ShouldBeEmpty();
list.ShouldNotContain(new object());

Powyższy przykład to tzw. standardowy sposób weryfikacji. Druga alternatywa to Fluent Interface. Takim sposobem można przepisać powyższe zapytania w następujący sposób (źródło: dokumentacja):

obj = new object();
obj.Should().Be.OfType(typeof(object));
obj.Should().Equal(obj);
obj.Should().Not.Be.Null();
obj.Should().Not.Be.SameAs(new object());
obj.Should().Not.Be.OfType<string>();
obj.Should().Not.Equal("foo");

Przejdźmy teraz do expectedObjects. W unit testach porównujemy otrzymane wyjście z tym czego się spodziewamy. W przypadku prostych typów (liczby) nie jest to nic trudnego. Co jeśli mamy klasę typu Person?

class Person
{
   public string FirstName { get; set; }
   public string LastName { get; set; }

   public string ContactNumber { get; set; }
   public string Email { get; set; }
   public string Skype { get; set; }
}

Możliwe jest oczywiście weryfikowanie każdego pola osobno, ale jest to jednak zbyt czasochłonne i monotonne. Dzięki expectedObjects, możemy porównywać również typy złożone:

Person personA = new Person() {FirstName = "Piotr"};
Person personB = new Person() { FirstName = "Pawel" };
personA.ToExpectedObject().ShouldEqual(personB);

W tym przypadku, obiekty nie zgadzają się (imię) i zostanie wyrzucony wyjątek:

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For Person.FirstName, expected "Piotr" but found "Pawel".

Jak widać, dokładnie w wyjątku będzie napisane, która część różni się.

expectedObjects potrafi również analizować zagniezdzone obiekty:

class Person
{
   public string FirstName { get; set; }
   public string LastName { get; set; }

   public Contact Contact { get; set; } 
}
class Contact
{
   public string ContactNumber { get; set; }
   public string Email { get; set; }
   public string Skype { get; set; }
}

Wywołanie:

Person personA = new Person() {Contact = new Contact() {Email = "email1"}};
Person personB = new Person() { Contact = new Contact() { Email = "email2" } };
personA.ToExpectedObject().ShouldEqual(personB);

Wyjątek:

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For Person.Contact.Email, expected "email1" but found "email2".

Analogicznie można sprawdzać całe kolekcje danych:

var listA = new List<Person>();
var listB = new List<Person>();

listA.Add(new Person() {FirstName = "Piotr"});
listA.Add(new Person() { FirstName = "Pawel" });

listB.Add(new Person() { FirstName = "Piotr" });
listB.Add(new Person() { FirstName = "Pawel" });

listA.ToExpectedObject().ShouldEqual(listB);

Wyjątek:

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For List`1[1].FirstName, expected "Pawel" but found "Pawel".

expectedObjects rozpoznają także słowniki:

var dictionaryA = new Dictionary<string,Person>();
var dictionaryB = new Dictionary<string, Person>();

dictionaryA.Add("1",new Person() { FirstName = "Piotr" });
dictionaryA.Add("2", new Person() { FirstName = "Pawel" });

dictionaryB.Add("1", new Person() { FirstName = "Piotr" });
dictionaryB.Add("3", new Person() { FirstName = "Pawel" });

dictionaryA.ToExpectedObject().ShouldEqual(dictionaryB);

W przykładzie klucze się różnią, co poskutkuje:

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For Dictionary`2[1].Key, expected "2" but found "3".

Inny scenariusz to takie same klucze ale różne wartości:

var dictionaryA = new Dictionary<string,Person>();
var dictionaryB = new Dictionary<string, Person>();

dictionaryA.Add("1",new Person() { FirstName = "Piotr" });
dictionaryA.Add("2", new Person() { FirstName = "Pawel" });

dictionaryB.Add("1", new Person() { FirstName = "Piotr" });
dictionaryB.Add("2", new Person() { FirstName = "test" });

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For Dictionary`2[1].Value.FirstName, expected "Pawel" but found "test".

Jak widać możliwości są duże i na dodatek można je rozszerzyć implementując następujący interfejs:

public interface IComparisonStrategy
{
    bool CanCompare(Type type);
    bool AreEqual(object expected, object actual, IComparisonContext comparisonContext);
}

Potem wystarczy wstrzyknąć powyższą strategię w momencie tworzenia obiektu:

_expected = new Foo("Bar")
.ToExpectedObject()
.Configure(ctx => ctx.PushStrategy<FooComparisonStrategy>());

2 thoughts on “Testy jednostkowe: expectedObjects oraz Should Assertion Library”

Leave a Reply

Your email address will not be published.