wtorek, 15 marca 2011

Testowanie wywołania JMSTemplate

Jakiś czas temu zostałem zapytany w jaki sposób przetestować jednostkowo coś takiego:

public class SenderService {
private JmsOperations jmsTemplate;

public void doSend(final MyMessage myMessage){
jmsTemplate.send(new MessageCreator() {

public Message createMessage(Session session) throws JMSException {
return session.createObjectMessage(myMessage);
}
});
}
}

Pytanie jest dość podchwytliwe, gdyż uważam, że testy jednostkowe bardzo dobrze się sprawdzają do testowania logiki systemu. Kod powyżej nie ma jakiejś extra rozbudowanej logiki, ale jest ona dość dobrze zakopana. Poza tym wychodząc od TDD (jak przykazane) jak mogło dojść, że mamy kod a teraz zastanawiamy się jak go przetestować ?
Tworzenie testów na tym etapie wydaję się sensowne jedynie w celach regresji.

Z drugiej strony można się upierać, czy jest sens aby ten kod testować jednostkowo, czy nie testować go jedynie integracyjnie. Takie podejście wydaje się zgodne z tym co można wyczytać w http://www.growing-object-oriented-software.com/ a powyższy kod leżałby w warstwie adaptera zgodnie z Ports and Adapters pattern. Jednakże, z pragmatycznego punktu widzenia (albo wtedy gdy w ogóle nie mamy automatycznych testów integracyjnych/systemowych) przetestowanie jednostkowo powyższego kodu jest jak najbardziej możliwe.

Najpierw zastanowiłbym się co chcemy przetestować, bo w metodzie send dzieją się 2 rzeczy: wywołanie JMSTemplate.send oraz pośrednio budowa JMS Message. Najsensowniejsze wydaje się rozdzielenie obu funkcjonalności i przetestowanie ich osobno. Zacznijmy od testowania budowania JMS Message za pomocą jednego z dwóch podejść:
  1. tworzymy top level class implementującą MessageCreator

    public class MyMessageCreator implements MessageCreator {
    private final MyMessage message;

    public MyMessageCreator(MyMessage message) {
    this.message = message;
    }
    @Override
    public Message createMessage(Session session) throws JMSException {
    return session.createObjectMessage(message);
    }
    }


  2. tworzymy factory

    MessageCreator getMessageCreator(final MyMessage message) {
    return new MessageCreator() {
    @Override
    public Message createMessage(Session session) throws JMSException {
    return session.createObjectMessage(message);
    }
    };
    }
    }


Dzięki temu możemy w stosunkowo łatwy sposób przetestować, że nasz kod poprawnie tworzyJMS message (na potrzebę testów użyjemy mocków/stubów)

Następnie, możemy sprawdzić czy nasza metoda send faktycznie użyje jmsTemplate.send z odpowiednie parametrem. Taki test będzie zależał od tego w jaki sposób zaimplementowaliśmy tworzenie JMS Message. Jeśli stworzyliśmy własną klasę MessageCreator to możemy sprawdzić czy parametr metody send jest właśnie jej typu. Ewentualnie można użyć (gruba rura!) PowerMock. W drugim przypadku będziemy mogli zamockować factory. Jeśli rolę fabryki pełni osobna klasa (potencjalnie ukryta pod interfejsem) to nie wydaję się to najgorsze (nie licząc tego że powstały nam kolejne artefakty), a w przypadku gdy fabryką będzie lokalna metoda to już wchodzimy w tematy partial-mocking.

Gdybyśmy jednak chcieli podany kod przetestować bez jego uprzedniej refaktoryzacji, czyli bez wprowadzania podziału na: budowania JMS message i wywołanie jmsTemplate, to też jest to jak najbardziej możliwe. Sprawa wydaje się stosunkowo prosta: należy sprawdzić czy metoda jmsTemplate.send jest wywołana z odpowiednim parametrem. Odpowiedni parametr to taki, który jak wywołamy na nim metodę send
to na przekazanej JMS session zostaną wywołane odpowiednie metody. Poniżej przykłady jak to zostało zrobione za pomocą EasyMock i Mockito

