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.
Tekst ciekawy, jednakże z niecierpliwością czekam(y) na coś niezwiązanego z unit testami czy TDD. (:
Bez przykładów kodu ten wpis nie ma dla mnie wiele wartości.
Moje doświadczenia są inne. Nie wiem dlaczego alternatywą do testów “scenariuszy” są ogromne ilości testów z bardzo wysoką ilością mock’ów i stub’ów.
Odwrotnie, jak testujemy scenariusze zwykle musimy mockowac tylko DAL – o to chodzi. Nie chcemy twozyc granular tests.
Napisałeś “… i testy na poziomie pojedynczych klas są zbyt kosztowne i sprowadzają się do tworzenia bardzo wysokiej liczby mock’ów lub stub’ów.”. Tego nie rozumiem. Istnieją też klasy, które nie mają wstrzykiwanych po 5 interfejsów. Niektóre klasy korzystają z innych klas i te też nie są wstrzykiwane.
Takie rzeczy jak piszesz oczywiście nie są złe. Ja to aktualnie robię przy użyciu Selenium Webdriver i testów pisanych w C# (nie nagrywanych). Ale nie jest prawdą, że albo to, albo miliony stubów i mocków. Zresztą super by było jak by był pokazany przykład przed i po.
@Piotr, masz rację, postaram się wymyślić jakiś przykład i jutro umieścić na blogu 🙂
Jak test może być wykonany w izolacji testując 5 klas na raz ? Jakiego typu jest to izolacja ? Izolacja bazy danych ? Izolacja 4 z 6 warstw ? Izolacja-cokolwiek-sobie-chcesz aka izolacja-co-ci-wygodniej ? 🙂 Czym te testy różnią się od integracyjnych ? Czym są w takim wypadku testy integracyjne ?
To są tylko moje podejrzenia (doświadczenie podpowiada mi jednak: “Maciek – mówię Ci, to tak wygląda!” 😀 ) ale podejrzewam, że zamieniłeś 5 plików z 50 skomplikowanymi testami na 1 plik z 10 bardziej skomplikowanymi testami. To wynika raczej z słabego poziomu testów jednostkowych a nie z złego zrozumienia “jednostki”.
Co z refaktoryzacją klas “narzędziowych” które wykonują całą brudną robotę? Jak do tego podchodzisz, skoro posiadasz tylko testy integracyjne? Nie testujesz tych “drobnych” zmian ? Czy dopisujesz nowy test integracyjny ?
Testy integracyjne mają to do siebie że są…integracyjne 🙂 a co za tym idzie mają złożony setup i zależności – jak utrzymujesz takie testy? co z ich zmianą ? ile trwa zdefiniowanie nowego przypadku? Z ilu linij składa się taki test? co z czytaniem tych testów przez innych członków zespołu? I ostatnie, ale nie mało istotne – ile takie testy się wykonują ?
@Piotr Perak,mgibas:
W skrocie, nie powinno testowac sie klas wewnetrznych (internals). Niestety malo, ktory z projektow uzywa dekleracji internal class i zwykle wszystkie klasy sa publiczne. W takiej sytuacji nalezy zidentyfikowac API czyli klasy dostepowe.
Innymi slowy, jesli mamy klasy A->B->C to zamiast testowac osobno A,B,C lepiej uzyc po prostu A.
Oczywiscie wyjatkiem sa algorytmy.
Testy jednostkowe zawsze powinny byc wykonywane w izolacji wiec nie ma tu mowy o testach integracyjnych. Wszystkie zdalne zasoby mockujemy.
Chodzi aby nie rozdrabniac zbyt bardzo testow poniewaz powoduje to ogromne problemy w przypadku jakichkolwiek zmian.