About the author

My photo

Java, Scala, and everything

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.

1 comment:

Leszek Gruchała said...

No i można dodać, że nie można mockować klas finalnych.

Dzięki, hej ;)