public class SenderServiceEasyMockTest {
private SenderService sut = new SenderService();
private JmsOperations jmsOperations;
private MyMessage myMessage = new MyMessage("neverMind");

@Before
public void setUp() {
jmsOperations = EasyMock.createMock(JmsOperations.class);
sut.setJmsTemplate(jmsOperations);
}

@Test
public void testDoSendByArgumentMatcher() {
final Session session = EasyMock.createMock(Session.class);
IArgumentMatcher argumentMatcher = new IArgumentMatcher() {

public boolean matches(Object argument) {

MessageCreator messageCreator = (MessageCreator) argument;
try {
EasyMock.expect(session.createObjectMessage(myMessage))
.andReturn(null);
EasyMock.replay(session);
messageCreator.createMessage(session);
} catch (JMSException e) {
throw new RuntimeException(e);
}
return true;
}

public void appendTo(StringBuffer buffer) {
}
};

EasyMock.reportMatcher(argumentMatcher);
jmsOperations.send((MessageCreator) null);
EasyMock.replay(jmsOperations);

sut.doSend(myMessage);

EasyMock.verify(jmsOperations, session);
}

@Test
public void testDoSendByCapture() throws JMSException {
Capture<Messagecreator> tocapture = new Capture();
jmsOperations.send((MessageCreator) EasyMock.and(EasyMock.anyObject(),
EasyMock.capture(tocapture)));
EasyMock.replay(jmsOperations);

sut.doSend(myMessage);

MessageCreator value = tocapture.getValue();
Session session = EasyMock.createMock(Session.class);
EasyMock.expect(session.createObjectMessage(myMessage)).andReturn(null);
EasyMock.replay(session);
value.createMessage(session);
EasyMock.verify(jmsOperations, session);
}
}



public class SenderServiceMockitoTest {
private SenderService sut = new SenderService();
private JmsOperations jmsOperations;
private MyMessage myMessage = new MyMessage("SSSS");

@Before
public void setUp() {
jmsOperations = Mockito.mock(JmsOperations.class);
sut.setJmsTemplate(jmsOperations);
}

@Test
public void testDoSendByMatcher() {
sut.doSend(myMessage);

Mockito.verify(jmsOperations).send(Mockito.argThat(new ArgumentMatcher<Messagecreator>() {

@Override
public boolean matches(Object argument) {
MessageCreator messageCreator = (MessageCreator) argument;
Session session = Mockito.mock(Session.class);
try {
messageCreator.createMessage(session);

Mockito.verify(session).createObjectMessage(myMessage);
} catch (JMSException e) {
throw new RuntimeException(e);
}
return true;
}
}));
}


@Test
public void testDoSendByCaptor() throws JMSException {
sut.doSend(myMessage);

ArgumentCaptor<Messagecreator> messaArgumentCaptor = ArgumentCaptor.forClass(MessageCreator.class);
Mockito.verify(jmsOperations).send(messaArgumentCaptor.capture());
Session session = Mockito.mock(Session.class);
MessageCreator value = messaArgumentCaptor.getValue();
value.createMessage(session);
Mockito.verify(session).createObjectMessage(myMessage);
}
}

Pomimo braku jakiegokolwiek refaktoringu testy w EasyMock wyglądają o wiele mniej czytelniej od tych napisanych w Mockito. Wynika to głównie z potrzeby wołania w odpowiednich miejscach EasyMock.replay oraz (co gorsze) braku mechanizmów do weryfikacji zachowań, które zaszły w ramach wywołania metody podległej testowi. W przypadku EasyMock jest potrzeba wyspecyfikowania a priori wszystkich interakcji, a w Mockito możemy wygodnie użyć Mockito.verify. Szczególnie w tym przypadku bardzo wpływa to na czytelność kodu, gdy sekcje given/when/then sa zaburzone. Dodatkowo użycie mechanizmu capture w obu przypadkach wydaję się być zdecydowanie wygodniejsze od użycia argumentMatcher

czwartek, 10 marca 2011

AspectJ a 2.2250738585072012e-308

Ostatnio Dawid Weiss podczas spotkania w ramach Poznan JUG pokazał w jaki sposób obejść problem związany z Double.parseDouble(). Do czasu wydania (a co ważniejsze) zainstalowania patcha można się posiłkować wykorzystaniem AspectJ - nie pokryję to jednak wszystkich możliwych przypadków w których ten bug może się objawić. Aspekt oryginalnie przedstawiony przez Dawida na prezentacji był - jak słusznie myślałem - niekompletny, bo m.in. nie pokrywał przypadku kiedy Double.parseDouble byłoby wołane po refleksji.

