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