wtorek, 2 lipca 2013

I hate Eclipse

Nienawidzę Eclipse’a. Jest to narzędzie, do którego używania jestem zmuszany, a które w swoim rozwoju się gdzieś tam dawno temu zatrzymało. Niby można w nim znaleźć to samo co oferuje konkurencja, czyli IntelliJ Idea, ale diabeł tkwi w szczegółach, które poniżej przedstawiam. Niby autorzy obydwu środowisk przeczytali  zapewne książkę Martina Fowlera Refactoring: Improving the Design of Existing Code, ale nie wszyscy wykonali robotę tak jak trzeba.

Zaznaczam jednocześnie, że spędziłem sporo czasu na poznanie obu tych środowisk, co wcale nie oznacza, że znam je na wylot. Jak by się więc coś nie zgadzało w opisywanych przypadkach, to proszę o sprostowanie w komentarzu.

Podpowiedzi w kodzie

W Idei mamy tak:



wciskam Alt + Enter i mam:



Czyli zaimportowało mi żądaną kolekcję (a dokładniej interfejs kolekcji), który może być parametryzowany. A więc dokładnie to co chciałem i to jest najczęściej oczekiwane przez 99% developerów.
W Eclipse, chcąc zrobić to samo, otrzymuję długaśną listę, która ma mi niby pomóc:



Na co mi java.awt.List? I to na pierwszej pozycji? Przecież ona nie działa z typami generycznymi, więc powinno być jasne, że to się nie będzie kompilować. A skorzystał ktoś z was kiedyś z którejś opcji od 5 w dół? Idea widząc ten sam kod, patrzy nie tylko na nazwę klasy, ale i na to czy występują ostre nawiasy. Przecież generyki mamy w Javie od połowy 2004 roku.

Zgadywanie typów

Tworząc kod z wykorzystaniem TDD, często najpierw wybieram nazwę dla zmiennej, wywołuję jakąś metodę na niej, a na koniec się zastanawiam, czy powinna to być zmienna lokalna czy pole w klasie i jaki de facto powinien być zadeklarowany typ. W Idei robi się to bardzo przyjemnie (Alt + Enter):



i po wybraniu pierwszej opcji:


Idea proponuje mi, co będzie pasowało, skoro na danym obiekcie chcę wywołać metodę put(). W tym przypadku chcę mieć mapę i takowa podpowiedź jest dostępna.
A w Eclipse:



i po wybraniu drugiej opcji muszę sam przeskoczyć do nowo utworzonej definicji. Miało by to sens, gdyby tworzona automatycznie definicja dała się skompilować. Przeskakuję:


ale brak tu jakiejś sensownej podpowiedzi:


ReformatSettings to klasa, w której wklejałem ten kod, a więc zero pomocy ze strony środowiska.

Tworzenie instancji nowych obiektów

W Idei mamy Ctrl + Space:



i widzimy to co pasuje do interfejsu zadeklarowanego po lewej stronie znaku równości. Na pierwszej pozycji najczęściej wybierana opcja, na drugiej pozycji goły interfejs. Dalej jakieś inne możliwe implementacje.
Analogicznie w Eclipse:


A tu bieda aż piszczy. Nawet jak spróbujemy podpowiedzieć Eclipse’owi co ma zrobić, to i tak mu nie wychodzi:


W powyższym przykładzie próbuję utworzyć instancję klasy implementującej interfejs Map. Wpisuję HM, wciskam Ctrl + Space spodziewając się podpowiedzi, aby utworzyć HashMap’ę. Co dostaję? Jakieś HMACParameterSpec – klasa której nigdy nigdzie nie użyłem, nie wiem do czego służy i (najgorsze) która nie implementuje interfejsu Map.
A jak sprawa wygląda w Idei:


Jako podpowiedź dostaję jedyną słuszną klasę jaka w tym kontekście pasuje do zadeklarowanego interfejsu.

Dopasowywanie podpowiedzi do kodu

Na ten przykład natrafiłem tworząc jeden z postów: Logowanie interakcji w Mockito i jak się trochę nad tym zastanowiłem, że takie kodowanie to czysta przyjemność.

Na początek Idea. Mamy taki oto stan i piszemy właśnie konfigurację mocka:


Tutaj akurat zwykłe Ctrl + Space jakoś szczególnie nie zachwyca (jak to się mówi dupy nie urywa). Ale zobaczmy co się dzieje gdy wciskamy Ctrl + Shift + Space (Smart type).


I tutaj już Idea widzi, co można dopasować. Jako że metoda createUser() jako argumenty przyjmuje login i password, to pierwszą preferowaną opcja jest tutaj wcześniej utworzona stała LOGIN. Co ciekawsze to nie ma na liście analogicznej stałej, ale o nazwie PASSWORD, a która teoretycznie na podstawie typu pasuje! Czyż nie jest to powalające? Mamy również poniżej kilka ciekawych metod, które zwracają String’a jako rezultat, który może nam tutaj ewentualnie podpasować i metodę any() z Mockito! Skąd Idea wie, że jeśli się zabawiamy z Mockami, to w tym miejscu możemy (i ma to sens) wywołać tą metodę?

Idźmy dalej. Zatwierdzamy pierwszą propozycję (LOGIN) Enterem i ponownie naciskamy Ctrl + Shift + Space:



I znów analogiczna podpowiedź. Wybieramy pierwszą, właściwą opcję i Enter. Dopisujemy jeszcze co ma się stać po wywołaniu tej metody, czyli:



I teraz tak:
Ctrl + Shift + Space
Ctrl + Shift + Space
Enter
Ctrl + Shift + Space
Enter
Ctrl + Shift + Enter

I mamy wszystko czego nam do szczęścia potrzeba, razem ze średnikiem na końcu linii.



A w Eclipse:



Jedynie nazwy i typy argumentów. Na dalszym miejscu jest trochę lepiej, przy okazji new:



Po naciśnięciu Ctrl + Space:



mamy jakąś konkretną podpowiedź. Później jest już trochę lepiej:


Ale nadal nie jest to co w Idei.

Dopasowywanie argumentów metod

Kolejna sprawa z podpowiadaniem. Często jako argument metody pasuje tylko jeden typ (np. Enum). W przypadku podpowiedzi Eclipse’a jest trochę ubogo:



Dostajemy tylko nazwy argumentów. Trochę lepiej sprawa ma się gdy zaczniemy pisać nazwę oczekiwanego Enuma:



Tylko na co mi te wszystkie klasy od drugiej w dół? Przecież one w ogóle tu nie pasują. Mój argument jest prostym typem wyliczeniowym, który sam zdefiniowałem, który po niczym nie dziedziczy i nikt po nim nie dziedziczy, bo się nie da. Jest to niepotrzebne zaciemnianie ekranu.

Analogicznie w Idei, przy Ctrl + Space mamy coś takiego:



czyli możemy tą wartość wyczarować z obiektów które mamy (args, export), lub z Enuma. Jeśli chcemy podać konkretną wartość, to lepiej w tym wypadku skorzystać z Ctrl + Shift + Space:



i tu już mamy to co na pewno do sygnatury metody będzie pasować w bardziej zwięzłej formie. A jeśli nie pamiętamy tego skrótu klawiszowego (lub nie pamiętamy dokładnego znaczenia), to zawsze można napisać pierwszą literę potrzebnego Enuma i skorzystać ze standardowego Ctrl + Space:



I wtedy pasujące konkretne wartości typu wyliczeniowego zostaną dopasowane do otoczenia.

Uzupełnianie metod zależnie od kontekstu

Często mi się zdarza, że muszę zmienić nazwę metody, gdy mam już porządnie przygotowaną listę argumentów. Oczywiście w Eclipse działa to tragicznie. Przykładowo mamy taką sytuację:


Argumenty metody są już dobrze dopasowane, ale z jakiś względów początkowo źle wybrałem nazwę asercji. Zaczynam pisać poprawną nazwę metody, potem Ctrl + Space:



i dostaję podpowiedź w postaci różnych wariantów oczekiwanej metody. Wybieram pierwszą podpowiedź z brzegu:


i muszę od nowa uzupełniać listę argumentów. Bezsens!

Jak to działa w Idei? Po naciśnięciu Ctrl + Space również dostaję listę pasujących metod:


Wybieram pierwszą:


i wszystko się od razu dopasowuje do kontekstu.

Skróty klawiszowe

Skróty klawiszowe, to trochę delikatna sprawa. Jednym pasują te z Idei, innym te z Eclipse’a. Najczęściej zależy to od tego, których się wcześniej nauczyliśmy i jak bardzo się do nich przyzwyczailiśmy. A przyzwyczajenia to zło. Uznajemy czasem, że skróty są narzucone z góry i są logicznie poukładane. Czy na pewno?

Ctrl + D w Eclipse to usunięcie linii (delete), a w Idei skopiowanie w dół (duplicate). Jest to dla mnie największa bolączka, gdy się przesiadam z jednego środowiska do drugiego, gdyż jest to pierwsza różnica, która mi doskwiera. Do usuwania linii w Idei służy Ctrl + Y, co swoje korzenie ma w edytorze Vim (Yank – yy – usuwa aktualną linię). Ponadto Alt + F7 (wyszukiwanie użyć) i Shift + F6 (zmiana nazwy) mają swoje korzenie z Total Comander'a.

Kolejna sprawa (moja ulubiona), to te genialne czteroklawiszowe skróty w Eclipse, na których można się dorobić trwałego kalectwa palców: Alt + Shift + X, T – wystartowanie testów, lub czegoś innego w zależnie od ostatniej naciśniętej litery. Tą czynność wykonujemy bardzo często w cyklu TDD, więc ten hotkey powinien być łatwiejszy. Jak się doinstaluje MoreUnit, to jest trochę lepiej, ale jak pokazuję ten skrót niektórym kolegom (zwłaszcza tym starszym, którzy etap nauki i rozwoju mają już dawno za sobą), to wymiękają w tym momencie, twierdząc, że taka gimnastyka to dla nich za dużo. Dodatkowo tą ostatnią literę trzeba wcisnąć nie za szybko i nie za późno.

Kolejny super skrót to np. Alt + Shift + Q, C – przeskoczenie do konsoli. Można również przeskoczyć do innych widoków... Tylko co z tego, skoro aby wrócić do edycji kodu należy kliknąć myszką, lub Ctrl + F7? Skrajna ułomność i niedopatrzenie.

