Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test Aspect in Spring MVC application

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???
    }
}
like image 898
Vadim Dissa Avatar asked Dec 01 '22 13:12

Vadim Dissa


2 Answers

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.

How to test that the aspect method actually gets called?

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());

How to test that the aspect method actually writes to log?

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.

like image 168
Roman Puchkovskiy Avatar answered Dec 05 '22 07:12

Roman Puchkovskiy


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();
}
like image 30
gmaslowski Avatar answered Dec 05 '22 07:12

gmaslowski