23 kwietnia 2008

JUnit4, OpenEJB3, Maven2, Cobertura i refactoring w Eclipse - wdrożenie wiedzy praktycznie

Dzisiejszy dzień to próba zastosowania narzędzi, które znałem od wieków, ale ze stosowaniem było (delikatnie ujmując) kiepsko - refactoring (ma to swoją nazwę w j. polskim?) oraz tworzenie testów jednostkowych w JUnit 4 z użyciem adnotacji, statycznymi importami oraz uruchomieniem ich poprzez Apache Maven 2. Wszystko za sprawą nachodzącej specyfikacji EJB 3.1, która za jakiś czas stanie się oficjalną, a ja nie zamierzam czekać na implementację od Suna, tylko dostarczyć ją w ramach projektu Apache OpenEJB. Niedawno udało nam się wypuścić wersję OpenEJB 3.0 z pełnym wsparciem dla EJB 3.0 i możliwością uruchamiania ziaren EJB bez specjalnego i skomplikowanego zestawiania środowiska, a jedynie zmianie CLASSPATH i odpowiednim skonstruowaniu kontekstu JNDI w aplikacji. Więcej najprawdopodobniej niebawem, kiedy uporządkuję kilka bieżących zadań wokół Wicket, OSGi, NetBeans i kilku innych projektów, a niecierpliwych kieruję na strony projektu - Apache OpenEJB. W związku z moim powrotem do korzeni i zwiększenia mojego udziału w rozwoju OpenEJB - rozpocząłem wgłębianie się w jego kod źródłowy właśnie za pomocą testów jednostkowych pisanych w JUnit 4. Niby teoretycznie proste, ale w praktyce nierzadko powoduje niemałą zagadkę logiczną i stworzenie środowiska odpowiedniego dla wykonania testu może przyprawić o ból głowy. Wtedy właśnie wychodzi styl programowania i zasada Najpierw test (strona kliencka), a później sama klasa testowana (sama aplikacja). Pod pojęciem strony klienckiej mam na myśli wymagany interfejs programistyczny, którego użycie planuje się w aplikacji.

Rozpocząłem moją przygodę od uruchomienia mvn clean cobertura:cobertura w ramach projektu mavenowego (wspomniałem, że był to moduł OpenEJB - openejb-ejbd, ale tak na prawdę nie ma to większego znaczenia).
 jlaskowski@work /cygdrive/c/oss/openejb3/server/openejb-ejbd
$ mvn clean cobertura:cobertura
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'cobertura'.
[INFO] ------------------------------------------------------------------------
[INFO] Building OpenEJB :: Server :: EJBd
[INFO] task-segment: [clean, cobertura:cobertura]
[INFO] ------------------------------------------------------------------------
...
[INFO] [cobertura:cobertura]
[INFO] Cobertura 1.9 - GNU GPL License (NO WARRANTY) - See COPYRIGHT file
Cobertura: Loaded information on 13 classes.
Report time: 594ms

[INFO] Cobertura Report generation was successful.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
Uruchomienie polecenia wykonuje testy oraz za pomocą wtyczki cobertura-maven-plugin następuje badanie pokrycia kodu źródłowego testami. W wyniku, w katalogu target/site/cobertura, znajduje się dokumentacja HTML z prezentacją pokrycia testami.
 jlaskowski@work /cygdrive/c/oss/openejb3/server/openejb-ejbd
$ ls -l target/site/cobertura/
cobertura/ org.apache.openejb.server.ejbd.AuthRequestHandler.html
css/ org.apache.openejb.server.ejbd.BasicClusterableRequestHandler.html
frame-packages.html org.apache.openejb.server.ejbd.CallContext.html
frame-sourcefiles-org.apache.openejb.server.ejbd.html org.apache.openejb.server.ejbd.ClientObjectFactory.html
frame-sourcefiles.html org.apache.openejb.server.ejbd.ClusterableRequestHandler.html
frame-summary-org.apache.openejb.server.ejbd.html org.apache.openejb.server.ejbd.DeploymentIndex.html
frame-summary.html org.apache.openejb.server.ejbd.EjbDaemon.html
help.html org.apache.openejb.server.ejbd.EjbRequestHandler.html
images/ org.apache.openejb.server.ejbd.EjbServer.html
index.html org.apache.openejb.server.ejbd.JndiRequestHandler.html
js/ org.apache.openejb.server.ejbd.ServerSideResolver.html
Otworzenie strony index.html prezentuje następujący raport:


Widać na nim, że jedynie klasa BasicClusterableRequestHandler jest pokryta w 100% przez testy, a pozostałe prezentują się marnie. Pokrycie w 100% nie powinno być celem samym w sobie, ale dla mnie najważniejsze było, aby zacząć, więc akurat wybrałem moduł openejb-ejbd, w którym wybrałem klasę DeploymentIndex (obecnie ma 59% pokrycia, ale jeszcze wczoraj było 0!). Sama prezentacja pokrycia w postaci zielonych i czerwonych miejsc wskazuje, które są objęte testami, a które wymagają troski.


Po rozmowie z innym programistą projektu OpenEJB i jego liderem - Davidem Blevinsem - okazało się, że:
 <jlaskowski> write...I'm all ears (trying to polish a junit test for DeploymentIndex)
<dblevins> we don't use the DeploymentIndex anymore
<jlaskowski> what?!
<dblevins> kind of an unfortunate place to start, but yea, it's cruft at the moment
...czyli klasa nie jest w ogóle w użyciu. No cóż pierwsze podejście nie musi być idealne ;-) Mimo wszystko dobrze było rozpocząć rozpoznawanie kodu źródłowego OpenEJB wybierając dowolną klasę, której pokrycie jest bliskie zeru, a że nie jest w użyciu jeszcze - nic straconego. Od razu Dave przyszedł mi jednak z pomocą i napisał:
 <dblevins> I suggest the Assembler
<jlaskowski> org.apache.openejb.assembler.classic.Assembler?
<dblevins> right
<jlaskowski> k
<dblevins> and the ConfigurationFactory
<dblevins> the flow of the system startup is this...
i Dave rozwinął jak to tam w środku się dzieje. Mam się czym zająć w wolnych chwilach i mam pewność, że dana klasa jest w użyciu. Jestem ocalony!

Co to jednak ma wspólnego z refactoringiem?! Podczas pracy z klasą testową - DeploymentIndexTest - rozpocząłem niewinnie z pojedyńczą metodą, która później rozwinęła się w dwie, a później dodałem metodę setUp(), która ustawia środowisko (=zmienne) i jest wykonywana każdorazowo przed każdym testem - metoda oznaczona adnotacją @Before. Okazało się, że wbrew temu jak do tej pory podchodziłem do sprawy modyfikacji klas metodą Ctrl-C/Ctrl-V, tym razem postanowiłem spróbować przyzwyczaić się do kolejnego elementu środowisk IDE (korzystałem z Eclipse IDE 3.4M6) i podczas zmiany rangi zmiennej lokalnej do poziomu zmiennej instancji skorzystałem z menu Refactor > Convert Local Variable to Field...


Po chwili miałem temat z głowy. Wystarczy umiejscowić kursor na zmiennej lokalnej i wybrać menu, aby przenieść ją na poziom zmiennej instancji. Przydała się również funkcja Refactor > Rename (Alt+Shift+R), która jednak była już kilkakrotnie przeze mnie wykorzystywana. Piszę o tym, gdyż mając w nawyku pracę z różnej maści edytorami byłem przyzwyczajony do ciągłego stosowania techniki Copy-Paste, która była uniwersalna, i mimo wiedzy na temat funkcjonalności Refactoring w Eclipse (również NetBeans czy IntelliJ IDEA) nie miałem nawyku jej stosowania tracąc czas na rzeczy, które nie były ani ciekawe, ani miłe. Zdecydowanie polecam tego typu podejście do zmiany struktury kody, niż owe Copy-Paste.

W trakcie poznawania zmian w JUnit 4 natrafiłem na sposób deklarowania oczekiwanego wyjątku w teście. Do tej pory stosowałem technikę:
 try {
...kod, który zgłaszał wyjątek...
fail("Powinien zostać zgłoszony wyjątek NazwaWyjątkuException");
} catch (NazwaWyjątkuException expected) {
// ignored
}
W JUnit 4 zalecanym sposobem jest użycie atrybutu expected adnotacji @Test, np. @Test(expected= IndexOutOfBoundsException.class), czyli to, co zostało zaprezentowane wyżej teraz powinno wyglądać następująco:
 @Test(expected=NazwaWyjątkuException.class)
public void mojTest() {
...kod, który zgłaszał wyjątek...
}
Więcej informacji o JUnit w JUnit Cookbook. Warto się z nim zapoznać, bo zawiera kwintesencję JUnit i jego przeczytanie nie powinno zabrać więcej niż 5-10 minut (pewnie przeczytanie tego wpisu zajęło więcej ;-)). Wracam do testów jednostkowych...

Pytanie konkursowe: W jaki sposób deklarujemy oczekiwany wyjątek w JUnit 4?