W Idei mamy problem przeskakiwania pomiędzy widokami o wiele lepiej rozwiązany. Do tego używamy Alt + numer widoku. A numery widoku mamy zazwyczaj widoczne na ekranie:


Są to te numerki tuż przy nazwie widoku. Dodatkowo, jak chcemy wrócić już do okna edycji to wciskamy po prostu Esc. Czyż to nie jest intuicyjne? A jak nam to okienko przeszkadza, to można wcisnąć Shift + Esc i je zamknąć, równocześnie przeskakując do okna edycji kodu. Można też ponownie skorzystać z kombinacji przeskakującej do aktualnego widoku (Alt + numer), która przy drugim naciśnięciu zamyka dany widok.

I tu wychodzi kolejna wyższość Idei nad Eclipsem. W Eclipsie mamy koncepcję perspektyw: Java, JavaEE, Debug itd. Jak się pomiędzy nimi przełączamy, to pewne widoki (views) znikają a inne się pojawiają, a jeszcze inne są trochę zdeformowane i na innych pozycjach. W Idei odpowiedniki Eclipsowych widoków pojawiają się i znikają „na żądanie”. Oczywiście wszystko sterowane z klawiatury.

Wracając do skrótów klawiaturowych. W Eclipse brakuje sporo ważnych skrótów, aby były od razu po instalacji dostępne. I tak bardzo często korzystam w Idei ze skakania od razu do implementacji (Ctrl + Alt + B). W Eclipse trzeba sobie samemu taki skrót podlinkować, np. Alt + F3, aby nie wiele się różnił od typowego skakania do deklaracji. Niestety nie wielu developerów wie o tej możliwości.

Przypisywanie wartości do pól, zmiennych lokalnych, stałych

W Eclipse mamy tzw. Quick Assist (Ctrl + 2), czyli przypisywanie do pola / zmiennej lokalnej, które działa bardzo kiepsko. Przykładowo w Eclipse dla takiego kodu:


nic nie da się zrobić, gdy kursor jest na końcu linii. Zresztą jak się zaznaczy kod od słówka new do końca linii to i tak Eclipse nic z tym nie wymyśli. Alt + Shift + L (Extract Local Variable) też nic nie pomoże. Idea natomiast radzi sobie z tym wyśmienicie:

Ctrl + Alt + V (Value):


Ctrl + Alt + F (Field)


Tutaj dodatkowo Idea sprawdza, czy to samo wyrażenie nie występuje jeszcze gdzieś w kodzie i sugeruje jego zastąpienie. Oczywiście z pomocą Alt + A można to zrobić bez konieczności użycia myszki.

Ctrl + Alt + C (Constans)


Tutaj dodatkowo Idea zapisuje stałą wielkimi literami, jak to się powszechnie w Javie przyjęło. Oczywiście wszystkie te refaktoringi można cofnąć za pomocą Esc lub Ctrl + Z. A jak w Eclipse wyeksportować do stałej? Mamy kolejnego łamańca: Alt + Shift + T, A, które i tak w przykładowym kontekście nie zadziała i do którego nie ma przypisanego standardowego skrótu.

Eclipse również sobie nie radzi jak ma trochę bardziej skomplikowane wyrażenie, którego wynik chcemy zapamiętać w zmiennej lokalnej. Przykładowo:


Nie działa ani Ctrl + 1, Ctrl + 2, ani Alt + Shift + L. Bieda! A w Idei Ctrl + Alt + V zachowuje się zgodnie z oczekiwaniem:


Idea pozwala nam zadeklarować pole jako final, zmienić przy tej okazji typ zmiennej, jak i sam dorzuca średnik na końcu.

Zaznaczanie tekstu

W Idei jest jeszcze cos takiego jak Ctrl + W (Incremental expression selection). W Eclipse jest niby coś analogicznego: Alt + Shift + Up, ale nie działa to tak fajnie jak w Idei.

Przesuwanie kodu w pionie

Kolejnym skrótem, którego mi bardzo brakuje w Eclipse jest inteligentne przesuwanie kodu Ctrl + Shift Up/Down (Move Statement Up). W Eclipse brak ekwiwalentnego skrótu, a działa on tak:

Przed:


Po:


Czyli po naciśnięciu Ctrl + Shift + Up cała metoda setName() została przeniesiona powyżej getName(), czyli tam gdzie ma ona sens. Jest to bardzo przydatna funkcjonalność podczas refaktoringu.

Komentowanie

Jeszcze irytująca mnie rzecz, to działanie Ctrl + / w Eclipse. Skrót ten wstawia komentarz w aktualnie zaznaczonych liniach, ale działa tylko w Javie. W przypadku plików XML, HTML i innych należy używać  kolejnego, ale działającego wszędzie łamańca: Ctrl + Shift + C, zamiast prostego Ctrl + /. W Idei ten ostatni skrót działa wszędzie i wstawia komentarz zależnie od kontekstu w jakim się znajdujemy.

Refactoring

W Idei gdy zmieniamy nazwę pola w klasie (Shift + F6), które to posiada gettery i settery, to dostajemy zapytanie, czy te metody dostępowe również przeorać:


Dodatkowo Idea sugeruje, aby dopasować nazwę parametru w konstruktorze do nowej nazwy pola:


wciskamy spację, lub Alt + A aby wybrać wszystkie sugestie i Ctrl + Enter aby zamknąć okno. Następnie Idea jeszcze sugeruję nazwę pola, która jest użyta jako tekst w metodzie toString():


Oczywiście akceptujemy to za pomocą Alt + D i gotowe! Wszystko co chcieliśmy zostało dopasowane.

A w Eclipse? Eclipse zmienia tylko nazwę pola. Musimy sami pamiętać o getterach / setterach, konstruktorach i tekstach w toString(). Czyli mamy jakieś plus 4 więcej kroków do wykonania!

W Idei również działa całkiem dobrze (albo właściwie nie wiele gorzej) refaktoring, gdy go wykonujemy, gdy nasz kod się nie kompiluje. Dla Eclipse’a jest to najczęściej nie do wykonania.

Usability

Standardowo w Eclipse jest sporo ukrytych funkcjonalności. Czyli jeśli nie przeczytasz setki tutoriali, lub jeśli nie przeklikasz każdej opcji z okna Preferences, to będziesz mógł sobie tylko żyły wypruwać. I tak podczas Legacy Code Retreat, miałem spory problem z głupim wklejeniem tekstu wielolinijkowego do edytora tak, aby był traktowany jako String. Praktyczny przykład poniżej:


W Eclipse funkcjonuje to tak:


Czyli istna tragedia. Razem z osobą, z którą aktualnie byłem sparowany, straciliśmy z 10 minut, na doprowadzenie tego do stanu używalności, za pomocą metody find & replace. W Idei jest to out of the box:


Później jeszcze poszukałem chwilkę w necie i znalazłem rozwiązanie na to:


Źródło: http://stackoverflow.com/questions/2159678/paste-a-multi-line-java-string-in-eclipse

Inne, ogólne

Kolejny wielki błąd w designie Eclipsa to brak ciągłej synchronizacji pomiędzy tym co jest w edytorze a stanem na dysku twardym. Wystarczy czasem zrobić builda z zewnątrz lub jakiegoś update’a z repozytorium i podczas próby otwarcia pliku otrzymujemy coś podobnego do tego widoku:


Jeszcze jestem w stanie zrozumieć, że Eclipse nie odświeża sobie wszystkich plików ze wszystkich otwartych projektów (choć w Idei to działa dobrze i szybko), ale w momencie próby otworzenia pliku mógłby sobie już sam automatycznie to odświeżyć, zamiast wypisywać out of sync.

Dodatkowo jakość pluginów Eclipsowych jest bardzo słaba. Są one rozwijane po za podstawowym środowiskiem, czyli czasem działają, czasem nie, a po jakimś czasie autorzy często zarzucają jego rozwój. Tak się np. stało z MouseFeed, który przez długi czas nie działał z Eclipsem 4.X. Na szczęście plugin został ostatnio reaktywowany: Eclipse Mousefeed Plugin Merged With Marketplace Plugin. W przypadku innych pluginów nie wszystko zawsze dobrze działa. W Idei natomiast rozwój pluginów jest wspierany przez samych twórców, którzy dostarczają odpowiednią ich jakość i ich niezawodność.

Kilka dni temu (tj. 26 czerwca 2013) opublikowano nową wersję Eclipse'a 4.3 Kepler. Jeszcze jej nie sprawdzałem, ale pewnie nie wiele się zmieniło. Zrobię to pewnie za jakiś czas, jak już pluginy dostosują.

To chyba by było na tyle, choć jak by się chciało, to by się jeszcze znalazło parę Eclipse’owych niedogodności, ale wtedy nigdy bym nie opublikował tego posta. Następnym razem jak ktoś się mnie zapyta, „a co takiego ma Idea, czego nie ma Eclipse?” to go po prostu odeślę do tego artykułu.

A ty które wybierasz?

niedziela, 17 lutego 2013

Porównanie tradycyjnych asercji, Hamcrest'a i FEST'a

Po tygodniowym szaleństwie w Alpach i niewielkim odmrożeniu, czas wrócić do pracy aby odpocząć ;) Temat może nie będzie zbytnio odkrywczy, ale akurat prezentowałem kolegom w projekcie jak działa FEST Asserts 2.X, więc niewielkim nakładem pracy można coś na blogu o tym skrobnąć. Jak ktoś już zna i używa, to nie musi dalej czytać. A kto nie zna jeszcze FEST’a to niech się przyjrzy przykładom przygotowanym przeze mnie i upiększy swoje nowe testy.

O wyższości FEST’a nad Hamcrest'em słyszałem po raz pierwszy na prezentacji Bartosza Bańkowskiego i Szczepana Fabera pt. Pokochaj swoje testy na Wroc JUG. I to już było wiadome w 2009 roku. W międzyczasie Hamcrest został wciągnięty do JUnit'a i Mockito, ale to z innych względów. A tym czasem FEST Asserts został trochę odświeżony i podbito jego numerek wersji. Dalej jest bardzo dobry i pozwala lepiej definiować fachowe asercje.

