piątek, 3 czerwca 2011

Testowanie pointcut-ów w springu.

Witam. Pisałem już kilka postów o aspektach w springu. Testowałem ich działanie poprzez sysout-y. Dziś postanowiłem opisać jak można napisać test jednostkowy sprawdzający działanie pointcut-a. Pomysł zaczerpnąłem od kolegi z pracy - Darka Kaczyńskiego - ukłon w jego stronę.
Dodatkowo pokażę jak skorzystać z JavaConfig - dla tych, którzy mają dość xml-a (można postawić springa bez linijki xml-a ;-) ).

No to trochę kodu :
public interface Service {
    String getMessage(String name);
    String getMessage(Double a);
    String getMessage();
}

public class ExampleService implements Service {

    @Override
    public String getMessage(String name) {
        return name;
    }

    @Override
    public String getMessage(Double a) {
        return null;
    }

    @Override
    public String getMessage() {
        return null;
    }
}
Popracujemy na takim prostym interfejsie. Zdefiniujmy też dwa pointcut-y:
public class Pointcuts {
    @Pointcut("execution(String getMessage())")
    public void getMessageWithNoArgument() {}

    @Pointcut("execution(String pl.turo..Service+.getMessage(String))")
    public void getMessageWithStringArgument()  {}
}
  1. Pierwszy wyciąga metody o nazwie getMessage zwracającej obiekt typu String oraz nieprzyjmującej żadnych parametrów. Mamy jedną metodę w interfejsie Service pasującą do tego pointcut-a.
  2. Drugi wyciąga metody z klas implementujących interfejs Service o nazwie getMessage, zwracającej obiekt typu String i przyjmującej jeden parametr typu String. W naszym interfejsie mamy taką jednę metodę.
Zatem jak sprawdzić czy aby napewno pointcut-y robią to czego od nich oczekujemy ?
Pomysł jest prosty:
  1. Napiszmy aspekt z advice typu around z testowanym pointcutem i w nim zmodyfikujmy zwracany obiekt.
  2. W teście sprawdzamy czy zwrócony przez serwis obiekt jest zmodyfikowany przez aspekt.
Na każdy pointcut utworzę osobną klasę aspektu (dla przejrzystości, ale nie ma to żadnego znaczenia):
/**
 * Aspekt z metodą advice na metody bezparametrowe zwracające String o nazwie getMessage
 */
@Aspect
public class TestNoMethodParameterAspect {
    public static final String NO_PARAMETER_PREFIX = "NO_PARAMETER_PREFIX_";

    @Around("pl.turo.spring.aop.utils.Pointcuts.getMessageWithNoArgument()")
    public Object around(ProceedingJoinPoint joinPoint) {
        String retString = null;
        try {
            // wiemy że oczekujemy w wyniku String (tak definiuje pointcut)
            retString = (String) joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        // hak na sprawdzenie czy rzeczywiście uruchomił się advice
        return NO_PARAMETER_PREFIX + retString;
    }
}
oraz
/**
 * Aspekt z metodą advice na metody klas implementujących interfejs Service; przyjmujące String w parametrze; zwracające String
 */
@Aspect
public class TestStringParameterAspect {
    public static final String STRING_PARAMETER_PREFIX = "STRING_PARAMETER_PREFIX_";

    @Around("pl.turo.spring.aop.utils.Pointcuts.getMessageWithStringArgument()")
    public Object around(ProceedingJoinPoint joinPoint) {
        String retString = null;
        try {
            // wiemy że oczekujemy w wyniku String (tak definiuje pointcut)
            retString = (String) joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        // hak na sprawdzenie czy rzeczywiście uruchomił się advice
        return STRING_PARAMETER_PREFIX + retString;
    }
}
Kod jest niemal identyczny (różni się jedynie prefix-em) ale dla jasności przykładu zdecydowałem się na niego w takiej postaci.
Czas na napisanie klasy testowej. Przeanalizujmy ją po kawałku (całą można pobrać wraz z projektem stąd).
Na początek jak postawić springa dzięki JavaConfig:
public class AopTest {
    private Service service;

    // 1
    private static AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();

    @BeforeClass
    public static void setUp() {
        // 2
        applicationContext.register(TestApplicationContextConfguration.class);                  
        applicationContext.refresh();
    }

