W ramach testów jednostkowych może być potrzebne porównywanie wartości obiektów zamiast ich referencji. W dzisiejszym artykule poruszę temat jak porównywać obiekty w testach jednostkowych, jeżeli nie została przesłonięta metoda Equals. Proponowane rozwiązania nie będą wiązały się z dodaniem logiki wymaganej przez testy jednostkowe do klas domenowych. Na początek w ramach wprowadzenia do tematu kilka słów o typie object.

System.Object

Typ danych System.Object jest klasą bazową dla wszystkich klas języka C#.  Klasa nie wymaga deklarowania dziedziczenia po klasie Object, ponieważ dziedziczenie jest wykonywane niejawnie. Oznacza to, że dowolna instancja klasy zawsze udostępnia metody zdefiniowane w klasie Object: ToString, Equals, GetHashCode i GetType. Zadaniem metody Equals jest porównanie danego obiektu z dowolnym innym obiektem. Domyślna implementacja metody Equals porównuje referencje. Gdy obiekt zostanie porównany z samym sobą otrzymamy wartość true. Przykładowo typ string przesłania metodę Equals i porównuje obiekty na podstawie wartości. Sprawdźmy to w praktyce, zaimplementujmy klasę Driver.

Na podstawie wcześniejszej omawianej teorii wiemy, że domyślna metoda Equals nie zwróci dla klasy Driver wartości true, mimo że obiekty posiadają pola o tej samej wartości. Do implementacji testów jednostkowych został wykorzystany framework NUnit.

Oczywiście możemy napisać test, w którym zostanie sprawdzona każda wartość pola obiektu poprzez wykonanie asercji dla każdej właściwości. W celu uruchomienia wszystkich asercji w teście, asercje zostały wywołane w Assert.Multiple.

Im więcej obiekt ma właściwości tym więcej instrukcji Assert musimy zdefiniować w teście, co pogarsza czytelność testu.  Powyższy test możemy poprawić wydzielając asercje do niestandardowej metody asercji, którą umieścimy w osobnej klasie. Idealnie było by stworzyć obiekt do porównania z ustawionymi właściwościami, a następnie porównać obiekt wynikowy z obiektem oczekiwanym w jednej asercji. Powyższą sytuacje mielibyśmy w przypadku przesłonięcia domyślnej metody Equals w obiekcie. Metodę Equals nadpisujemy implementując interfejs IEquatable<T>.

Powyższa sytuacja rozwiązałaby problem, ale nie taki jest cel powyższego wpisu.  Zapominamy o przykładowej implementacji interfejsu IEquatable<T>. Załóżmy, że klasa Driver jest z zewnętrznej biblioteki, której nie możemy zmodyfikować. Co możemy w takim przypadku zrobić?

Sposób nr. 1 – Implementacja IEqualityComparer<T>

Interfejs IEqualityComparer<T> definiuje metody służące do porównania obiektów. Stwórzmy klasę DriverComparer  implementującą interfejs IEqualityComparer<Driver>.

Wykorzystamy teraz DriverComparer  w celu porównania dwóch obiektów o tych samych wartościach.

Powyższej test możemy lekko zmodyfikować, stosując inną metodę asercji.

Nic nie stoi nam na przeszkodzie by przetestować także kolekcje obiektów Driver wykorzystując instancje DriverComparer.

Sposób nr. 2 – BeEquivalentTo z Fluent Assertions

Implementując testy jednostkowe w C# ciągle słyszymy o Xunit, MsTest i NUnit, ale czy istnieją inne alternatywy do pisania asercji. W moim przypadku do implementacji testów wykorzystam bibliotekę Fluent Assertions, która umożliwia pisanie testów w sposób „płynny”. Asercje z Fluent Assertions są zapisane w sposób bardziej naturalny. Sprawia to, że testy jednostkowe mogą być bardziej czytelne dla programisty/testera. Do projektów bibliotekę Fluent Assertions możemy pobrać wykorzystując system dystrybucji bibliotek NuGet. Osoby zainteresowane możliwościami Fluent Assertions zapraszam do lektury dokumentacji. W ramach aktualnych testów interesować będą nas dwie metody Be i BeEquivalentTo. Metoda Be zapewnia, że obiekt jest równy innemu obiektowi przy użyciu implementacji metody System.Object.Equals. W przypadku metody BeEquivalentTo obiekt jest równoważny innemu obiektowi, gdy oba obiekty mają jednakowe właściwości o tych samych wartościach nie zależnie od rodzaju tych obiektów. Załóżmy, że mamy obiekt typu Driver i obiekt typu Client. W przypadku, gdy oba obiekty posiadają właściwości o tych samych nazwach i wartościach, wywołanie metody BeEquivalentTo zakończy się sukcesem. Była teoria teraz czas na testy dla powyższych przypadków.

W ramach implementacji testów jednostkowych utworzyłem klasę Client posiadającą takie same właściwości jak Driver, oraz klasę Customer z zmienioną nazwę właściwości z LastName na Nick w porównaniu do klasy Driver. Na potrzeby ostatnich dwóch testów w celu wychwycenia różnic użyłem inicjalizatora obiektu. Ostatni test uzyskał wynik negatywny mimo takich samych wartości ze względu na różnice w nazwie właściwości. Zobaczmy wyniki wszystkich testów w formie graficznej.

Equals TestsOsoby nieczujące potrzeby wykorzystania Fluent Assertions mogą zaimplementować swoją metodę generyczną, która pobierze właściwości z dwóch obiektów o tych samych nazwach i porówna je. Do implementacji będzie wymagane zastosowanie metod z przestrzeni nazw System.Reflection.

Sposób nr. 3 – JSON Serializer

Kolejną alternatywą jest serializacja obiektów do formatu JSON i porównanie ciągów znaków w metodzie Assert.AreEqual.

Sprawdźmy jak zachowa się test, gdy zmienimy wartość właściwości LastName w jednym obiekcie z Nowak na Kowalski.

Equals jsonOczywiście test uzyskał negatywny wynik. W przypadku porównywania JSON-ów szukamy błędu przechodząc pod wskazany indeks w ciągu znaków.

Podsumowanie

W sytuacji, gdy nie chcemy lub nie możemy przesłonić metody Object.Equals w celach testowych, istnieje możliwość zastosowania implementacji IEqualityComparer<T>, wykorzystania metody BeEquivalentTo z Fluent Assertions, lub porównania serializowanych obiektów w formacie JSON. Wybór sposobu w celu poradzenia z powyższym problemem zostawiam wam. Znacie może jakieś inne rozwiązania?