Czas na przykład. Jest on całkiem życiowy, aczkolwiek domena problemu została zmieniona i trochę uproszczona dla przejrzystości przykładu. Ja po zobaczeniu podobnego kodu u siebie w projekcie, podczas review, gdzie pewien proces zwracał sporo skomplikowanych wyników, stwierdziłem, że muszę natychmiast zastosować FEST’a. Trochę nawijki z architektem i dostałem zielone światło. Test w pierwszej, typowej JUnit’owej formie poniżej:

@Test
public void shouldGenerateInnerPlanets() {
 // given
 SolarSystem solarSystem = new SolarSystem();

 // when
 Set<Planet> innerPlanets = solarSystem.getInnerPlanets();

 // then
 assertEquals(4, innerPlanets.size());
 for (Planet innerPlanet : innerPlanets) {
  if (innerPlanet.getName().equals("Mercury")) {
   List<Gases> mercuryGases = asList(Gases.OXYGEN, Gases.SODIUM, Gases.HYDROGEN);
   assertPlanet(innerPlanet, RotationDirection.LEFT, 4_879_400, 87.96935, 3.701, mercuryGases);
  } else if (innerPlanet.getName().equals("Venus")) {
   List<Gases> venusGases = asList(Gases.CARBON_DIOXIDE, Gases.NITROGEN);
   assertPlanet(innerPlanet, RotationDirection.RIGHT, 12_103_700, 224.700_96, 8.87, venusGases);
  } else if (innerPlanet.getName().equals("Earth")) {
   List<Gases> earthGases = asList(Gases.NITROGEN, Gases.OXYGEN);
   assertPlanet(innerPlanet, RotationDirection.LEFT, 12_756_273, 365.256_363_004, 9.806_65, earthGases);
  } else if (innerPlanet.getName().equals("Mars")) {
   List<Gases> marsGases = asList(Gases.CARBON_DIOXIDE, Gases.NITROGEN);
   assertPlanet(innerPlanet, RotationDirection.LEFT, 6_804_900, 686.960_1, 3.69, marsGases);
  } else {
   throw new AssertionError("Undefined Planet Name: " + innerPlanet.getName()
     + " in result\n" + innerPlanet.toString());
  }
 }
}

private void assertPlanet(Planet planet, RotationDirection direction, long diameterInMeter,
        double yearInEarthDays, double acceleration, List<Gases> atmosphereGases) {
 assertEquals(direction, planet.getRotationDirection());
 assertEquals(diameterInMeter, planet.getDiameter());
 assertEquals(yearInEarthDays, planet.getSiderealYear().inEarthDays(), 0.01);
 assertEquals(acceleration, planet.getAcceleration(), 0.01);
 for (Gases gas : atmosphereGases) {
  assertTrue("Planet " + planet.getName() + " doesn't contains " + gas,
    planet.getAtmosphereGases().contains(gas));
 }
}  

Testujemy metodę generującą zbiór planet wewnętrznych, czyli 4ch pierwszych z naszego układu słonecznego. Pierwsza asercja w linii 10 sprawdza, czy metoda zwróciła odpowiednią ilość planet. Co się stanie, gdy w wyniku będzie nieprawidłowa ilość elementów? Będziemy o tym wiedzieć, ale co dokładnie jest, a czego nie ma, nie będzie wyszczególnione w komunikacie błędów zepsutego testu. Można do pracy wtedy za prząść debugera, ale ja jego nienawidzę.

Kolejny problem to konieczność odszukania właściwego obiektu. Nasza testowana metoda zwraca zbiór planet, przez co nie możemy być pewni, co do kolejności obiektów w wyniku. Dzięki temu dostaliśmy brzydki blok if-else. W takim każdym if’ie definiowana jest najpierw lista gazów, jakie ma zawierać dana planeta. Można by było to co prawda zrobić inline w wywołaniu assertPlanet(), ale wtedy w ogóle łańcuszek argumentów byłby nieczytelny.

Drugim elementem każdego if’a jest właśnie wywołanie assertPlanet() z jakimiś argumentami. Co one oznaczają? Trzeba skoczyć do ciała metody i sprawdzić. Jaka jest ich kolejność? Przy takiej ich liczbie na pewno jej nie zapamiętamy na dłużej niż 5 minut. Można ewentualnie zrezygnować z metody assertPlanet() i ją zinline’nować, ale wtedy mamy sporo duplikacji w kodzie, niskiej czytelności i słabej reużywalności.

Czas na wersję tego samego kodu z Hamcrestem.

@Test
public void shouldGenerateInnerPlanets() {
 // given
 SolarSystem solarSystem = new SolarSystem();

 // when
 Set<Planet> innerPlanets = solarSystem.getInnerPlanets();

 // then
 assertThat(innerPlanets, hasItem(withName("Mercury")));
 Planet mercury = findPlanetByName(innerPlanets, "Mercury");
 assertThat(mercury, is(rotation(RotationDirection.LEFT)));
 assertThat(mercury, is(diameterInMeter(4_879_400)));
 assertThat(mercury, is(yearLongInEarthDays(87.96935)));
 assertThat(mercury, is(acceleration(3.701)));
 assertThat(mercury, hasGas(Gases.OXYGEN));
 assertThat(mercury, hasGas(Gases.SODIUM));
 assertThat(mercury, hasGas(Gases.HYDROGEN));

 assertThat(innerPlanets, hasItem(withName("Venus")));
 Planet venus = findPlanetByName(innerPlanets, "Venus");
 assertThat(venus, is(rotation(RotationDirection.RIGHT)));
 assertThat(venus, is(diameterInMeter(12_103_700)));
 assertThat(venus, is(yearLongInEarthDays(224.700_96)));
 assertThat(venus, is(acceleration(8.87)));
 assertThat(venus, hasOnlyGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN));

 assertThat(innerPlanets, hasItem(withName("Earth")));
 Planet earth = findPlanetByName(innerPlanets, "Earth");
 assertThat(earth, is(rotation(RotationDirection.LEFT)));
 assertThat(earth, is(diameterInMeter(12_756_273)));
 assertThat(earth, is(yearLongInEarthDays(365.256_363_004)));
 assertThat(earth, is(acceleration(9.806_65)));
 assertThat(earth, hasGases(Gases.NITROGEN, Gases.OXYGEN));
 assertThat(earth, hasNotGases(Gases.SODIUM));

 assertThat(innerPlanets, hasItem(withName("Mars")));
 Planet mars = findPlanetByName(innerPlanets, "Mars");
 assertThat(mars, is(rotation(RotationDirection.LEFT)));
 assertThat(mars, is(diameterInMeter(6_804_900)));
 assertThat(mars, is(yearLongInEarthDays(686.960_1)));
 assertThat(mars, is(acceleration(3.69)));
 assertThat(mars, hasGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN));
 assertThat(mars, hasNotGases(Gases.OXYGEN));

 assertThat(innerPlanets, hasSize(4));
}


Kod wygląd już bardziej czytelnie, ale musiałem do tego dopisać 8 klas Matcher’ów. Właściwie na każdą asercję potrzeba oddzielnego Matcher’a. Mimo wszystko nie pozbyliśmy się problemu wyszukiwania potrzebnego obiektu, co widać w linii 11. Może jest jakieś lepsze obejście na to, ale ja nie znalazłem. Na koniec testu, warto jeszcze sprawdzić, czy testowany zbiór posiada żądaną ilość elementów. Jednak tutaj i tak ewentualny komunikat błędu będzie mało czytelny.

Dobra czas na gwiazdę wieczoru, czyli na test napisany z wykorzystaniem FEST’a.

@Test
public void shouldGenerateInnerPlanets() {
 // given
 SolarSystem service = new SolarSystem();

 // when
 Set<Planet> innerPlanets = service.getInnerPlanets();

 // then
 assertThat(innerPlanets)
   .containsPlanetWithName3("Mercury")
   .withRotation(RotationDirection.LEFT)
   .withDiameterInMeter(4_879_400)
   .withYearInEarthDays(87.96935)
   .withAcceleration(3.701)
   .withGas(Gases.OXYGEN)
   .withGas(Gases.SODIUM)
   .withGas(Gases.HYDROGEN);

 assertThat(innerPlanets)
   .containsPlanetWithName3("Venus")
   .withRotation(RotationDirection.RIGHT)
   .withDiameterInMeter(12_103_700)
   .withYearInEarthDays(224.700_96)
   .withAcceleration(8.87)
   .containsOnlyGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN);

 assertThat(innerPlanets)
   .containsPlanetWithName3("Earth")
   .withRotation(RotationDirection.LEFT)
   .withDiameterInMeter(12_756_273)
   .withYearInEarthDays(365.256_363_004)
   .withAcceleration(9.806_65)
   .containsGases(Gases.NITROGEN, Gases.OXYGEN)
   .doesNotContainGases(Gases.SODIUM);

 assertThat(innerPlanets)
   .containsPlanetWithName3("Mars")
   .withRotation(RotationDirection.LEFT)
   .withDiameterInMeter(6_804_900)
   .withYearInEarthDays(686.960_1)
   .withAcceleration(3.69)
   .containsGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN)
   .doesNotContainGases(Gases.OXYGEN);

 assertThat(innerPlanets)
   .containsOnlyPlanets("Mercury", "Venus", "Earth", "Mars");
} 

Początek testu jest taki jak w poprzednich przypadkach, ale od sekcji  //then  robi się ciekawie. Widać wyraźnie 4 logicznie oddzielone od siebie bloki, po jednym na sprawdzenie każdej z planet. Można też łatwo zauważyć łańcuch wywołań, który powinien nam być przynajmniej ze wzorca Bulder znany, a który można ogólniej zdefiniować, jako fluent interface. Hamcrest stara się również iść w tym kierunku, ale jak dla mnie te zapis jest bardziej zwięzły i przejrzysty. Mamy po prostu nazwę właściwości, którą weryfikujemy, konkretną wartość i zazwyczaj jakiś prefix w nazwie metody. Ja preferuję dla kolekcji prefix contains, a dla konkretnych właściwości with lub has. Ale jest to już kwestia umowna, co komu lepiej leży.

Jak więc tworzyć takie asercje?

public class PlanetSetAssert extends AbstractAssert<PlanetSetAssert, Set<Planet>> {

    private PlanetSetAssert(Set<Planet> actual) {
        super(actual, PlanetSetAssert.class);
    }

    public static PlanetSetAssert assertThat(Set<Planet> actual) {
        Assertions.assertThat(actual)
                .isNotNull();
        return new PlanetSetAssert(actual);
    }