    @Before
    public void seUp() {
        // 3
        this.service = applicationContext.getBean(Service.class);                               
    }

    /**
     * Konfiguracja testowego kontekstu aplikacji.
     */
    // 4
    @Configuration                                                                         
    public static class TestApplicationContextConfguration {

        @Bean
        public ExampleService service() {
            return new ExampleService();
        }

        @Bean
        public AnnotationAwareAspectJAutoProxyCreator autoProxyCreator() {
            return new AnnotationAwareAspectJAutoProxyCreator();
        }

        @Bean
        public TestNoMethodParameterAspect testNoMethodParameterAspect() {
            return new TestNoMethodParameterAspect();
        }

        @Bean
        public TestStringParameterAspect testStringParameterAspect() {
            return new TestStringParameterAspect();
        }
    }
}
Zauważmy że:
  1. sami tworzymy kontekst springa (// 1),
  2. w kontekście rejestrujemy jednego beana TestApplicationContextConfguration (// 2)
  3. pobieramy referencję do beana serwisu (// 3)
  4. klasa TestApplicationContextConfguration jest publiczna i oznaczona adnotacją @Configuration. Każda metoda w tej klasie która będzie oznaczona adnotacją @Bean będzie uruchamiana przez springa. Zwracany obiekt będzie beanem w springu pod id identycznym jak nazwa metody. Zatem dzięki TestApplicationContextConfguration będziemy mieli w kontekście 4 beany (service, testNoMethodParameterAspect, testStringParameterAspect, autoProxyCreator)
  5. zdziwić może autoProxyCreator - otóż jest to bena odpowiedzialny za wykrycie beanów oznaczonych adnotacją @Aspect i odpowiednie ich przetworzenie (jest to PostProcessor). W xml-u skorzystalibyśmy z namespace aop i spring sam by tego bena dołożył. Konfigurując spring za pomocą JavaConfig należy samemu o takich technikaliach pamiętać.
Mamy już klasę testu ze skonfigurowanym kontekstem springa. Czas na metody testowe:
@Test
public void testEmptyMessage() {
    // metoda w klasie ExampleService jest bezparametrowa i zawsze zwraca null
    String message = service.getMessage();

    // advice w TestNoMethodParameterAspect do niej powinien dołożyć prefix
    assertTrue(message.startsWith(TestNoMethodParameterAspect.NO_PARAMETER_PREFIX));

    // advice w TestStringParameterAspect nie powinien się uruchomić
    assertFalse(message.startsWith(TestStringParameterAspect.STRING_PARAMETER_PREFIX));
}


@Test
public void testStringPropertyMessage() {
    // metoda w klasie pl.turo.spring.aop.service.ExampleService posiada jeden parametr i zawsze zwraca go zwraca
    String message = service.getMessage("janko");

    assertNotNull(message, "Metoda nie może wzrócić null");

    // advice w TestNoMethodParameterAspect nie powinien się uruchomić:
    assertFalse(message.startsWith(TestNoMethodParameterAspect.NO_PARAMETER_PREFIX));

    // advice w TestStringParameterAspect powinien dołożyć swój prefix
    assertTrue(message.startsWith(TestStringParameterAspect.STRING_PARAMETER_PREFIX));
}


@Test
public void testDoubleMessage() {
    // metoda w ExampleService zawsze zwraca null
    String message = service.getMessage(2d);

    // metoda jako parametr przyjmuje coś innego niż String więc żaden z aspektów nie powienien nic dodać do wyniku
    assertNull(message);
}
Testy przechodzą, co oznacza że aspekty się uruchamiają wtedy, kiedy tego oczekiwaliśmy. Dociekliwi mogliby spytać czemu wybrałem advice typu around a nie afterReturning - otóż testujemy czy pointcut poprawnie 'wyciągnie' metody. Zatem jeśli poleci wyjątek w metodzie to nas to nie interesuje (nie to testujemy czy serwis działa poprawnie) - afterReturning nie zostanie uruchomiony jeśli metoda rzuci wyjątek. Jak zwykle udostępniam projekt z całościowym kodem.

Brak komentarzy:

Prześlij komentarz