NSubstitute–dobra alternatywa dla Moq

Zwykle używałem Moq w swoich projektach, ale ostatnio miałem do czynienia z NSubstitute i jego składnia dużo bardziej podoba mi się.

Standardowo instalujemy pakiet NuGet:

image

Załóżmy, że mamy interfejs:

interface IPersonRepository { Person GetById(int personId); void Add(Person person); }

Konfiguracja metod jest bardzo prosta i naturalna:

IPersonRepository personRepositoryMock = Substitute.For<IPersonRepository>(); personRepositoryMock.GetById(1).Returns(new Person() { FirstName = "test" }); Person person = personRepositoryMock.GetById(1);

W przypadku Moq, mamy do czynienia z wyrażeniami lambda, które nie są aż tak bardzo przejrzyste jak powyższy kod.

Podobnie sprawa wygląda z weryfikacją, czy jakaś metoda została wykonana:

personRepositoryMock.Received(5).GetById(Arg.Any<int>());

Analogiczną metodą jest DidNotReceived:

personRepositoryMock.DidNotReceive().GetById(Arg.Any<int>());

Możliwe jest również odpalenie zdarzenia za pomocą Raise.EventWith:

IPersonRepository personRepositoryMock = Substitute.For<IPersonRepository>(); bool wasCalled = false; personRepositoryMock.SampleEvent += (sender, args) => wasCalled = true; personRepositoryMock.SampleEvent += Raise.EventWith(new object(), new EventArgs());

Ze względu, że nie mamy do czynienia z lambda, wsparcie ze strony resharper jest lepsze i moim zdaniem łatwiej robić TDD.

Antywzorzec: Lava Flow

Lava Flow jest antywzorcem, który niestety często można spotkać w starych projektach, nierzadko przekazywanych od jednego zespołu do drugiego.

Zacznijmy od cytatu z Wikipedii:

“In computer programming jargon, lava flow is a problem in which computer code written under sub-optimal conditions is put into production and added to while still in a developmental state.”

Definicja prawdopodobnie nie jest do końca jasna, ale mówi o sytuacji, w której zmiany w kodzie dokonywane są bez pełnego zrozumienia projektu oraz w pośpiechu.

Antywzorzec Lava Layer powstaje, gdy próbujemy korzystać z wielu technologii lub rozwiązań mających podobne zastosowanie. Jako przykład rozważmy DAL…

W starych projektach, często można spotkać czyste zapytania SQL\ADO.NET.  Jeśli projekt jest ogromny, trudno wszystko natychmiast naprawić. Często jednak programiści podejmują się takiego zadania i np. w przypadku zapytań SQL, mógłby ktoś zastosować wzorzec Gateway. Załóżmy, że po pewnym czasie ktoś inny, w tym samym projekcie, wprowadzana kolejną modyfikacje – jakiś framework ORM np. EntityFramework.

W przypadku braku komunikacji między poszczególnymi programistami, może okazać się, że mamy kilka technologii (SQL queries, EntityFramework, nHibernate, Dapper) w tym samym projekcie, mające te same zadanie. Kod oczywiście jest bardzo trudny wtedy w utrzymaniu i np. dodanie nowej encji do projektu wygląda inaczej w zależności od jej typu.

Innymi słowy, Lava to brak spójnej wizji, którą podążają wszyscy programiści. Każda osoba, próbując zmienić DAL, ma dobre intencje – chce skorzystać z nowej technologii. Dzisiaj EntityFrameowrk czy Dapper są dość stabilnymi bibliotekami, ale za parę lat może okazać się, że programiści będą chcieli pozbyć się tego z kodu jak szybko tylko to możliwe. 

Problem wynika często z braku odpowiedzialności za kod. Jeśli projekt jest przesuwany pomiędzy różne osoby czy zespoły, wizja autora kodu jest po drodze po prostu gubiona.

Osoby przychodzące potem do zespołu, zwykle krytykują kod, który został przed nimi napisany. Bardzo często w legacy code jest wiele okropnych rzeczy, ale mimo to, kluczowe jest zrozumienie dlaczego pewne decyzje zostały podjęte w dany sposób. Narzekanie na istniejący kod i budowanie wszystkiego od nowa zwykle jest bardzo złym rozwiązaniem. Przed jakimikolwiek ulepszeniami, należy zrozumieć intencje poprzednich autorów oraz dokładne rozwiązania z których korzystali.