    public PlanetSetAssert containsPlanetWithName(String expectedPlanetName) {
        for (Planet planet : actual) {
            if(planet.getName().equals(expectedPlanetName)) {
                return this;
            }
        }
        throw new AssertionError("Actual Set doesn't contains Planet with name: " + expectedPlanetName);
    } 

Aby stworzyć własną, fachową asercję, należy rozszerzyć klasę AbstractAssert. Ta przyjmuje za dwa typy generyczne kolejno samą siebie i typ który będziemy obsługiwać, w tym przypadku Set<Planet>. Później definiujemy konstruktor, który jako argument bierze badany obiekt. Konstruktora nie będziemy wywoływać poza obrębem tej klasy, więc można go uczynić prywatnym. Korzystać natomiast będziemy ze statycznej metody fabrykującej assertThat(), która to jako jedyna ze zdefiniowanego wcześniej konstruktora korzysta. Ja preferuję w tym miejscu od razu sprawdzić, czy przekazany obiekt nie jest null’em. Oficjalny tutoral (swoją drogą bardzo dobry, z licznymi komentarzami) preferuje wykonywać to sprawdzenie w każdej metodzie, ale jak dla mnie to jest niepotrzebna redundancja.

Na koniec jest przykładowa implementacja metody, sprawdzającej, czy w danym zbiorze znajduje się planeta o podanej nazwie. W tym przypadku metoda zwraca this, co pozwala wywoływać łańcuchowo dalsze asercje na zbiorze planet. Minusem takiego podejścia jest konieczność definiowania własnego komunikatu błędu, w przypadku niepowodzenia – linia 19. W przypadku gdy zależy nam na sprawdzeniu, czy dany zbiór zawiera szukany element, można tą samą funkcjonalność zaimplementować troszkę inaczej.

public PlanetSetAssert containsPlanetWithName2(String expectedPlanetName) {
 Planet expectedPlanet = new Planet(expectedPlanetName);

 Assertions.assertThat(actual)
   .usingElementComparator(new PlanetNameComparator())
   .contains(expectedPlanet);
 return this;
}

private class PlanetNameComparator implements Comparator<Planet> {
 @Override
 public int compare(Planet p1, Planet p2) {
  return p1.getName().compareTo(p2.getName());
 }
} 


W tym przypadku, za pomocą metody usingElementComparator(), podmieniamy sposób porównywania elementów w kolekcji. Bardzo użyteczny mechanizm. Nadpisujemy tylko typowego porównywacza (komparatora) i gotowe. I co fajniejsze, to dostajemy bardzo przejrzysty komunikat błędu:

java.lang.AssertionError: expecting:
<[Venus, Earth, Mars]>
 to contain:
<[Mercury]>
 but could not find:
<[Mercury]>
 according to 'PlanetNameComparator' comparator
 at com.blogspot.mstachniuk.hamcrestvsfest.fest.PlanetSetAssert.containsPlanetWithName2(PlanetSetAssert.java:34)
  at com.blogspot.mstachniuk.hamcrestvsfest.fest.SolarSystemFestTest.shouldGenerateInnerPlanets(SolarSystemFestTest.java:24)

Właśnie przejrzyste komunikaty błędów (zwłaszcza wśród kolekcji) to siła tego narzędzia.

Teraz czas na moją ulubioną implementację tego typu metody, która to pozwala nam budować jeszcze bardziej płynące interfejsy:

public PlanetAssert containsPlanetWithName3(String expectedPlanetName) {
 Planet expectedPlanet = new Planet(expectedPlanetName);

 PlanetNameComparator comparator = new PlanetNameComparator();
 Assertions.assertThat(actual)
   .usingElementComparator(comparator)
   .contains(expectedPlanet);

 for (Planet planet : actual) {
  if(comparator.compare(planet, expectedPlanet) == 0) {
   return PlanetAssert.assertThat(planet);
  }
 }
 return null;
} 

Początek metody jest analogiczny jak w poprzednim przykładzie. Natomiast zamiast zwracać obiekt this, wyszukuję żądany element i zwracam już asercję, dla tej znalezionej planety. Dzięki temu mogę już sprawdzić inne jej właściwości. Właściwie, mógłbym w pętli wywołać zwykłą metodę equals() do wyszukania elementu, ale skoro mam już napisanego komparatora…

Wrcając jeszcze to całego kodu z listingu 3, to warto na końcu metody testowej jeszcze sprawdzić, czy w zwracanym wyniku, są tylko te planety, których się spodziewamy (jeśli tak mamy w wymaganiach). Przykładowa implementacja, jak to można zrobić:

public PlanetSetAssert containsOnlyPlanets(String... planets) {
 List<Planet> expectedPlanets = new ArrayList<Planet>();
 for (String planet : planets) {
  Planet p = new Planet(planet);
  expectedPlanets.add(p);
 }
 Assertions.assertThat(actual)
   .usingElementComparator(new PlanetNameComparator())
   .containsOnly(expectedPlanets.toArray(new Planet[expectedPlanets.size()]));
 return this;
}

Co ogólnie jest jeszcze fajnego w takich asercjach? To to, że sprawdzamy dokładnie to co chcemy sprawdzić, zamiast sprawdzać zawsze wszystko, jak było to w pierwszej wersji testu. Również zdefiniowałem sobie różne metody odnośnie sprawdzania występujących na danej planecie gazów atmosferycznych: withGas(Gases.OXYGEN), containsGases(Gases.NITROGEN, Gases.OXYGEN), containsOnlyGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN), doesNotContainGases(Gases.SODIUM). Do wyboru do koloru / potrzeby. Po więcej szczegółów odsyłam do wiki projektu.

Z ciekawostek, warto jeszcze zaprezentować generator FEST asercji. Można sobie to samemu przetestować, ściągając mój projekt z github’a i odpalając assertion-generator\planet.bat. Skrypt automatycznie kopiuje wygenerowaną klasę do podkatalogu fest2 gdzieś w hierarchii testowej, gdzie jednocześnie znajduje się test niej korzystający. Wystarczy zmienić nazwę pakietu i doimportować potrzebne klasy, aby test z powrotem działał.

Tak wygląda kod, który korzysta z przykładowo wygenerowanych asercji:

@Test
public void shouldGenerateInnerPlanets() {
 // given
 SolarSystem service = new SolarSystem();

 // when
 Set<Planet> innerPlanets = service.getInnerPlanets();

 // then
 Planet mercury = findPlanetByName(innerPlanets, "Mercury");
 assertThat(mercury)
   .hasRotationDirection(RotationDirection.LEFT)
   .hasDiameter(4_879_400)
   .hasSiderealYear(new SiderealYear(87.96935))
   .hasAcceleration(3.701)
   .hasAtmosphereGases(Gases.OXYGEN, Gases.SODIUM, Gases.HYDROGEN);

 Planet venus = findPlanetByName(innerPlanets, "Venus");
 assertThat(venus)
   .hasRotationDirection(RotationDirection.RIGHT)
   .hasDiameter(12_103_700)
   .hasSiderealYear(new SiderealYear(224.700_96))
   .hasAcceleration(8.87)
   .hasAtmosphereGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN);

 Planet earth = findPlanetByName(innerPlanets, "Earth");
 assertThat(earth)
   .hasRotationDirection(RotationDirection.LEFT)
   .hasDiameter(12_756_273)
   .hasSiderealYear(new SiderealYear(365.256_363_004))
   .hasAcceleration(9.806_65)
   .hasAtmosphereGases(Gases.NITROGEN, Gases.OXYGEN);

 Planet mars = findPlanetByName(innerPlanets, "Mars");
 assertThat(mars)
   .hasRotationDirection(RotationDirection.LEFT)
   .hasDiameter(6_804_900)
   .hasSiderealYear(new SiderealYear(686.960_1))
   .hasAcceleration(3.69)
   .hasAtmosphereGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN);
}


Niestety musimy sami sobie dopisać metodę szukającą odpowiednią planetę, albo asercję dla Set<Planet>. W wygenerowanym kodzie jest jednoznaczna konwencja, że wszystkie metody sprawdzające nasze własności, dostają prefix has. Można sobie oczywiście to i sporo innych rzeczy zmienić w szablonach generowanego kodu.

A jak komuś mało takie generowanie kodu, to może sobie ten kod generować podczas builda maven’owego, dzięki Maven plugin for fest-assertion-generator. Można to również w moim przykładowym projekcie przetestować, usuwając pliki *Assert.java z katalogu: com\blogspot\mstachniuk\hamcrestvsfest\. Wówczas po odpaleniu mvn install, lub mvn test lub mvn generate-test-sources zostaną wygenerowane na nowo pliki z asercjami. Wówczas test SolarSystemFest3Test, który z nich korzysta, będzie nadal działał.

Ostatnio pojawił się jeszcze plugin do Eclipse’a: FEST eclipse plugin generujący prosto z tegoż środowiska nasze asercje. Zapowiada się ciekawie, więc zachęcam do skorzystania.

Pomyślnych asercji!

wtorek, 15 stycznia 2013

Modyfikowanie niemodyfikowalnych kolekcji w Javie

Jakiś czas temu w pracy natrafiłem na coś, czego na pierwszy rzut oka nie zrozumiałem czemu nie działa. Mianowicie była tworzona niemodyfikowalna lista, zawierająca pewne elementy. Jednak po chwili lista stawała się pusta. Samo usuwanie z listy działa poprawnie (tzn. rzuca wyjątkiem), Kod poniżej:
public class ModifyUnmodifiableListTest {

    final List<String> exampleList = new ArrayList<String>();

    @Before
    public void setUp() {
        exampleList.add("text 1");
        exampleList.add("text 2");
        exampleList.add("text 3");
    }

    @Test(expected = UnsupportedOperationException.class)
    public void shouldThrowExceptionWhenModifyUnmodifiableList() {
        List<String> unmodifiableList = Collections
                .unmodifiableList(exampleList);
        unmodifiableList.clear();
    }

Operacja clear() wyrzuca UnsupportedOperationException zgodnie z oczekiwaniem. Poniżej kod który mnie zadziwił:

