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 :
Pomysł jest prosty:
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:
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() {} }
- 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.
- 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ę.
Pomysł jest prosty:
- Napiszmy aspekt z advice typu around z testowanym pointcutem i w nim zmodyfikujmy zwracany obiekt.
- W teście sprawdzamy czy zwrócony przez serwis obiekt jest zmodyfikowany przez aspekt.
/** * 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:
- sami tworzymy kontekst springa (// 1),
- w kontekście rejestrujemy jednego beana TestApplicationContextConfguration (// 2)
- pobieramy referencję do beana serwisu (// 3)
- 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)
- 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ć.
@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