Następnie, bardzo ważne jest, aby być spójnym w refaktoryzacji. Jeśli zdecydujemy się na zastąpienie technologii A, technologią B, nie możemy w międzyczasie wprowadzić nowego rozwiązania C.  Każda refaktoryzacja to proces iteracyjny i naturalne jest, że rozwiązania A i B będą przez pewien czas egzystować równolegle. Powinniśmy jednak dążyć do sytuacji kiedy stosowanie rozwiązania A będzie zmniejszać się, na rzecz B. Jeśli w trakcie tego procesu, okaże się, że jednak technologia C jest lepsza to powinniśmy najpierw dokończyć albo anulować poprzednie kroki (tzn. wprowadzenie B).

Bez tej dyscypliny, może okazać się, że skończymy z rozwiązaniami A,B,C,D,E i nikt nie będzie w stanie w zespole powiedzieć dlaczego tak stało się.

Problem pojawia się, gdy programiści odchodzą z firmy i nie zostawiają jasnej wizji w jakim kierunku projekt powinien pójść. Wtedy zwykle nowi programiści przychodzący do firmy, oczywiście nie znają tych intencji i próbują wprowadzać swoje ulepszenia, mając  dobre intencje.

Mowa tutaj o pojedynczym projekcie. Poszczególne zespoły powinny mieć wybór między różnymi technologiami i wtedy nie ma w tym nic złego, że w jednym serwisie jest EntityFramework w a drugim Dapper.

Inna moja obserwacja jest taka, że im bardziej monolitowa architektura tym łatwiej o Lava w projekcie. W kolejnych postach będę pisał o mikroserwisach, które są może czymś oczywistym, ale rozwiązują naprawdę wiele problemów – nie tylko natury technicznej.

Visual Studio: Productivity Power Tools 2013

Dzisiaj chciałbym przedstawić dodatek do VS, a mianowicie Productivity Power Tools 2013. Kilka usprawnień jest naprawdę ciekawych i dlatego zachęcam do zainstalowania dodatku.

Moim najbardziej ulubionym rozszerzeniem jest sposób przedstawiania błędów w Solution Explorer. Bez dodatku, standardowo błędy związane z kompilacja są pokazywane w oknie Error List:

image

Productivity Tools pokazuje te same informacje również w Solution Explorer:

image

Każdy plik, projekt czy solucja, gdy zawiera jakieś błędy jest podkreślona na czerwono. Podobnie sprawa wygląda z “warnings”. Po najechaniu kursora dostaniemy dodatkowe informacje:

image

Inna drobna zmiana, ale bardzo przydatna to stworzenie skrótu Ctrl+Click, który umożliwia podgląd metody (Peek):

image

O wiele łatwiejsze jest naciśnięcie Ctrl + klik myszką niż korzystanie ze skrótów klawiaturowych czy też ręczne otwieranie menu podręcznego. Warto zwrócić uwagę, że Ctrl+Click jest używany przez Resharper jako “Go To Definition”. Jeśli mamy zainstalowany Resharper oraz chcemy uniknąć konfliktu, wtedy przechodzimy do Tools->Options i wyłączamy Ctrl+Click:

image

Inna nowość to Structure Visualizer:

image

Po lewej stronie możemy zauważyć, pionowe linie określające poziom zagnieżdżania kodu.

Najeżdżając na jedną z linii, dostaniemy informacje o strukturze kodu w formie tooltip’a, wyświetlanego u góry:

image

Wprowadzono również usprawnienie w formie Peek Help. Naciskając Alt+F1 na danej funkcji, wyświetlona zostanie dokumentacja MSDN:

image

Jest to dużo wygodniejsze niż otwieranie dokumentacji w osobnym oknie przeglądarki.

Kolejny drobiazg, to Power Commands, opcja dostępna z menu kontekstowego Solution Explorer:

image

“Remove And Sort Usings” jest prawdopodobnie najbardziej przydatną opcją.