    @Test
    public void shouldModifyUnmodifiableList() {
        // given
        List<String> unmodifiableList = Collections
                .unmodifiableList(exampleList);

        // when
        exampleList.clear();

        // then
        assertEquals(0, unmodifiableList.size());
    }

Test przechodzi, czyli na nowoutworzonej liście nie ma elementów!

Co się stało? Otóż metoda jak można przeczytać w dokumentacji:
Returns an unmodifiable view of the specified list
Zwraca nam widok (projekcję) na oryginalną listę. Czyli źródłową kolekcje można dalej modyfikować!

Jak można więc stworzyć niezależną kopię kolekcji? A no znajdzie się kilka sposobów. Jednym z prostszych jest skonwertowanie kolekcji do tablicy:
    @Test
    public void shouldCloneUnmodifiableListToArray() {
        // given
        String[] tab = exampleList.toArray(
                new String[exampleList.size()]);

        // when
        exampleList.clear();

        // then
        assertEquals(3, tab.length);
        assertEquals("text 1", tab[0]);
    }

Jak komuś koniecznie potrzebna lista lub inna kolekcja, to można z powrotem z tablicy zrobić kolekcje.

Inną możliwością jest metoda na piechotę, czyli ręczne dodanie elementów kolekcji do nowej kolekcji.
    @Test
    public void shouldCloneUnmodifiableListInForLoop() {
        // given
        List<String> list = new ArrayList<String>();
        for (String s : exampleList) {
            list.add(s);
        }

        // when
        exampleList.clear();

        // then
        assertEquals(3, list.size());
        assertEquals("text 1", list.get(0));
    }

Można jeszcze próbować walczyć z Collections.copy(...) ale tutaj lista docelowa musi mieć co najmniej tyle samo elementów, co lista źródłowa.

Rozwiązanie jakie mi jeszcze wpadło do głowy to wykorzystanie Apache Commons a dokładniej SerializationUtils.clone(...).

Jest to już bardziej uniwersalna metoda robienia głębokiej kopii, która działa na wszystkim co jest serializowane.

A jakie jest rozwiązanie najprostsze? Po prostu skorzystać z konstruktora docelowej kolekcji:
    @Test
    public void shouldCreateNewListBasedOnList() {
        // given
        List<String> list = new ArrayList<String>(exampleList);

        // when
        exampleList.clear();

        // then
        assertEquals(3, list.size());
        assertEquals("text 1", list.get(0));
    }

To tyle. Cały kod to pobawienia się dostępny jest na githubie: ModifyUnmodifiableList.

wtorek, 11 grudnia 2012

Pierwszy Legacy Code Retreat


W sobotę miało miejsce niezwykle wydarzenie, mianowicie drugi Global Day of Code Retereat. Jako że niedawno było organizowane w Wolfsburgu to wydarzenie, postanowiono tym razem zrobić Legacy Code Retreat dla odmiany. Całość odbyła się z inicjatywy tych samych osób i w tym samym miejscu i sponsoringu.

Podczas Legacy Code Retreat pracuje się z już gotowym kodem, który można znaleźć na githubie: Trivia Game. Kod jest przygotowany w kilku językach i jest właściwie nie testowany. Osiągnięto to za pomocą metod zwracających void, wymieszaniu wyświetlania wyników z logiką, niejasne przeznaczenie zmiennych i boską klasę. Zadaniem uczestników jest ogarnięcie tego chaosu programując w parach.

Miałem dobre przeczucie podczas pierwszej sesji, aby początkowo stworzyć test testujący integracyjnie to co jest. Z założenia aktualny stan jest fachowo poprawny, więc nawet jak się widzi coś w kodzie co wydaje się nie prawdą, należy to zaakceptować. Taki całościowy test nazywa się Golden Master. Najlepiej stworzyć ich kilka i nie mogą one nigdy być czerwone, gdy będziemy modyfikować kod.

Jak już mamy kilka testów, możemy zacząć refaktoring. Najlepiej taki, do którego wsparcie daje nam IDE, gdyż wtedy nie powinniśmy nic zniszczyć. I tak powoli można pisać bardziej szczegółowe testy, konkretnych metod.

Podczas Legacy Code Retreat można ćwiczyć m.in. następujące rzeczy:
  • tworzenie Golden Master
  • pisanie testów dla nietestowanego kodu
  • testowanie poprzez partial mocking (nazywane również Subclass To Test)
  • refaktoring do kodu obiektów
  • refkatoring do kodu czysto funkcyjnego
  • refaktoring do ValueObjects

Miałem podczas warsztatów okazję przez dwie sesje pracować z kodem Scalowym. Jednak słabe wsparcie środowiska i mała przerwa w eksplorowaniu tegoż języka, sprawiły, że było bardzo ciężko przemieścić się do przodu. Inna sprawa to standardowe ułomności Eclipse’a, ale to już zasługuje na osobny post. Podczas kilku sesji korzystałem też z IntelliJ IDEA i tam już było trochę lepiej. Więcej nie opisuję, aby nie psuć wam ewentualnej zabawy z Legacy kodem, podczas warsztatów, które sami u siebie zorganizujecie.

Co do jakości i organizacji eventu nie ma się do czego przyczepić. Nie było problemów ze stołami, przedłużaczami, rzutnikami. Były naklejki z imionami, odliczanie czasu podczas każdej sesji, kanapki na śniadanie, kawa, herbata, soki, Club Mate i inne napoje. Obiad również był dobry, ciepły, wystarczył dla wszystkich i jednocześnie nie wiele się zmarnowało. Zabrakło mi jedynie videokonferencji z innymi miastami, jak to było rok temu. A to ja w sumie mogłem podsunąć pomysł organizatorom. Niestety jakoś mi to uciekło.

Osobiście wolę tworzyć nowy kod, niż grzebać w starym, tym bardziej w Legacy. Z tego powodu impreza podobała mi się trochę mniej niż typowy Code Retreat. Z drugiej strony, w naszej profesji umiejętność pracy z nieswoim kodem jest właściwie niezbędna i trzeba ją wykorzystywać od czasu do czasu. Oby taki sytuacji było jak najmniej, więc dbajmy o to, aby nie tworzyć Legacy Kodu.

poniedziałek, 10 grudnia 2012

Java Developers Day 2012


Długo się zbierałem do napisania tego postu, ale w końcu się udało, skoro to czytacie. Kurs podstaw programowania funkcyjnego w Scali dobiegł końca, zaliczony na 96.5 %, certyfikat ściągnięty, spadł śnieg, motywacja wzrosła, więc można się zająć blogiem.

Co do samej konferencji Java Developers Day, to się do niej zraziłem w 2010 roku. Tym razem jednak wygrałem darmową wejściówkę, więc głupio by było nie skorzystać. Pojechałem więc do Krakowa.

Pierwsza prezentacja na jakiej byłem: The dark art of performance tuning or how to become a performance hero without spending a penny on tools, Leonida Igolnika. Facet pracuje w Oracle jako “Vice President of Product Development” I gadał ponad 5 minut o sobie. Jak to mówią, ten co dużo mówi o sobie, ma mało do powiedzenia na temat prezentacji, więc miałem małe obawy co do wykładu. Jednak szybko się pozytywnie rozkręciło. Leonid przekonywał nas, że w przypadku problemów z wydajnością naszych aplikacji, nie powinniśmy patrzeć do kodu, a na narzędzia.

I tak na początek musimy poznać nasze środowisko (system operacyjny) jaki i Runtime Environment. Prelegent pokazał jak można to zrobić na Unixie. Chcąc sprawdzić parametry JVM na Windowsie, można odpalić (oczywiście z konsoli) jps, aby poznać PID naszej aplikacji / serwera aplikacji. Następnie za pomocą jinfo można sprawdzić, co tak naprawdę jest uruchomione (np. na produkcji) i z jakimi parametrami VM.

Następnie za pomocą jmap -heap PID można sprawdzić używaną implementację GC i statystyki pamięci. Narzędzie to posiada jeszcze kilka ciekawych opcji: zrzuty pamięci, histogram obiektów heap’u i inne statystyki. To na co należy szczególnie zwrócić uwagę, to ile zajmują Stringi w naszej pamięci, gdyż one siedzą w przestrzeni PermGen (permanent generation). Problem ten nie występuje w Javie 7, gdyż teksty nie są dłużej alokowane w tym obszarze pamięci, a na heap’ie. Rozwiązuje to kilka problemów. Więcej na ten temat można przeczytać na Java SE 7 Features and Enhancements.

Coś było jeszcze wspomniane o Kirku Pepperdine ekspercie od tuning’owania Javy i autorze słów: „dominating consumer of the CPU”. Prelegent polecał również to video: Everything I Ever Learned about JVM Performance Tuning @twitter. Można się z niego dowiedzieć, ile zajmują w pamięci instancje prostych obiektów, jak jest reprezentowana wartość null w polach klas i jak często się uruchamia full GC na serwerach Twittera.

Następnie było przedstawiane chyba najlepsze darmowe narzędzie do profilowania, czyli jVisualVM. Łączy ono przedstawione wcześniej aplikacje konsolowe i jest rozszerzalne przez pluginy. Prelegent pokazał, że jak mamy duże zużycie CPU i jest to głównie System Time, to może to być problem z przełączaniem kontekstu.

Później Leonid przedstawił plugin do jVisualVM o nazwie TDA - Thread Dump Analyzer. Pomaga  on nam analizować stacktrace’y. Był też przykład błędnego użycia WeakHashMap i synchronized. W końcu niemal każde użycie tego słowa kluczowego, jest informacją, że tu gdzieś się czai błąd. Było jeszcze o narzędziu GCViewer, do analizowania tego co Garbage Collector wypluwa, o opcji -XX:-HeapDumpOnOutOfMemoryError i o Memory Analyzer - plugin’ie do Eclipse, który pozwala przeanalizować referencje pomiędzy obiektami.

Po takim wykładzie aż chce się mieć problemy w projekcie i możliwość dostania tego często jakże niewdzięcznego zadania, tylko po to aby pobawić się tymi zabawkami, jakie przedstawiał prelegent. Jak dla mnie najlepsza prezentacja podczas całej konferencji, mimo że gościu jest z Oracle’a.

Następnie byłem na prezentacji Jarosława Pałki: The deconstruction of architecture in times of crisis. Prelegent mówił, że ostatnio za bardzo się skupiamy na frameworkach (technology masturbation), że one rozwiązują jedynie problemy ich twórców, zamiast bardziej skupić się na tym jak dostarczyć funkcjonalność naszemu klientowi. Była wyjaśniona zasada działania Tragedii wspólnego pastwiska i jak to może prowadzić do upadku projektu.

Z ważnych tematów, jakie wyniosłem z tej prezentacji, to gdy pewien zasób jest współdzielony przez wiele procesów, to procesy powinny mieć wyłączność do danego zasobu. Przykładowo procesy batch’owe, robiące coś na bazie danych, powinny mieć w czasie swojego działania współdzieloną bazę tylko na wyłączność. Rozwiązuje to wiele problemów. Ważne jest również monitorowanie metryk systemu. Ogółem prezentacja fajna sporo przykładów z życia, ale trochę mało konkretów.

Następnie byłem na prezentacji Rebecci Wirfs-Brock Why We Need Architects (and Architecture) on Agile Projects. Prelegentka mówiła, że małe, niekrytyczne projekty nie potrzebują wiele zajmowania się architekturą. Według niej, architekt powinien być odpowiedzialny za:

