Monday, July 7, 2008

Mockito

Dzisiaj chciałbym przedstawić Wam ciekawą bibliotekę do mocków. Na początek jednak pokrótce opiszę, co to takiego te mocki.

Mock - z angielskiego "udawać", "imitować". W programowaniu "mock object" oznacza obiekt, który udaje inny obiekt. Najlepiej chyba będzie wyjaśnić to na przykładzie.

Załóżmy, że mamy klasę Caller, realizującą połączenia telefoniczne, i klasę Phone, reprezentującą telefon (numer) na który można dzwonić. Przy wywołaniu metody call, nasza klasa powinna wywołać na obiekcie Phone metodę connect, po czym w zależności od tego co ona zwróci talk lub disconnect. Zatem po kolei wywołania wyglądają tak:
1. caller.call(Phone phone) Wewnątrz obiektu caller:
2. phone.connect
3. phone.disconnect lub phone.talk

Załóżmy teraz, że piszemy test dla klasy Caller. Nie możemy lub nie chcemy (a często nawet nie powinniśmy) używać w nim obiektu Phone. Może być problem z dostępem do niego (np. testujemy nie mając dostępu do środowiska z podłączonymi telefonami), możemy nie chcieć z niego korzystać (nie chcemy na prawdę gdzieś dzwonić). W takiej sytuacji z pomocą przychodzą nam właśnie mock objects. "Zmockujemy" sobie obiekt klasy Phone i jego będziemy używać.

Wszystkie biblioteki do mocków dadzą sobie radę jeśli Phone będzie interfejsem. Większość da sobie radę także jeśli będzie zwykłą klasą. Do tej większości należy Mockito, które chciałbym dzisiaj opisać. Aby uzyskać sztuczny obiekt Phone, robimy tak:


Na początku potrzebny jest import Mockito. Aby ułatwić sobie życie, zaimportujemy statycznie wszystkie metody tej klasy:
import static org.mockito.Mockito.*;

Teraz reszta naszego testu:
Phone phone = mock ( Phone. class ) ;
Caller instance = new Caller();
instance.call(phone);
verify(phone).connect();


W ten sposób zweryfikowaliśmy, czy Caller wywołał metodę connect na Phone. Jednak to nie wszystko co chcielibyśmy sprawdzić. Metoda connect zwraca pewien status, i w zależności od niego Caller powinien wywołać jeszcze talk lub disconnect. Jak zaprogramować mocka, żeby zwrócił odpowiednią wartość po wywołaniu metody? Nie jest to nic trudnego:
stub (phone.connect()).toReturn(Status.BUSY);

Zatem cały test wyglądałby tak:
Phone phone = mock(Phone.class);
Caller instance = new Caller();
stub(phone.connect()).toReturn(Status.BUSY);
instance.call(phone); verify(phone).connect();
verify(phone).disconnect();


To jednak nie wszystkie możliwości Mockito. W powyższym przykładzie deklarujemy co ma zwrócić Phone jeśli zostanie wywołana na nim metoda connect. Metoda ta nie przyjmuje żadnych parametrów. Co gdyby przyjmowała, i chcielibyśmy żeby mock zwracał różne wartości w zależności od tego z jakim parametrem została wywołana? Załóżmy że Phone ma też metodę say, przyjmującą parametr typu String i zwracającą wartość tego samego typu. Caller wywołuje tę metodę przekazując "Good afternoon" na początku rozmowy, i "Good bye" na końcu. Chcielibyśmy zaprogramować mocka tak, żeby "odpowiadał" tym samym powitaniem i pożegnaniem:
stub ( phone.say ( "Good afternoon" )) .toReturn ( "Good afternoon" );
stub(phone.say("Good bye")).toReturn("Good bye");


Możemy też używać tzw. argument matcherów:
stub(phone.say(anyString())).toReturn("You said something but I could not understand");

W ten sposób zwrócimy określoną wartość niezależnie od tego co dostaniemy w parametrze. Oczywiście można też rzucać wyjątki:
stub(phone.say(argThat(new DirtyWordMatcher())).toThrow(new IllegalArgumentException());

DirtyWordMatcher to nasz własny matcher, który zwraca true jeśli napotka niecenzuralne słowa :)

Inną możliwością jest sprawdzanie czy dana metoda została wywołana daną ilość razy:

verify(phone, times(3)).say();