Kolejna zmiana to maksymalizacja okna. Klikając dwukrotnie na jakimkolwiek okienku, automatycznie zostanie one zmaksymalizowane, co nie było możliwe bez Power Tools. Przydatne szczególnie dla okienek Debug, Watch itp. Czasami mamy sporo zmiennych tam i chcemy tylko na chwilę zobaczyć szczegóły. Za pomocą dwukrotnego kliknięcia, możemy zrobić to bardzo szybko.

Okienko Quick Launch może posłużyć teraz do wykonywania różnych zadań. W celu wyświetlenia wszystkich możliwych zadań do wykonania, wystarczy wpisać “@tasks e”:

image

Na przykład, w celu włączenia numeracji linii wystarczyć uruchomić komendę LineNumOn.

W Menu File, mamy teraz do dyspozycji “Recently Closed Documents”, które oczywiście zawiera ostatnio zamknięte dokumenty:

image

Usprawnienia również wprowadzono w wyświetlaniu i koloryzacji tabów:

image

Kolor tabu, zależy od lokalizacji pliku.

Dapper – proste i wydajne narzędzie ORM

Nie opisywałbym kolejnego ORM na blogu, ale myślę, że warto zapoznać się z Dapper, ponieważ jest to dość specyficzna biblioteka. Celem Dapper jest prostota i wydajność. Nie może on zastąpić EntityFramework czy nHibernate, ale w wielu przypadkach, nie musimy korzystać z tak ciężkich i zaawansowanych rozwiązań.

Cały kod źródłowy jest dostępny w jednym pliku:

https://github.com/StackExchange/dapper-dot-net/blob/master/Dapper%20NET40/SqlMapper.cs

Osobiście preferuję jednak NuGet:

image

Załóżmy, że obiekt, który chcemy zmapować wygląda następująco:

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

Dapper to nic innego jak metody rozszerzające dla IDbConnection. Jedną z tych metod jest Query:

SqlConnection sqlConnection = new SqlConnection(@"Server=PIOTR-PC\SQLEXPRESS;Database=Test;Trusted_Connection=True;"); Person[] persons = sqlConnection.Query<Person>("select * from Persons").ToArray(); foreach (Person person in persons) { Console.WriteLine("{0} {1}",person.FirstName,person.LastName); }

Powyższy kod to wszystko, co potrzebujemy, aby zmapować tabelę na obiekt POCO. W Query jako typ generyczny przekazujemy Person, a jako parametry zapytanie SQL. Co więcej (odradzam) możemy skorzystać z dynamic:

SqlConnection sqlConnection = new SqlConnection(@"Server=PIOTR-PC\SQLEXPRESS;Database=Test;Trusted_Connection=True;"); dynamic[] persons = sqlConnection.Query<dynamic>("select * from Persons").ToArray(); foreach (dynamic person in persons) { Console.WriteLine("{0} {1}",person.FirstName,person.LastName); }

W każdym razie w większości przypadków nie będziemy korzystać z dynamic.  Dapper implementuje jeszcze metodę Execute:

public static int Execute(this IDbConnection cnn, string sql, object param) { return Execute(cnn, sql, param, null, null, null); }

Korzystamy z niej, gdy chcemy wykonać SQL, a nie interesuje nas wynik (np. Insert albo Update).

Jak widać, Dapper jest prosty i co za tym idzie, wydajność jest wysoka. Wyniki testów wydajnościowych można przeczytać w oficjalnej dokumentacji. W każdym razie jest niewiele wolniejszy od czystego ADO.NET (SqlConnection). Ze względu na fakt, że Dapper to nic innego jak metody rozszerzające, wszystkie typy baz danych są wspierane.

W Dapper nie ma konfiguracji mapowania. Jeśli kolumny w bazie nie pokrywają się z nazwami właściwości, wtedy możemy:

Person[] persons = sqlConnection.Query<Person>("select FirstName,LastName as Surname from Persons").ToArray();

Moim zdaniem, rozwiązanie doskonałe, gdy zależy nam na wydajności, a nie chcemy korzystać z czystego SqlConnection.

Więcej przykładów można znaleźć tutaj:

https://github.com/StackExchange/dapper-dot-net/blob/master/Tests/Tests.cs

TDD: Jak szczegółowo testować?