  • redukcję technicznego długu
  • integrację nowej wiedzy z kodem (czyli refactoring, redesign, sprzątanie kodu)
  • testy jednostkowe
  • standardy kodowania
  • zwięzłość (użycie API, logowanie, obsługa błędów)

Generalnie kiedy zespół jest większy niż 9 osób, to należało by się w jakiś sposób podzielić i skoordynować działania pomiędzy grupami. Przy dużych projektach, powinno się dodatkowo poświęcić iterację zerową na eksperymenty i prototypy, aby można było dobrać odpowiednią architekturę. Powinna również w projekcie znajdować się osobna tablica dla architektonicznych tematów.

Następnie udałem się na wykład Sławka Sobótki pt. Ewolucyjna Destylacja Architektury – myślenie wizualne na przykładzie Ports&Adapters. Dla tego prelegenta, architektura to segregacja kodu na kawałki. Bardzo ciekawe stwierdzenie, które przypadło mi do gustu. Sławek pytał uczestników, jak wyobrażamy sobie kod. W końcu piszemy go codziennie, więc jakąś postać w naszej pamięci powinien on mieć. Ja sam do dzisiaj nie mam jednoznacznej odpowiedzi na to pytanie. Z jednej strony widzę interakcję pomiędzy obiektami, a z drugiej myślę wymaganiu które aktualnie implementuję i formalizuję je do postaci zrozumiałej dla kompilatora. Kod wyobrażam sobie w tym przypadku jako transformację żądanej odpowiedzialności w tekst. Nie mam na pewno w głowie diagramów klas w stylu UMLa, gdyż raczej na co dzień uprawiam TDD. Będę musiał sobie w pracy przypomnieć tą kwestię, to może odpowiem sobie wtedy na to pytanie.

To o czym było mówione, można znaleźć na prezentacji: http://prezi.com/p0psif9qixgz/ports-adapters/ Z ważniejszych aspektów, to należy wymienić różne, możliwe architektury aplikacji:

  • Micro Kernel
  • Pipes & Filters
  • Layers
  • CQRS
  • oraz tytułowe Ports & Adapters.

Odnośnie ostatniej architektury była poświęcona prezentacja, wraz z wyjaśnieniami na przykładowym kodzie. Było również trochę o Sadze, oraz o możliwych problemach, jakie mogą wystąpić podczas stosowania Ports & Adapters.

Prezentacja była pełna żartów, ciekawa i trzymała wysoki poziom. Część rzeczy już widziałem na innych prezentacjach, ale prelegent wyjaśnij to na swoim blogu we wpisie: Materiały z konferencji Java Developers Day. Generalnie zbierając wiedzę rozsianą po różnych prezentacjach Sławka, kształtuje mi się coraz bardziej wyraźnie, rozwiązanie znane pod nazwą DDD, którym namiętnie od jakiegoś czasu zajmuje się prelegent. Pytanie tylko, czy ludzie będą chcieli zmienić swoje podejście i wyjść po za ramę modelu trójwarstwowego? I czy to podejście wejdzie do kanonu nauczania na studiach informatycznych?

Następnie udałem się na prezentację Pawła Badeńskiego The Catcher in the Code. Na tej prezentacji siadłem trochę za blisko i nie za bardzo widziałem slajdy (mównica zasłaniała). Wykład był bardzo ogólny, a jego przesłaniem było, że powinniśmy pisać czysty kod. Było o 4ch ważnych regułach:

  • pisaniu takiego kodu ze świadomością, jakby osoba która musi później z nim pracować, była seryjnym mordercą i wiedziała gdzie mieszkasz
  • nazwy metod i zmiennych należy tak samo dokładnie dobierać jak się wybiera imię dla swojego dziecka
  • write your code with your brain in mind
  • stop codding, start telling the stories (cieżko było mi to jakoś sensownie przetłumaczyć)

Ogółem nie lubię jak na konferencjach są takie luźne, niewiele uczące prezentacje. Temat czystego kodu przewija się już od kilku lat i myślę, że na konferencję przychodzą osoby, które już wiedzą o tym.

Następnie byłem na prezentacji Piotra Buckiego o XSS. Generalnie ataki Cross-site scripting dzieli się na persistent (czyli zapisany na serwerze) i non-persistent, czyli najczęściej zmodyfikowany URL. Jak się bronić przed tego typu atakami? Przede wszystkim filtrowanie danych wejściowych nazywane po angielsku Sanitization. Dzięki temu widzimy później w naszej bazie danych typowo HTMLowe krzaczki. Technika te jest ważna, ale zazwyczaj nie wystarczająca. Zalecanym rozwiązaniem jest konwersja HTML’a do DOM’a i stosowanie białej listy dozwolonych tagów.

Inną możliwością jest escape’owanie danych wyjściowych. Czyli jak komuś uda się zapisać złośliwy skrypt w bazie danych, to lepiej go już wyświetlić w postaci tekstowej na stronie zamiast pozwolić się wykonać. Należy jednak pamiętać, aby escape’ować do odpowiedniego kontekstu.

Było jeszcze przejście po frameworkach Javowych, z wyszczególnieniem jakich konstrukcji używać, a jakich nie i kilka ogólnych przykładów podatności. Ogółem wykład średni, jakoś nie porwał ani mnie, ani publiki (żadne pytanie nie padło). Czegoś mi ewidentnie w tej prelekcji brakowało. Może jakiś konkretny przykład, że XSS jest poważnym zagrożeniem by lepiej podziałał na wyobraźnię uczestników?

Następnie udałem się na wykład Hardy Ferentschik’ na temat Hibernate Search. Prelegent jest developerem tegoż mapera obiektowo relacyjnego. Hardy pokazał, jak można korzystać z Hibernate Search. Jest to projekt, który bazuje na Lucynce, zintegrowany oczywiście z Hibernatem. Wadą projektu Apache jest konieczność dobudowania indeksu, gdy nastąpiły jakieś zmiany w bazie. Hibernate Search wprowadził więc przyrostowe aktualizowanie indeksu. Czyli jak coś aktualizujemy / zapisujemy w bazie, to równocześnie jest aktualizowany indeks Lucene. Wszystko jest oczywiście konfigurowalne za pomocą adnotacji, czyli definiujemy, po których polach klasy będzie możliwe wyszukiwanie.

Chcąc wyszukać już cos konkretnego z naszego zbioru danych, można skorzystać z API dostarczonego przez Lucene, lub z Hibernate’owego Query DSL’a. Było jeszcze o wydajności projektu, projekcjach i paru innych możliwościach projektu.

Drugiego dnia konferencji udałem się na prelekcję Henri Kerola na temat Vaadina. Pamiętam, że na 33 Degree byłem zachwycony prezentacją tegoż frameworka. Tutaj jednak już po minucie, wiedziałem, że długo na wykładzie tego pana nie wysiedzę. Niestety ten pan się kompletnie nie nadaje do występów publicznych. Początkowo było trochę o frameworku, aż w pewnym momencie zaczęło się kodowanie na żywo. Henri pokazał, jak łatwo można spiąć bazę danych z tym co jest w warstwie widoku i dostają za darmo Lazy Loading, podczas przewijania listy użytkowników w dół. Jednak wklejenie SQL’a bezpośrednio w kodzie UI bardzo mi się nie spodobało. Jak już uczyć, to bez antypatternów. W porównaniu z tym co pokazał Joonas Lehtinen na 33 Degree było to smutne.

Następnie chciałem iść na BDD Rafała Jamróza, ale zagadałem się na korytarzu, a sala była wypełniona po brzegi, a nie chciało mi się stać. Udałem się więc na prelekcję Adama Biena pt. Java EE–Future Is Now, But It Is Not Evenly Distributed Yet. Adam pokazał projekt mavenowy, z którego usuwał zależności, które są zbędne przy korzystaniu z Javy EE 6. Tworząc projekt w korporacyjnej szóstce nie potrzeba nam już web.xml’a, ani innych biblotek, które standardowo wrzucamy do nowych projektów.

Prelegent wyśmiał kilka konwencji, które są namiętnie bez zrozumienia stosowane na co dzień w projektach. Przykładowo postfix w nazwach klasy Impl. Co się wtedy stanie, gdy przyjdzie nam napisać kolejną implementację tego samego interface’u? Nazwiemy ją Impl2, Impl3, itd. No i po co nam interface’y, skoro mamy jedną implementację? Bo pewnego dnia trzeba będzie napisać inną implementację... Później było o tym co można w Javie EE 6 robić. Co ciekawe, prelegent używał na prezentacji NetBeans’a. Było to wielkie zaskoczenie dla mnie, gdyż dawno nie widziałem tego środowiska w akcji.

Później byłem n a wykładzie Patrycji Węgrzynowicz na temat bezpieczeństwa open source’owych bibliotek Javowych, których używamy na co dzień. Na początku było omówione, jak można oceniać podatności w aplikacjach, jak są przyznawane za to punkty i jak można scharakteryzować owe luki w oprogramowaniu. Później było sporo wykresów, bazujących na NVD. Patrycja początkowo skupiła się na serwerach aplikacji i co ciekawe to najbardziej dziurawe są te komercyjne. Było również trochę o framework’ach tj. Struts 2, Seam, GWT i szkoda że tylko o tych. Brakowało mi czegoś w tym wykładzie, jakoś mnie nie zaciekawił zbytnio.

Na koniec byłem jeszcze na wykładzie Martina Gunnarssona i Pär Sikö o JavieFX. Trochę dziwnie się patrzy na wykład prowadzony przez 2 osoby. Bo gdy jeden mówi to drugi nie zawsze wie co ze sobą zrobić. Panowie mówili fajnym angielskim, kompletnie bez akcentu, jednak jeden mówił zdecydowanie za cicho. Prelegenci pokazywali ciekawe dema i stworzyli pewną kabaretową atmosferę. Mam na myśli śmieszne dialogi, jakie pomiędzy sobą prowadzili. Niestety na zawartości merytorycznej już się nie skupiłem.

Po obiedzie pokręciłem się jeszcze trochę po konferencji, porozmawiałem z ludźmi i udałem się w stronę Wrocławia.

Z konferencji nie jestem zadowolony, nie za wiele z niej wyniosłem pod względem merytorycznym. Od strony organizacyjnej nie było się do czego przyczepić: dobre obiady (choć był problem z miejscem do jedzenia), impreza, miejsce konferencji... Tylko coś organizatorzy nie trafiają z doborem wykładów. Jak dla mnie JDD ma szansę być dobrą konferencją, ale chyba zawsze będzie tylko miała tą szansę.

poniedziałek, 5 listopada 2012

Listy w Scali

Ostatnimi czasy biorę intensywnie udział w kursie programowania funkcyjnego w Scali na portalu Coursera.org. Generalnie kiedyś się zraziłem do tego języka, gdyż jakiś tutorial i aktualnie używana przeze mnie wersja środowiska, powodowała, że przekopiowane przykłady nie działały. Teraz jest już trochę lepiej, aczkolwiek IDE do Scali dalej kuleją.

Na sam kurs zapisałem się zachęcony przez kolegę z pracy – fana Scali. Pomyślałem, że skoro nie będę robił tego sam, a będzie wsparcie w razie czego, to warto się zaangażować. I rzeczywiście jeszcze kilka znajomych robi ten kurs. Ostatnio nawet organizuję cykliczne spotkania w pracy, podczas których prezentujemy i porównujemy nasz kod. Oczywiście omawiamy stare zadania, które są już po deadline, aby nie łamać Honor Code. Jeśli macie taką możliwość, to zachęcam do organizowania podobnych aktywności u siebie.

Czy uczestnictwo w takim kursie coś daje? Nawet jeśli Scala nie trafi do powszechnego użytku, to na pewno uczestnictwo w kursie pomaga w zrozumieniu paradygmatu funkcyjnego i wynikającego z niego konsekwencji. Horyzonty się poszerzają, co ostatnio pomogło mi w lepszym zrozumieniu Java Scripta na darmowym kursie we Wrocławiu. Więcej o tym szkoleniu w innym wpisie.

Dla mnie wielką satysfakcją jest moment, kiedy dostaję olśnienia i nagle całkiem odmiennie patrzę na coś, co używam na co dzień. Podczas tego kursu był już taki moment, podczas rozwiązywania zadań z drugiego tygodnia. Należało wówczas zaimplementować zbiór (Set). Zadanie niby proste, ale nie gdy w kodzie mamy coś takiego:
type Set = Int => Boolean 
Długo nie mogłem zrozumieć, o co chodzi. Czytałem wątki na forum kursu, szukałem w Necie... W końcu zrozumiałem, że w tym przypadku Set został zdefiniowany, jako funkcja przyjmująca wartość liczbową, a zwracająca informację, czy dana liczba należy do zbioru, czy nie. Całkiem odmienne spojrzenie, na coś co zawsze było pudełkiem przechowującym elementy. Jak widać, można inaczej. Co prawda sprawia to parę problemów (możliwy zakres elementów), ale wyjście po za tą ramę było dla mnie niesamowite.

Dobra, koniec tych przemyśleń, czas na konkrety. Listy towarzyszą uczestnikom kursu już od samego początku. Zostały one jednak dokładniej przedstawione, dopiero podczas piątego tygodnia.

Listy w Scali są niezmienne. Co prawda istnieje alternatywna implementacja: MutableList, ale chodzi o to aby używać niezmienników. Wiadomo dlaczego. Nie oznacza to jednak, że jak wrzucimy element do listy (a ściślej mówiąc stworzymy listę zawierającą dany element), to nie możemy zmienić stanu tegoż obiektu. Owszem możemy. W Javie było by to tak:
class Person {
    private final String name;
    public int variable = 0;

