wtorek, 10 sierpnia 2010

Bzycząco pozorowane obiektów wspaniałości.

Sława!
Tak to przypadkiem zupełnym ostatnimi czasy potrzeba mnie naszła napisania paru testów jednostkowych. Ponieważ nieszczęścia parami chadzać lubują, to do potrzeby tej doszła potrzeba upozorowywania obiektów, a że afektem darzę w tym celu podbudowę Mockito, to nie zastanawiając się Mockito w dłoń, hajda na koń, Baśkę uwolnić od zbója! Uwalnianie idzie gładko, Tatarzyn umyka, że kurz wstrzymuje ruch lotniczy na połowie kontynentu, to lecim na Szczecin bez zagrychy. Aż tu nagle jak mi NullPointerException z siurpryzy między oczy nie przydzwoni, aż się moje testy nogami nakryły.
 - O Ty! W ząbek czesany figlarzu! Ładnie to tak zza węgla bez uprzedzenia po zębach przygarowywać? - mu wygarniam, a ten nic, tylko szczerzy się jak głupi do sera i repetę zasuwa. Ale nie takie cwaniaki z nami pogrywać próbowały, czyż nie proszę Szanowego Państwa? Trzeba wziąć ino sprawę rozumowo, a nie sercowem uczuciem. Takoż więc należy się urwipołciowi dokładnie przykapować i detalicznie inspekt przedstawić. Kod mój niniejszym się przedstawia:
ClassImplentingIface ciff = Mockito.mock(ClassImplentingIface.class);

Mockito.when(ciff.doSomething(3)).thenReturn("ReturnedValue");

assert "ReturnedValue" == ciff.doSomething(3);
Gdzie ClassImplementingIface jest klasą w języku groovy, a objawiony wyjątek to:
java.lang.NullPointerException at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:39) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:40) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:117) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125) at com.blogspot.pacykarz.Testowa.test(Testowa.groovy:19)
Jak widać wyjątek rzuca się już przy konfiguracji stworzonego pozoranta, a śledztwo szczegółowsze wykazuje, iż próba wywołania jakiejkolwiek metody na nim skutkuje podobnym wyjątkiem. Dogłębniejsze rzucenie wzroku narządem i winnego mamy! Otóż jest nim, jakże zacny i szanowany skądinąd, mechanizm mopowania, którego metody w tej sytuacji też są pozorowane przez Mockito i stąd cały ten ambaras. Jak więc wyjść z twarzą i po honorowemu z zajścia?
Sposób pierwszy i niezawodny, to pozorować zamiast klasy jej interfejs(y). Szparko wszystko zaiwania wygalantowanie, że tylko pozazdrościć. Pozazdrościć jeżeli pozorowana klasa nie implementuje interfejsu, który możemy wykorzystać. Co wtedy? Wtedy niestety są ograniczenia i dwudziesty stopień zasilania. Pochylmy się znów nad egzamplą:
//This working always:
Iface mockedIface = Mockito.mock(Iface.class);
Mockito.when(mockedIface.doSomething(Mockito.anyInt())).thenReturn("ReturnedValue");

assert "ReturnedValue" == mockedIface.doSomething(3);
Mockito.verify(mockedIface, Mockito.times(1)).doSomething(Mockito.anyInt());

//This not working with matchers
ClassNotImplentingIface mockedClass = Mockito.mock(ClassNotImplentingIface.class, Mockito.withSettings().extraInterfaces(GroovyInterceptable));

Mockito.when(mockedClass.doSomething(3)).thenReturn("ReturnedValue");

assert "ReturnedValue" == mockedClass.doSomething(3);
Mockito.verify(mockedClass, Mockito.times(1)).doSomething(3);

//This also not working with matchers, but also returns wrong verifications.
ClassNotImplentingIface spiedClass = Mockito.spy(new ClassNotImplentingIface());

Mockito.when(spiedClass.doSomething(3)).thenReturn("ReturnedValue");

assert "ReturnedValue" == spiedClass.doSomething(3);

//Be aware of given 4 times invocation instead of 1!
Mockito.verify(spiedClass, Mockito.times(4)).doSomething(3);

//This also not working with matchers and returns wrong verifications.
ClassNotImplentingIface mockedClass2 = Mockito.mock(ClassNotImplentingIface.class, new Answer() {
  Object answer(InvocationOnMock invocationOnMock) {
      if (invocationOnMock.getMethod().getName() ==~ /.*get.*MetaClass/) {
          return invocationOnMock.callRealMethod();
      } else {
         return null; /* or other default answer*/
      }
  }
});

Mockito.when(mockedClass2.doSomething(3)).thenReturn("ReturnedValue");

assert "ReturnedValue" == mockedClass2.doSomething(3);

//Be aware of given 4 times invocation instead of 1!
Mockito.verify(mockedClass2, Mockito.times(4)).doSomething(3);
Pierwszym obejściem jest dodanie do pozoranta interfejsu GroovyInterceptable, jeżeli możemy sobie na to pozwolić. Istnieje wtedy duże prawdopodobieństwo, że kod testowy będzie działać, z jednym szkopułem. Nie jest możliwe niestety używanie odpowiedników (matcher) z Hamcrest/Mockito, gdyż przy próbie ich użycia, dobrze nam znajomy NPE podstawia hakiem nogie i to tak dyskretnie, że dostajemy dość mylący komunikat o mieszaniu odpowiedników z surowymi typami.
Obejście drugie polega na szpiegowaniu obiektu. Niestety w tym wypadku nie dość, że również nie możemy skorzystać z odpowiedników, to do tego dostajemy wyniki weryfikacji zwiększone o pewien narzut z mopowania, czego mus być świadomym.
Obejście trzecie zaś to użycie przy tworzeniu pozoranta domyślnej odpowiedzi (np. w sytuacji gdy żaden z powyższych konceptów niemożliwy jest do zastosowania). Podobnie jak w drugim, tutaj także brużdżą odpowiedniki i wyniki weryfikacji, a cały myk zaś polega na obsłużeniu wszystkich żądań o metaklasy przez realną, a nie pozorowaną metodę klasy. Kto wie, może nawet mykiem tym dałoby się obsłużyć zastosowanie odpowiedników? Nie upieram się przy stwierdzeniu, że się nie da.
Ostatnim na dziś, czwartym sposobem, bardzo możliwe, że często może być najrozumniejszym, jest użycie innej podbudowy pozoranckiej dedykowanej dla grówiego, np. GMock czy inszego MockFor.