Definicja testów jednostkowych nie jest jednoznaczna i moim zdaniem zmieniała się przez lata. Jednostkę (“unit”) można w różny sposób interpretować.

Wiele programistów uważa, że należy testować wyłącznie poszczególne klasy. Dobrą stroną takiego podejścia jest fakt, że jak test zakończy się niepowodzeniem, wtedy od razu wiadomo gdzie szukać przyczyny. Przy dobrym zestawie testów, debugger przestaje być potrzebny. 

Osobiście preferuje zupełnie inne podejście. W aplikacjach biznesowych, moim zdaniem aż tak wysoki poziom atomowości nie ma sensu. Jakakolwiek zmiana wymaga wtedy refaktoryzacji istniejących testów, co zniechęca programistów do jakichkolwiek ulepszeń. Utrzymanie kodu po pewnym czasie staje się bardzo trudne i przede wszystkim czasochłonne. 

Aplikacje biznesowe w większości przypadkach nie zawierają skomplikowanej logiki dlatego lepiej testować klasy, które faktycznie są publiczne. Niestety, większość projektów nad jakimi pracowałem, nie przestrzegało poprawnie zasady enkapsulacji klas. Wiele klas tak naprawdę mogłoby być sealed internal. Nie mniej jednak, znając dany projekt, możemy określić punkty wejściowe czy też API do naszej logiki biznesowej. Z tego względu, zamiast testować szczegółowe obiekty biznesowe, lepiej wejść parę poziomów wyżek i skupić się na API, publicznych klasach czy też punktach wejściowych . Z takim podejściem, możemy bezpiecznie refaktoryzować kod i testy powinny pozostać niezmienione!

Dzięki temu,  w większości przypadkach programiści nie muszą martwić się, że zmieniając jakiś kod, zepsują testy. Jeśli testy byłby zbyt szczegółowe, wtedy musielibyśmy modyfikować za każdą refaktoryzacją również testy jednostkowe, co powoduje ogromne niebezpieczeństwo, że coś zostanie po prostu popsute.

Jednostkę definiuję bardziej jako scenariusz biznesowy, a nie szczegół implementacyjny. Oczywiście testy jednostkowe muszą być wykonywane w izolacji i nie mają nic wspólnego z testami integracyjnymi. Po prostu zamiast 5 klas testować osobno, piszę jeden TestFixture, gdzie testy weryfikują wyłącznie pojedyncze scenariusze. W aplikacjach biznesowych sprawdza się to, ponieważ w pojedynczych klasach nie ma aż tak skomplikowanej logiki. Jeśli mam jakieś algorytmy czy bardziej skomplikowaną logikę, wtedy oczywiście piszę testy szczegółowe ponieważ przez API ciężko byłoby pokryć wszystkie przypadki. W większości przypadkach logika nie jest aż tak skomplikowana i testy na poziomie pojedynczych klas są zbyt kosztowne i sprowadzają się do tworzenia bardzo wysokiej liczby mock’ów lub stub’ów.

Innymi słowy, testy nie powinny być powiązane ze szczegółami implementacyjnymi, które mają prawo zmienić się w każdej chwili (refaktoryzacja), a które nie zmieniają zachowania. Jeśli zachowanie pozostaje takie same, wtedy przecież również testy powinny pozostać niezmienione.

Code review: Czytelność testów

Dziś chciałbym pokazać kilka scenariuszy, które według mnie są przykładem złych testów.

O tym jak szczegółowo należy pisać testy, opiszę w następnym poście więc tym dzisiaj nie będę zajmować się.

Jednym z często spotykanych błędów jest testowanie zbyt wielu scenariuszy na raz. Test jednostkowy nie powinien wyglądać na przykład w taki sposób:

// przygotowanie danych // wykonanie operacji if(id==1) { Assert.AreEqual(1,result.Count); } else { Assert.AreEqual(2,result.Count); }

Test nie powinien zawierać żadnej logiki bo co jeśli pomylimy się? Jeśli wykonujemy różne asercje, w zależności od jakiś danych, wtedy oznacza to, że powinniśmy stworzyć drugi, osobny test. Asercje powinny być proste i jasne. Jeśli inny programista zagląda do testów i musi analizować je linia po linii, oznacza to, że  asercje są zbyt skomplikowane. Innymi słowy, żadnych instrukcji warunkowych podczas asercji.

