SzukamNeta.pl: Zapewnianie najwyżej jakości tworzonego kodu
Transkrypt
SzukamNeta.pl: Zapewnianie najwyżej jakości tworzonego kodu
SzukamNeta.pl: Zapewnianie najwyżej jakości tworzonego kodu przy pomocy Testability Explorer – aspekt naukowy projektu biznesowego Wprowadzenie Niniejszy dokument traktuje o narzędziu Testability Explorer dostępnym na stronie http://code.google.com/p/testability-explorer/ , które analizuje kod wytworzony w języku JAVA pod kątem tego jak trudno jest go testować przy pomocy testów unitowych. Dzięki zastosowaniu TE (skrót od testability Explorer, którego będziemy używać w całym dokumencie), programiści potrafią już w trakcie pisania kodu produkcyjnego, gdy w raz z kodem piszą testy (albo nawet przed napisaniem kodu – Test Driven Development), zidentyfikować miejsca, którym muszą poświęcić więcej uwagi i dopracować. Co jest testowane? Uruchomienie Testability Explorera, które zostanie omówione na przykładzie realizowanego projektu w dalszej części dokumentu, powoduje przegląd całego kodu aplikacji pod kątem dwóch podstawowych kryteriów: „Wstrzykiwalności” (Injectability) oraz „Globalności” (Global State). Teraz przyjrzymy się dokładnie obu kryteriom i na przykładach, zaczerpniętych z wiki projektu (http://code.google.com/p/testability-explorer/wiki/HowItWorks), omówimy zasadę działania. Injectability Uciekając od pokrętnego, wymuszonego przetłumaczenia terminologii angielskiej na jezyk polski, w dalszej części dokumentu odnosić się będziemy do tego kryterium uzywając jej anglojęzycznej nazwy. Poprzez injectability rozumiemy możliwość testowania jednostkowego całej klasy w oderwaniu od reszty systemu. Jak łatwo zauważyć, sama nazwa testu: test jednostkowy, już wskazuje na sposób testowania. Chcemy oderwać jeden „kawałek” całego systemu (np. klasę) i „na boku” gruntowanie przetestować ją pod kątem tego czego od niej oczekujemy, a co nam dostarcza. W pojęciu tym można się również dopatrywać instancji zasady znanej z dziedziny projektowania, a dokładniej wzorców projektowych. W jednej z lepszych publikacji na ten temat „Head first: Design Patterns” autorzy pośród jednej z wielu zasad zalecają skoncentrowanie się na tworzeniu interfejsów, a nie implementacji. Ponadto, jedną ze złotych zasad wzorców projektowych jest projektowanie i ogólnie całego programowania zorientowanego obiektowo jest otwartość na rozszerzanie ale zamkniętość na modyfikację. I właśnie tę cechę klas zawartych w tworzonym systemie testuje TE. Z uwagi na możliwość stosowania tych metryk w dowolnym momencie tworzenia projektu, programiści w dowolnej fazie mogą wrócić do stworzonego niedawno kodu, zmodyfikować, a w skrajnych przypadkach przeprojektować daną część systemu tak, aby była ona zgodna z zasadami projektowania obiektowego, tym samym dając pozytywne wyniki testów testowalności. 1 Przykładowy kod - nietestowalny Przyjrzyjmy się poniższemu kodowi klasy, zaczerpniętemu z Wiki projektu TE. public class SumOfPrimes1 { private final Primeness primeness = new Primeness(); public int sum(int max) { int sum = 0; for (int i = 0; i < max; i++) { if (primeness.isPrime(i)) { sum += i; } } return sum; } } Niewprawione oko programisty, który nie patrzy na kod klasy pod kątem jego testowalności, na pierwszy jego rzut stwierdzi, że wszystko jest w najlepszym porządku, kod jest jasny, przejrzysty i komentuje się w zasadzie sam. Zauważmy jednak, że testowanie klasy SumOfPrimes1 będzie wymagało od nas w zasadzie przetestowania jednocześnie klasy Primeness, gdyż jest ona zagregowana i wykorzystywana w metodzie sum. Taka konieczność sama w sobie stanowi zaprzeczenie jednostkowości testów unitowych. Prowadzi nas to do stwierdzenia, że klasa w takiej postaci nie pozwala na „wyciągnięcie” klasy SumOfPrimes1 z systemu i przetestowanie jej niezależnie od pozostałych (nie ma miejsca „wstrzyknięcia”). Taki stan powoduje wygenerowanie odpowiedniego wpisu w raporcie TE, który to wpis odzwierciedlony jest również w ogólnym, procentowym rankingu wystawianym projektowi po teście. Przykładowy kod – testowalny Po przeczytaniu poprzedniego podrozdziału powstało pytanie: Jak, bez przeprojektowywania którejkolwiek z części systemu, sprawić żeby klasa była testowalna? Rozwiązanie jest banalnie proste i jego prostotę prezentuje poniższy kod. public class SumOfPrimes2 { private final Primeness primeness; public SumOfPrimes2(Primeness primeness) { this.primeness = primeness; } public int sum(int max) { int sum = 0; for (int i = 0; i < max; i++) { if (primeness.isPrime(i)) { sum += i; } } return sum; } } Czym powyższy kod różni się od poprzednika? Podstawowym wyróżnikiem jest wprowadzenie konstruktora. Konstruktora parametryzowanego, zawierającego jedną kluczową linijkę 2 (wyboldowaną). Zabieg ten, tj. wprowadzenie paramteryzowanego konstruktora, uniezależnia klasę SumOfPrimes2 całkowicie od klasy Primeness. Dzięki temu, że obiekt SumOfPrimes2 powstaje poprzez przekazanie Primeness w konstruktorze, pozwala na przekazanie do wnętrza tej klasy dowolnego obiektu będącego tego typu, a więc także mock’a, czyli klasy implementującej niejako wszystkie metody klasy bazowej, tutaj klasy Primeness, ale w taki sposób, że nie posiadają one wykonywalnego kodu służącego konkretnemu celowi, a szereg metod pozwalających np. określić ilość wywołać danej metody, przekazane parametry i tym podobne. Przekazanie mock’a do powyższego kodu sprawia, że cała klasa może być testowana niezależnie od reszty systemu, a zatem staje się injectable w świetle kryteriów stosowanych przez TE. Global state Drugim aspektem analizowanym przez TE jest aspekt odwoływania się i korzystania z globalnego stanu systemu w pojedynczych klasach. Zjawisko to jest zjawiskiem niepożądanym z uwagi na dwa fakty: • • Tworzy ukryte powiązania pomiędzy klasami – ukryte, bo nigdy nie wiemy jak duża liczba obiektów i jakie to są obiekty, ma w danej chwili wpływ na globalny stan systemu, Nie pozwala na wyizolowanie klasy do testów – klasa bez systemu nie funkcjonuje, a ponadto kolejność testów, z uwagi na kolejność zmian systemu, może mieć wpływ na powodzenie całej operacji. Zważywszy przedstawione powyżej problemu, również ten aspekt jest badany i oceniany przez TE. Przykład Ilustracją niepożądanych praktyk niech będzie poniższy kod: public static class Gadget { public static final Gadget instance = new Gadget("Global", 1); public final String id; public int count; private Gadget(String id, int count) { this.id = id; this.count = count; } } Podobnie jak w przypadku poprzednio omawianego kryterium, niewprawione oko nie znajdzie niczego złego w tak napisanym kodzie, gdyż jedyna składowa dostępna globalnie, czyli instance jest obarczona modyfikatorem final, więc nawet odwołania z innych obiektów nie będą zależne od stanu systemu w danym momencie. Niestety jednak istnieje sposób na odwołanie do zmiennych nie opatrzonych modyfikatorem final, a zatem takich, których stan może ulegać w dowolnym momencie modyfikacji. Przykładem takiego pola jest pole count, osiągalne za pomocą globalnego odwołania Gadget.instance.count. 3 Jak testowano? W ramach realizowanego projektu zdecydowano się na zautomatyzowanie testu poprzez zainstalowanie odpowiedniego pluginu do Hudsona oraz odpowiednie skonfigurowanie Mavena. Przykładowe wyniki testów Wyniki testów dla poszczególnych buildów dostępne są za pośrednictwem projektowego Hudsona pod adresem http://szukamneta.pl:8080/hudson/. Widok główny Na poniższym rysunku widzimy aktualny stan testowalności tworzonego projektu. Testowalność na tak niskim poziomie jest uzasadniona szczytowym okresem prowadzenia prac (środek 3 SPRINTa). 4 Widok ogólny – wykres dla całego projektu Widok szczegółowy – metoda klasy UserPanel Oprócz widoku ogólnego dla całego projektu, mamy do dyspozycji widoki poszczególnych klas oraz widok pojedynczej metody (pokazany poniżej). Widzimy wyraźnie jaki jest szacowany koszt wynikający z faktu iż konstruktor pokazanej klasy nie może zostać przeciążony. Podobne raporty, równie szczegółowe, generowane są dla każdej klasy znajdującej się w projekcie, dając ostatecznie ogólna procentową ocenę testowalności, która uwzględniana jest na stronie głównej Hudsona. 5 Podsumowanie Przeanalizowawszy narzędzie Testability Explorer pod kątem jego zastosowania w tworzonym projekcie, stwierdzono szerokie zastosowanie w dziedzinie zapewnienia najwyższej jakość tworzonego kodu. Może on, z uwagi na generowanie wymiernych wskaźników, zostać postawiony w jednym rzędzie z narzędziami takimi jak cobertura. Jednakże z uwagi na fakt skupienia się na wyłącznie testowalności całego tworzonego kodu powinien być stosowany jako uzupełnienie pozostałych dostępnych narzędzi metrykowania, a nie jako ich substytut. W realizowanym projekcie stanowi on uzupełnienie dla m.in. wspomnianej cobertury oraz surefire-report. Grzegorz Musiał, 25.11.2009r. 6