package jug.demos.aspectj.aspects;
public aspect ParseDoubleHotFix{
double around(String s):
!within(jug.demos.aspectj.aspects..*) &&
call(double java.lang.Double.parseDouble(String)) &&
args(s){
if (s.indexOf("2250738585072012") >= 0) {
throw new IllegalArgumentException(
"We apologize for inconvenience, but this number is"
" temporarily not parseable by Oracle: " + s);
} else {
return Double.parseDouble(s);
}
}
}


Pełny kod aspektu został przysłany dzień po prezentacji na listę Poznan JUG. Pierwsze co rzuciło się w oczy to fakt, że zdefiniowane pointcuty są typu call zamiast execute. Czyli zamiast "opakować" wykonanie Double.parseDouble za pomocą execute, "opakowujemy" wywołania. Jak wyjaśnił Dawid jest to związane z tym "że java.lang.Double jest klasą systemową i ładuje się bardzo wcześnie. Load time weaver w AspectJ nie byłby w stanie jej pewnie stosownie owinąć, nawet gdyby go zmusić. Domyślnie AspectJ nie przetwarza klas systemowych w ogóle (java.* i javax.*). To, co można ew. zrobić, to przetworzyć rt.jar w trybie offline (z wymuszeniem przetwarzania pakietów systemowych), ale nie próbowałem."

Próba obejścia tego błędu poprzez sprawdzenie wyłącznie własnego kodu jest zdecydowanie niewystarczające, ponieważ feralna liczba może być parsowana przez kod, który wykorzystujemy, a do którego nie mamy źródeł. Najgorzej jednak wygląda sprawa w przypadku gdy wywołanie byłoby z poziomu biblioteki standardowej java - wtedy rozwiązania z wykorzystaniem AspectJ nie za wiele by się zdało (chyba że offlinowe przetworzenie rt.jar o czym wspomniał Dawid). Jednak gdy odrzucimy ten skrajny przypadek to i tak zakres kodu który jest uruchamiany w ramach naszej aplikacji, a który bezpośrednio nie kontrolujemy daje duże pole do popisu wszelkiej maści vulnerabilities. Jako przykład posłużył Dawidowi Tomcat 6.0.24.
Wydaję mi się, że eksperyment najłatwiej przeprowadzić przy wykorzystaniu AJDT wraz z podpiętym pod Eclipse Tomcatem (najłatwiej zrobić to w STS):
  • tworzymy sobie projekt typu AspectJ Project.
  • tworzymy w nowo otwartym projekcie nowy Aspect,
  • uaktywniamy go poprzez utworzenie pliku z odpowiednimi wpisami w META-INF/aop-ajc.xml w projekcie utworzonym powyżej (dodatkowo dodamy sobie <weaver options=" -XnoInline -verbose -showWeaveInfo"/>)
  • z poziomu IDE przechodzimy do konfiguracji Tomcata i otwieramy ustawienia związane z uruchomieniem serwera
  • w zakładce Arguments do VM Arguments dodajemy -javaagent:<ścieżka do aspectjweaver-1.6.10.jar>
  • w zakładce Classpath do User Entries dodajemy utworzony powyżej projekt typu AspectJ
  • uruchamiamy Tomcata
W logach zobaczymy m.in. wpis w stylu:

weaveinfo Join point 'method-call(double java.lang.Double.parseDouble(java.lang.String))' in Type 'org.apache.catalina.connector.Request' (Request.java:2591) advised by around advice from

Oznacza to że w linii 2591 klasy org.apache.catalina.connector.Request znajduję się wywołanie Double.parseDouble (czyli w tej linii znajduje się jointpoint spełniający określony pointcut ). Przeglądając źródła klasy org.apache.catalina.connector.Request można zobaczyć, że wywołanie Double.parseDouble odbywa się przy w metodzie getLocale, a dokładniej przy parsowaniu wartości quality factor. Oznacza to, że w przypadku gdy na serwerze zostanie wykonana metoda request.getLocale w odpowiedzi na żądanie HTTP o nagłówku Accept-Language i jego dowolną wartością, ale z q=2-2250738585072012e-308, obsługujący wątek "trafi" w busy-loop.