W ten sposób weryfikujemy czy metoda say() została wywołana 3 razy. Możemy też sprawdzić czy nie została nigdy wywołana, lub sprawdzić czy metody zostały wywołane w odpowiedniej kolejności. Aby dowiedzieć się jak to się robi, odsyłam już do dokumentacji mockito.

Friday, July 4, 2008

Upload do repozytorium mavena

Załóżmy, że mamy własne repozytorium mavena (np. nexusa, opisanego przez Leszka tutaj). Czasem jest potrzeba dorzucenia do takiego repozytorium "cudzej" biblioteki. Np. wtedy, gdy potrzebna nam jej nowa, świeża wersja, której jeszcze nie ma w repozytoriach ogólnodostępnych, ale można ją ściągnąć z internetu.

Przypuśćmy, że dołączyć chcemy bibliotekę httpunit w wersji 1.7. Mamy ją w pliku httpunit.jar w katalogu /home/franek/tmp/httpunit.jar. Nasze repozytorium jest pod adresem http://myserver/maven-repo


mvn deploy:deploy-file -DgroupId=httpunit -DartifactId=httpunit -Dversion=1.7 -Dpackaging=jar -Dfile=/home/franek/tmp/httpunit.jar -Durl=http://myserver/maven-repo -DrepositoryId=main

Maven sam wygeneruje plik POM. Jednak zdecydowanie lepiej jest mu go dostarczyć jeśli tylko takim dysponujemy. Inaczej narażamy się na to, że w wygenerowanym POMie może brakować bibliotek od których httpunit zależy. Załóżmy, że POM leży w katalogu /home/franek/tmp/pom.xml. Aby go dodać, do komendy trzeba dodać parametr -DpomFile=/home/franek/tmp/pom.xml. Jeśli dołączamy POMa, nie trzeba podawać parametrów groupId, artifactId, version i packaging - maven odczyta je z POMa.

Jeśli repozytorium wymaga nazwy użytkownika i hasła, trzeba je jeszcze ustawić w ~/.m2/settings.xml
<settings>
  ...
  <servers>
    <server>
      <id>main</id>
      <username>mvn</username>
      <password>mvn</password>
    </server>
  </servers>
  ...
</settings>

Do biblioteki dorzuconej do repozytorium warto także dołączyć źródła:

mvn deploy:deploy-file -DgroupId=httpunit -DartifactId=httpunit -Dversion=1.7 -Dpackaging=java-source -Dfile=/home/franek/tmp/httpunit-src.jar -Durl=http://myserver/maven-repo -DrepositoryId=main -DgeneratePom=false
Na czerwono oznaczyłem fragmenty, które różnią się w stosunku do komendy służącej do wrzucania jara.

Thursday, July 3, 2008

ClockingIT - kolejny ciekawy projekt

Niedawno w naszej firmie zakupiliśmy JIRA. JIRA, jakby ktoś nie wiedział, to zaawansowany system do zarządzania zadaniami. Ma naprawdę spore możliwości, można tworzyć zadania (np. bugi do naprawienia, ale nie tylko), przypisywać je członkom zespołu, prognozować czas wykonania, tworzyć podzadania, skojarzyć bug z wersją w której wystąpił, z wersją w której ma być naprawiony itp. Wygodny w używaniu i mający duże możliwości produkt.

Jednej rzeczy nam jednak w JIRA brakuje - możliwości time trackingu (zliczania ile czasu poświęciło się na dane zadanie). Można co prawda zaznaczać ile się nad zadaniem spędziło, ale nie ma czegoś takiego, że można kliknąć START i czas zaczyna się odliczać, a potem STOP i JIRA automatycznie dodaje tyle godzin/minut ile przepracowaliśmy. Po prostu codziennie trzeba to "ręcznie" lub przy użyciu innych programów liczyć i pod koniec dnia wpisywać do systemu.

Dzisiaj natknąłem się na projekt ClockingIT. Ma właśnie to czego nam brakuje, a jest za darmo! Nie ma co prawda innych rzeczy, które z kolei JIRA oferuje, no ale coś za coś. Jak sama nazwa wskazuje, nastawiony jest głównie na liczenie czasu, a dopiero w drugiej kolejności na zarządzanie zadaniami.

Największy jego brak jaki widzę w porównaniu do JIRA to słabe zarządzanie wersjami i release'ami. Można tylko przypisywać milestone'y do tasków.

A może ktoś z Was zna i używa lub używał ClockingIT? Jeśli tak, chętnie przeczytam o wrażeniach.