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