    public Person(String name, int variable) {
        this.name = name;
        this.variable = variable;
    }

    public void change() {
        variable = variable + 42;
    }
}

public class Main {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Person("Jan", 0));
        List immutableList = Collections.unmodifiableList(list);
        System.out.println(immutableList.get(0).variable);
        immutableList.get(0).change();
        System.out.println(immutableList.get(0).variable);
    }
} 

W Scali można analogicznie:
class Person(name: String, var variable: Int) {
  def change() {
    variable = variable + 42
  }
}

object Main {

  def main(args: Array[String]) {

    val person = new Person("name", 0)
    val list = List(person)
    println(list(0).variable)
    list(0).change()
    println(list(0).variable)
  }
} 

Tworzenie list w Skali mocno różni się od list w Javie. Z racji ich nie zmienności, musimy ich zwartość zdefiniować w momencie tworzenia.
val person = new Person("name", 0)
val list = List(person) 

Warto przy tej okazji zobaczyć, do czego będzie w tym momencie ewaluowane wywołanie List(person). Otóż List w Scali to:
  • abstakcyjna klasa List
  • objekt List
Klasa i objekt jednocześnie? Prawie. W Scali słówko kluczowe object oznacza definicję singletona. Po prostu ten wzorzec został wbudowany w język. Z praktycznego punktu widzenia, oznacza to, że możemy mieć tylko jedną instancję tegoż obiektu. Nie trzeba więc używać słówka new w tym przypadku.

Z drugiej strony, możemy przecież wywołać naszego singletona wielokrotnie z różnymi argumentami.
val list = List(person)
val list2 = List(person)
Jakby podejrzeć adresy w pamięci, to się okaże, że nie wskazują one tego samego miejsca. Co się więc dzieje? Jeśli chcemy skorzystać z dobrodziejstwa niepisania new, należy w naszym object’cie zdefiniować metodę apply() zwracającą żądaną instancję. Oczywiście metoda ta może przyjmować argumenty i zwraca żądaną instancję. Pełni wiec to bardziej rolę Fabryki niż singletonu. Jak to jest zdefiniowane dla listy? a no tak:
override def apply[A](xs: A*): List[A] = xs.toList 
Czyli na naszych argumentach (typu WrappedArray) zostanie wywołana metoda toList() i zadnie będzie gdzieś tam daleko delegowane...

No dobra, a jakiego typu listę otrzymamy? Patrząc na List.scala to można zobaczyć, że są dwie klasy rozszerzające tą Listę:
  • case object Nil extends List[Nothing]
  • final case class ::[B](...) extends List[B]