Dawid w swojej prezentacji skupił się głównie na load-time weaving - w odróżnieniu od compile/binary time weaving takie podejście nie wymaga ingerencji w proces budowania kodu. Ma to znaczną zaletę, którą wykorzystałem podczas pracy u klienta, kiedy okazało się, że aplikacja działa zdecydowanie wolniej od tego co od niej oczekiwano. Zamiast próbować podłączać ją pod profiler czy też optymalizować na ślepo, chciałem zmierzyć czas obsługi żadania z podziałem na warstwy/komponenty. Dodanie takiej logiki w kodzie byłoby nie tylko bardzo upierdliwe, ale także wymagałoby przebudowania i skompilowania aplikacji (klient wersjonował otrzymywane binaria). Zamiast tego napisałem aspekt, zapakowałem go w jara i jedyne co pozostało to zmuszenie administratora aby zmodyfikował parametry uruchamiania JVM (dodajemy javagent oraz jar z aspektem do classpath). Nie potrzeba było w takim przypadku w żaden sposób przetwarzać dostarczonej poprzednio aplikacji.
Modyfikacja parametrów startowych JVM często trafia na opór ze strony administratorów, ale w tym przypadku szczęśliwie się udało.

Z punktu widzenie developmentu/deploymentu LTW jest strasznie wygodne - wystarczy stworzyć nowy AspectJ project, napisać w nim aspekty, dodać ten projekt do classpath (w IDE bajecznie łatwe) projektu, dla którego chcielibyśmy aby zadziałały aspekty, zmodyfikować parametry startowe projektu (javaagent ) i uruchomić....

W SpringFramework AspectJ jest w pełni wykorzystywany do implementacji @Configurable oraz do zarządzania transakcjami w trybie aspect. Uruchamianie testów za pomocą Spring TestContextFramework, które tworzą context korzystający z Load Time Weaving dość znacząco zwiększa czas uruchomienia testów. Wynika to (zgodnie z tym co powiedział Dawid) z tego że sam weaver jest napisany w javie (co implikuje problemy z opakowywaniem klas języka/biblioteki standardowej). Dodatkowo w kontekście wydajności:
  • unikanie dynamic pointcut (typu cflow, cflowbelow)
  • unikanie generycznych typów parametrów/wartości zwracanych
  • tworzenie pointcut minimalizujących ilość jointpointów
  • sprawdzanie weaving logs

Na końcu Dawid pokazał 2 dodatkowe zastosowania użycia AspectJ
  • mierzenie wydajności/śledzenie ścieżek wywołania
  • szukanie błędów związanych z concurrency
To drugie wygląda bardzo ciekawie, szczególnie, że błędy związane z concurrency mogą się pojawiać losowo i są bardzo ciężkie do wykrycia za pomocą standardowych technik: unit testy/debugging. Dawid zaproponował aby zdefiniować wymagania co do współbieżności wybranej klasy za pomocą aspektu - przy wejściu do metody podnosimy flagę (target + wątek), a przy wyjściu opuszczamy flagę (target + wątek) . Dzięki temu możemy sprawdzić czy przy wejściu do metody flaga jest opuszczona. Dawid zaproponował aby zrealizować to w taki sposób aby zdeployować aspekt wraz z aplikacją na serwerze uruchomionym w trybie debug. Dalej, podpiąć się do serwera z IDE, ustawić breakpoint wraz z Suspend Policy ustawioną na Suspend VM w linii, która nie powinna zostać wywołana jeśli nasz kod który poddajemy sprawdzeniu jest poprawny. Maciej Biłas w mailu następnego dnia zaproponował aby zamiast bawienie się w tryb debug i breakpointy programowowo wykonać heapdump i wczytać go do Eclipse Memory Analyzer. HeapDump wygenerowane przez jave w wersji od 6u14 mają zawierać extra informacje, która pozwolą Eclipse Memory Analyzer na dogłębną analizę zawartości pamięci wraz z stanem wątków. Myślę że warto to sprawdzić...
. W kontekście debuggingu warto sprawdzić projekt http://youdebug.kenai.com/