Test powinien być napisany jak skrypt, czyli stanowić dokumentację.  Skrajnie zły przykład to:

[Test] public void Test() { base.VerifySomething(5,"Hello world"); }

W logice biznesowej często zdarza się, że metoda publiczna wywołuje bazową metodę z jakimiś parametrami, ewentualnie poprzedzając to inną prostą logiką. W testach chcemy mieć jasny skrypt zdarzeń zachodzących po kolei. Musi być wyraźnie widoczne jak dane są przygotowane, wywołane,  a na końcu zweryfikowane. Wywołując jakieś metody bazowe, tak naprawdę dla czytelnika takiego kodu, wygląda to jak czarna skrzynka, a nie dokumentacja, którą testy przecież powinny stanowić.

Nie oznacza to, że musimy duplikować kod we wszystkich testach, które wykorzystują podobne kroki. To naturalne jest, że duża część kodu może być taka sama dla różnych testów. W takich przypadkach, zamiast implementacji jednej, dużej metody, która wykonuje wszystkie kroki, lepie opakować w metody wyłącznie pojedyncze kroki. Podobnie z asercjami, można w końcu skomplikowane asercje  zaimplementować za pomocą metod rozszerzających. Nie powinniśmy jednak kopiować wszystkich asercji do osobnej metody, nawet jak są wykorzystywane w różnych testach. Tak jak podkreślałem wielokrotnie już, test powinien stanowić skrypt opisujący testowany scenariusz.

Nie ma łatwej reguły co powinno trafiać do osobnych metod, a co powinno być bezpośrednio w metodzie testującej. Celem jest stworzenie przejrzystego testu, który można przeczytać i zrozumieć bez skakania do różnych metod w klasie\projekcie. Bardzo kuszące jest zlikwidowanie zduplikowanego kodu (wykorzystywanego w różnych testach) poprzez ekstrakcje go do innej metody, ale trzeba to robić z rozsądkiem. Nie chcemy mieć w końcu w każdym teście czarnych skrzynek w postaci metod typu “PrepareData” czy “InitializeScenarioSteps”. Czasami nawet lepiej pozostawić pewien kod zduplikowany (oczywiście w granicach rozsądku) niż na siłę wszystko automatyzować i wydzielać.

nCrunch–wykonywanie testów w tle

Dzisiaj chciałbym pokazać bardzo fajne narzędzie ułatwiające pisanie testów, szczególnie podczas refaktoryzacji i pracy nad starym kodem.

Dzięki nCrunch jesteśmy w stanie zobaczyć bardzo szybko, które linie kodu są pokryte przez testy. nCrunch wykonuje testy w tle i muszę przyznać, że robi to bardzo szybko. Wygląda na to, że w pełni wykorzystuje możliwości programowania współbieżnego.

Załóżmy, że mamy jakiś kod, który nie ma jeszcze testów (legacy code). W takiej sytuacji, nCrunch zaznaczy wszystkie linie czarnymi kropkami:

image

Następnie, napiszemy podstawowy test sprawdzający dzielenie:

image

Zielone kropki w przypadku testów oznaczają, że został on wykonany pomyślnie. Nie musimy ręcznie go uruchamiać, ponieważ nCrunch zrobi to za nas w tle. Działa to na tyle wydajnie, że szybciej jest poczekać aż nCrunch wykona go automatycznie w tle niż uruchamiać test z poziomu Visual Studio. Gdyby test nie udał się, wtedy odpowiednie linie będą zaznaczone na czerwono tzn.:

image

Przejdźmy teraz do implementacji MathHelper:

image

Ze screenu widać, że jedna linia nie jest pokryta ponieważ nie dostarczyliśmy na wejście wartości zero. Najeżdżając kursorem na kropki, dostaniemy dodatkowe informacje o pokryciu:

image

Oprócz graficznego wsparcia, mamy również standardowe tabelki pokrycia, ale to już było dawno osiągalne choćby z poziomu samego Visual Studio.

nCrunch został stworzony jako narzędzie do TDD, ale szczególnie jego zalety zauważam w przypadku pracy ze starym kodem, który nie był napisany w sposób TDD, stąd jego pokrycie może być dość słabe.