Pierwsza konkretna implementacja to element końcowy listy, za którym nic już nie ma. Natomiast dwukropek dwukropek, to nazwa klasy! Trochę się zszokowałem, gdy to zobaczyłem – rozumiem nazwać tak metodę, ale klasę? Jak widać można. Klasa ta tak naprawdę skleja głowę z ogonem. Implementacja jest bardzo prosta:
final case class ::[B](private var hd: B, private[scala] var tl: List[B]) extends List[B] {
  override def head : B = hd
  override def tail : List[B] = tl
  override def isEmpty: Boolean = false 

A samego operatora używamy zazwyczaj w bardziej czytelnej formie
val person = new Person("name", 0)
val person2 = new Person("name2", 0)
val list = List(person2)
val list2 = person :: list 

A jak za pomocą :: utworzyć jednoelementową listę? Należy skorzystać ze wspomnianego już Nil’a:
val list3 = person :: Nil 
Analogicznie jak :: działa +: Ten drugi operator (a właściwie metoda) dodatkowo tworzy kopię bazowej listy. Możemy również dodawać elementy na końcu listy, ale raczej nie jest to optymalne. W tym celu używamy :+. Przykład:
scala> val text = List("Ala", "ma")
text: List[java.lang.String] = List(Ala, ma)

scala> val text2 = text :+ "kota"
text2: List[java.lang.String] = List(Ala, ma, kota) 

Można również sklejać listy. Do tego używamy potrójnego dwukropka ::: Przykład:
scala> val numbers = List(4, 6, -3, 1)
numbers: List[Int] = List(4, 6, -3, 1)

scala> val numbers2 = List(2, 3)
numbers2: List[Int] = List(2, 3)

scala> val numbers3 = numbers2 ::: numbers
numbers3: List[Int] = List(2, 3, 4, 6, -3, 1) 

Operator ten dokleja nowe elementy na początku istniejącej listy. Podobnie działa dwuplus ++, który dodatkowo tworzy kopię Listy:
scala> val numbers4 = numbers2 ++ numbers
numbers4: List[Int] = List(2, 3, 4, 6, -3, 1) 

Nie wiem jak by to można było na przykładzie przedstawić (a debugować mi się nie chce), po prostu musimy ufać dokumentacji.

Uff przebrnęliśmy szczęśliwie przez te ‘śmieszne’ operatory. Muszę przyznać, że dla początkującego adepta Scali, są te operatory trochę niejasne i mylące, dlatego warto poświęcić im chwilę. Istnieje jeszcze kilka metod produkujących listy:
xs.reverse - zwraca odwróconą listę
xs updated (n, x) - zwraca listę zawierającą to samo co xs, ale w miejscu o indeksie n wstawia x’a
xs.toList – metoda dostępna w wielu innych typach

Co jeszcze ciekawego mamy w listach w Scali?

Mamy 3 fundamentalne operacje:
head – zwraca pierwszy element
tail – zwraca wszystko poza pierwszym elementem
isEmpty – zwraca true dla :: i false dla Nil

Ponadto lista zawiera sporo innych już mniej wydajnych operacji (last, init, take…), jak i takie które znamy dobrze z Javy (contains, indexOf…).

Scala udostępnia nam trochę więcej operacji na listach, niż mamy to w czystej Javie. I tak na przykład przekształcenie listy w listę zawierającą inne elementy. W Javie zazwyczaj piszemy petlę:
List<String> names = new ArrayList<String>();
for (Person person : persons) {
    names.add(person.getName());
} 

W Scali można to wykonać prościej za pomocą map():
val names = persons map (p => p.getName) 

Czyli dla każdego elementu p z listy persons wywołaj getName i zwróć to wszystko opakowane w listę i zapisz w names. Czy jest to czytelniejsze? Jak się pamięta co robi metoda map() i jeśli jest się w stanie sobie wyobrazić co oznacza p, to tak. O ile jak czytam własny kod to jestem w stanie przeanalizować co się dzieje, ale jak bym miał to zrobić z czyimś kodem, to bym się pewnie musiał dłużej zastanawiać, o co kaman.

Tą, jakże krótką i zwięzłą definicję, można jeszcze zapisać na kilka sposobów:
val names = persons map (p => p getName)
val names = persons map (_.getName)
val names = persons map (_ getName) 

Następną przydatną operacją jest filtrowanie, czyli pobranie podzbioru listy. W Javie to by było jakoś tak:
List<Person> personsStartsWithJ = new ArrayList<Person>();
for (Person person : persons) {
    if(person.getName().startsWith("J")) {
        personsStartsWithJ.add(person);
    }
} 

W Scali można to zapisać na jeden ze sposobów:
val personsStartsWithJ = persons filter(p => p.getName.startsWith("J"))
val personsStartsWithJ = persons filter(_.getName.startsWith("J")) 

Dodatkowo mamy metodę filterNot(), która zwraca elementy niespełniające podanego warunku. Można również wykonać te dwie operacje (filter i filterNot) jednocześnie:
val personsPart = persons partition(_.getName.startsWith("J"))
println(personsPart._1)
println(personsPart._2) 

Przykładowy efekt działania:
List(Person(Jan))
List(Person(Stefan), Person(Emil), Person(Dominik)) 

Czyli bardzo sensowne użycie tuple’a, pomagające zwrócić dwa wyniki z metody. Nie kojarzę żadnych standarowych metod z Javy zwracających dwa wyniki. Tak wiem że SRP, ale jak akurat potrzebujemy obydwie listy, to dwukrotna iteracja może rzutować na performance. W Javie trzeba to na piechotę robić:
List<Person> personsStartsWithJ = new ArrayList<Person>();
List<Person> personsNotStartsWithJ = new ArrayList<Person>();
for (Person person : persons) {
    if(person.getName().startsWith("J")) {
        personsStartsWithJ.add(person);
    } else {
        personsNotStartsWithJ.add(person);
    }
} 

To na razie chyba tyle co chciałem przedstawić, zapisać, zapamiętać i podzielić się tym z Wami na temat Scali. Czas wrócić do oglądania kolejnych wykładów...

wtorek, 16 października 2012

Logowanie interakcji w Mockito

Ostatnio przyszedł do mnie mój architekt i zaczął rozmowę coś w stylu:
Marcin, ty nasz ekspercie od testowania jednostkowego, integracyjnego i Mockito…
Ucieszony miłymi słowami pochwały i podbudowany faktem, że stanowię ważną część zespołu słuchałem dalej, co ma mi do powiedzenia kolega z zespołu. Wtedy jeszcze nie wiedziałem, że to pierwsze zdanie to tak naprawdę prosta, skuteczna, magiczna sztuczka, dzięki której bardzo łatwo jest później przekonać odbiorcę tego komunikatu, do wykonania pewnego zdania. To już trochę zahacza o manipulację / wywoływanie (ang. elicitation) / urabianie / programowanie neurolingwistyczne, czy też inne tego typu techniki, a więc wykracza po za tematykę tego bloga.

Wracając do rozmowy:
No bo właśnie mamy taki problem, że zrobiliśmy refaktoring niedawno stworzonej klasy i generalnie wszystko działa, ale trzeba jeszcze testy dopasować. Ten i tamten już poszli do domu, więc pytam czy byś tego nie zrobił. Uważam, że Tobie, jako że jesteś najbardziej w testach zorientowany, pójdzie szybko i gładko...
No dobra, myślę sobie. Dzisiaj kończy się czas przeznaczony na kodowanie w tym Sprincie, trzeba więc cos zdeployować, aby testerzy mieli jutro co robić. No dobra, obowiązki wzywają.

Architekt pokazał mi i opisał jakie to zmiany zostały wykonane. Generalnie był to pewien proces, który kodowaliśmy przez cały sprint. Miał on wiele danych wejściowych i generował sporo na wyjściu. No i wcześniej była sobie taka jedna metoda, która to miała 6 argumentów, kilka mocków i produkowała coś pożytecznego. Zostało to opakowane w fabrykę, która podawała teraz cześć danych od dupy strony (wywołanie innych serwisów), jak i również sama metoda została przeorana. Czyli liczba argumentów zmalała, doszło kilka zależności, nowy sposób instancjowania, no i trzeba był do tego przerobić testy.

Cofnięcie zmian nie wchodziło w rachubę, gdyż była to sprawka w sumie 3ch osób, a wykorzystywany system kontroli wersji (dziękujemy IBM Synergy) zrobił by więcej złego niż dobrego. Olanie testów jedynie przesuwało problem w czasie, a na syndrom wybitego okna nie mogłem sobie pozwolić.

Zacząłem więc to po kolei przerabiać testy, ich czytelność malała, a definicje zachowania mocków stawały się niezrozumiałe. Najgorsze że pierwszy test, który przerabiałem, ciągle nie działał. Nie wiedziałem w końcu czy to wina domyślnych wartości zwracanych przez Mocki z Mockito, czy też trefnego refactoringu (a raczej redesignu).

No i w tym momencie postanowiłem sprawdzić interakcje z moimi mockami, czy czasem gdzieś przypadkiem Mockito nie zwraca np. pustej listy, która to powoduje brak wyników. Chwila szperania w dokumentacji i znalazłem taki ciekawy twór. Przykładowy kod poniżej.
@Test
public void shouldCreateAccount() {
    // given
    InvocationListener logger = new VerboseMockInvocationLogger();
    AccountService accountService = new AccountService();
    StrengthPasswordValidator validator = mock(
            StrengthPasswordValidator.class,
            withSettings().invocationListeners(logger));
    UserDao userDao = mock(UserDao.class,
            withSettings().invocationListeners(logger));
    UserFactory userFactory = mock(UserFactory.class,
            withSettings().invocationListeners(logger));

    when(userFactory.createUser(LOGIN, PASSWORD))
            .thenReturn(new User(LOGIN, PASSWORD));

    accountService.setPasswordValidator(validator);
    accountService.setUserDao(userDao);
    accountService.setUserFactory(userFactory);

    // when
    User user = accountService.createAccount(LOGIN, PASSWORD);

    // then
    assertEquals(LOGIN, user.getLogin());
    assertEquals(PASSWORD, user.getPassword());
} 
Testujemy zakładanie konta użytkownika. Życiowy przykład, wiele nie trzeba tłumaczyć.

Na początek w linii 4tej tworzymy instancję obiektu typu VerboseMockInvocationLogger. Jest to standardowa implementacja interfejsu InvocationListener, która wypisuje na standardowe wyjście zaistniałe interakcje z mockami. Aby przekazać ten obiekt do naszych mocków, musimy podczas ich tworzenia, ustawić ten obiekt. Jak? Widzimy to w linii 8smej. Powtarzamy to dla każdego mocka, który nas interesuje.

Dalej standardowo. Za pomocą when() konfigurujemy Mock’a, injectujemy zaleznosci (linie 17-19) wywołanie metody testowej i assercje. Dzięki takiemu Testu możemy otrzymać interesujący output:

########### Logging method invocation #1 on mock/spy ########
userFactory.createUser("login", "password");
   invoked: -> at com.blogspot.mstachniuk.unittestpatterns.service.mockinvocations.AccountServiceTest.shouldCreateAccount(AccountServiceTest.java:31)
   has returned: "null"

############ Logging method invocation #2 on mock/spy ########
strengthPasswordValidator.validate(
    "password"
);
   invoked: -> at com.blogspot.mstachniuk.unittestpatterns.service.AccountService.createAccount(AccountService.java:19)
   has returned: "0" (java.lang.Integer)

############ Logging method invocation #3 on mock/spy ########
userDao.findUserByLogin("login");
   invoked: -> at com.blogspot.mstachniuk.unittestpatterns.service.AccountService.existAccount(AccountService.java:43)
   has returned: "null"

############ Logging method invocation #4 on mock/spy ########
   stubbed: -> at com.blogspot.mstachniuk.unittestpatterns.service.mockinvocations.AccountServiceTest.shouldCreateAccount(AccountServiceTest.java:31)
userFactory.createUser("login", "password"); 
   invoked: -> at com.blogspot.mstachniuk.unittestpatterns.service.AccountService.createAccount(AccountService.java:23)
   has returned: "User{login='login', password='password'}" (com.blogspot.mstachniuk.unittestpatterns.domain.User)
Pierwsze wywołanie, to te w teście:
when(userFactory.createUser(LOGIN, PASSWORD))
        .thenReturn(new User(LOGIN, PASSWORD));
Niestety (a może stety) nie można ukryć tego wywołania w standardowym outpucie, chyba że się napisze własna implementację InvocationListener’a.

W wywołaniu #2 widzimy, że wywołano metodę validate() z parametrem "password" i zwrócono zero. Co prawda metoda ta zwraca void, ale to pierdoła i nie należy się nią przejmować. Co fajniejsze, to po kliknięciu na AccountService.java:19 zostaniemy przeniesieni w miejsce wywołania tej metody. Bardzo użyteczny feature.

Trzecia interakcja jest analogiczna, ale za to czwarta jest ciekawa. Widzimy z jakimi parametrami wywołano mockowaną metodę, z którego miejsca w kodzie i gdzie zostało to zachowanie zdefiniowane! Od razu można przejrzeć konfigurację testu i się zorientować co skąd się bierze. Jak komuś za mało / za dużo, to zawsze można samemu sobie zaimplementować InvocationListener'a. Dzięki temu można trochę lepiej poznać bebechy tej biblioteki.

To tyle odnośnie tej logowania interakcji w Mockito. Obyście musieli jej używać jak najrzadziej, gdyż jak dla mnie świadczy to o skomplikowaniu i zagubieniu we własnych kodzie. Warto jednak o czymś takim zawczasu wiedzieć, może czasem uratować tyłek. Mi dzięki temu udało się dojść co było nie tak.

Cały kod wykorzystywany w tym wpisie można ściągnąć z githuba, a prezentowany kod testowy znajduje się w klasie: AccountServiceTestWithMockInvocations.

Morał tej historii:
Zwracaj uwagę na to co i jak mówią inni i nie daj się zmanipulować! Zwłaszcza w pracy :)