I have a Spring MVC application where I use an Aspect to catch exceptions in all controller methods
@Component
@Aspect
public class ControllerExceptionAspect {
private Logger logger;
public ControllerExceptionAspect() {
logger = Logger.getLogger(ControllerExceptionAspect.class);
}
public ControllerExceptionAspect(Logger logger) {
this.logger = logger;
}
// Catching all exceptions from all methods in all controllers classes
@AfterThrowing(pointcut = "execution(* com.my.package..controller..*(..))", throwing = "exception")
public void afterThrowingAdvice(Exception exception) {
logger.error("CONTROLLER ASPECT: EXCEPTION IN METHOD -> " +
exception.getClass());
}
}
Aspect works fine but unfortunatelly I can not test it. I tried many times but can not get how to catch whether an Aspect method was called after I simulate exception in Controller
@SuppressWarnings("ALL")
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = RootConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
public class ControllerExceptionAspectTest {
@Autowired
ApplicationContext applicationContext;
@Test
public void testControllerExceptionAspectGetsExecutedWhenExceptionOccures(){
HomeController homeController = (HomeController)applicationContext.getAutowireCapableBeanFactory().getBean("homeController");
try{homeController.callMethod("00000");}
catch (Exception e){}
ControllerExceptionAspect controllerExceptionAspect = (ControllerExceptionAspect)applicationContext.getAutowireCapableBeanFactory().getBean("controllerExceptionAspect");
// HOW TO CATCH THAT ASPECT METHOD WAS CALLED???
}
}
It is pretty easy to test an aspect (including its pointcut expressions) in isolation, without the whole web context (or any context at all).
I will first try to give a generalized example, not the one that was in the OP question.
Let's imagine that we have an aspect that must throw an exception if a method's first argument is null, otherwise allow the method invocation proceed.
It should only be applied to controllers annotated with our custom @ThrowOnNullFirstArg
annotation.
@Aspect
public class ThrowOnNullFirstArgAspect {
@Pointcut("" +
"within(@org.springframework.stereotype.Controller *) || " +
"within(@(@org.springframework.stereotype.Controller *) *)")
private void isController() {}
@Around("isController()")
public Object executeAroundController(ProceedingJoinPoint point) throws Throwable {
throwIfNullFirstArgIsPassed(point);
return point.proceed();
}
private void throwIfNullFirstArgIsPassed(ProceedingJoinPoint point) {
if (!(point.getSignature() instanceof MethodSignature)) {
return;
}
if (point.getArgs().length > 0 && point.getArgs()[0] == null) {
throw new IllegalStateException("The first argument is not allowed to be null");
}
}
}
We could test it like this:
public class ThrowOnNullFirstArgAspectTest {
private final ThrowOnNullFirstArgAspect aspect = new ThrowOnNullFirstArgAspect();
private TestController controllerProxy;
@Before
public void setUp() {
AspectJProxyFactory aspectJProxyFactory = new AspectJProxyFactory(new TestController());
aspectJProxyFactory.addAspect(aspect);
DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory();
AopProxy aopProxy = proxyFactory.createAopProxy(aspectJProxyFactory);
controllerProxy = (TestController) aopProxy.getProxy();
}
@Test
public void whenInvokingWithNullFirstArg_thenExceptionShouldBeThrown() {
try {
controllerProxy.someMethod(null);
fail("An exception should be thrown");
} catch (IllegalStateException e) {
assertThat(e.getMessage(), is("The first argument is not allowed to be null"));
}
}
@Test
public void whenInvokingWithNonNullFirstArg_thenNothingShouldBeThrown() {
String result = controllerProxy.someMethod(Descriptor.builder().externalId("id").build());
assertThat(result, is("ok"));
}
@Controller
@ThrowOnNullFirstArg
private static class TestController {
@SuppressWarnings("unused")
String someMethod(Descriptor descriptor) {
return "ok";
}
}
}
The key part is inside the setUp()
method. Please note that it also allows to verify the correctness of your pointcut expression.
If the aspect method only has some some effect that is difficult to verify in tests, you could use a mock library like Mockito and make a stub around your real aspect and then verify that the method was actually called.
private ControllerExceptionAspect aspect = Mockito.stub(new ControllerExceptionAspect());
Then in your test, after invoking the controller via proxy
Mockito.verify(aspect).afterThrowingAdvice(Matchers.any());
If you are using logback-classic, you could write an Appender
implementation and add it to the Logger
of interest, and then inspect whether a message that you expect gets logged or not.
public class TestAppender extends AppenderBase<ILoggingEvent> {
public List<ILoggingEvent> events = new ArrayList<>();
@Override
protected void append(ILoggingEvent event) {
events.add(event);
}
}
In the fixture setup:
appender = new TestAppender();
// logback Appender must be started to accept messages
appender.start();
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ControllerExceptionAspect.class.class);
logger.addAppender(appender);
and in your test:
List<ILoggingEvent> errors = appender.events.stream()
.filter(event -> event.getLevel() == Level.ERROR)
.collect(Collectors.toList());
assertEquals("Exactly one ERROR is expected in log", 1, errors.size());
// any other assertions you need
Probably you would also need to stop()
the Appender
in @After
method, but I'm not sure.
I think that what you're trying to achieve is testing the configuration which you created (aspects pointcut), rather than aspect itself which could be unit tested. What I'm afraid is that there's no simple way of achieving that.
You could follow some internet advices on catching logs or other ideas. Honestly I'd test the expected behavior of the Aspect only if you really need to test that it was invoked. If it's loging I wouldn't do it. If it's setting something to the db (or other side effect) I'd verify if the value is in the db. That's the sloppy ground of integration testing.
If you really, really must test the aspect the way you want, you can write something similar the given code. But remember that normal (not-test) runtime spring configuration would require a dummy implementation of Verifier interface existing in the Spring context.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Config.class)
public class AspectTesting {
@Autowired
ServiceWithAspect service;
@Autowired
Verifier verifyingAspect;
@Test
public void test() {
// given
boolean condition = false;
// when
try {
service.doit();
} catch (Exception swallow) {}
// then
try {
condition = ((VerifyingAspect) ((Advised) verifyingAspect).getTargetSource().getTarget()).wasExecuted();
} catch (Exception swallow) {}
// then
Assert.assertTrue(condition);
}
}
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("aspects")
class Config {
}
@Component
class VerifyingAspect implements Verifier {
private boolean executed = false;
public boolean wasExecuted() {
return executed;
}
@Override
public void invoked() {
executed = true;
}
}
@Service
class ServiceWithAspect {
public void doit() {
throw new RuntimeException();
}
}
@Component
@Aspect
class TestedAspect {
@Autowired
Verifier verifier;
@AfterThrowing(pointcut = "execution(* *(..))", throwing = "exception")
public void afterThrowingAdvice(Exception exception) {
// your aspect logic here
verifier.invoked();
}
}
interface Verifier {
void invoked();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With