How to test an aspect with SpringBootTest?
Asked Answered
M

6

19

I created a simple aspect in Spring using Spring Boot 2.1.6.RELEASE. It basically logs the total time spent on a method.

@Aspect
@Component
public class TimeLoggerAspect {

  private static final Logger log = LoggerFactory.getLogger(TimeLoggerAspect.class);

  @Around("@annotation(demo.TimeLogger)")
  public Object methodTimeLogger(ProceedingJoinPoint joinPoint) 
          throws Throwable {
    long startTime = System.currentTimeMillis();

    Object proceed = joinPoint.proceed();

    long totalTime = System.currentTimeMillis() - startTime;
    log.info("Method " + joinPoint.getSignature() + ": " + totalTime + "ms");

    return proceed;
  }
}

the aspect is triggered by a TimeLogger annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TimeLogger {
}

and is used in a component like this

@Component
public class DemoComponent {
  @TimeLogger
  public void sayHello() {
    System.out.println("hello");
  }
}

A spring boot demo application will invoke sayHello via the run method of the CommandLineRunner interface.

@SpringBootApplication
public class DemoApplication implements CommandLineRunner {

  @Autowired
  private DemoComponent demoComponent;

  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

  @Override
  public void run(String... args) throws Exception {
    demoComponent.sayHello();
  }
}

For completeness, I add my modifications in build.gradle: adding libraries for aop, spring test and jupiter (junit).

    compile("org.springframework.boot:spring-boot-starter-aop")

    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.junit.jupiter:junit-jupiter-api")
    testRuntime("org.junit.jupiter:junit-jupiter-engine")

Running the application will output (trimmed for readability)

hello
... TimeLoggerAspect : Method void demo.DemoComponent.sayHello(): 4ms

So far, so good. Now I create a test based on @SpringBootTest annotation and jupiter.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = {DemoComponent.class, TimeLoggerAspect.class})
public class DemoComponentFailTest {

  @Autowired
  private DemoComponent demoComponent;

  @Test
  public void shouldLogMethodTiming() {
      demoComponent.sayHello();
  }
}

and here I get the output

hello

No output from the TimeLoggerAspect, since it seems it is not being triggered.

Is something missing to trigger the aspect in the test? Or are there other ways of testing the aspect in spring boot?

Myocarditis answered 25/6, 2019 at 23:2 Comment(3)
On a somewhat related note, have you looked into the Spring PerformanceMonitorInterceptor which seems to already do what you're attempting to implement?Volauvent
Otherwise, have you attempted using @SpringBootTest without restricting it to specific classes?Volauvent
K. The real case is a bit difficult so I used it as an example. Remove "classes" on the annotation does not help.Myocarditis
K
8

You have to put @EnableAspectJAutoProxy with your file @Configuration that declares the bean with @Aspect.

@Aspect
@Configuration
@EnableAspectJAutoProxy
public class TimeLoggerAspect {

  private static final Logger log = LoggerFactory.getLogger(TimeLoggerAspect.class);

  @Around("@annotation(demo.TimeLogger)")
  public Object methodTimeLogger(ProceedingJoinPoint joinPoint) 
          throws Throwable {
    long startTime = System.currentTimeMillis();

    Object proceed = joinPoint.proceed();

    long totalTime = System.currentTimeMillis() - startTime;
    log.info("Method " + joinPoint.getSignature() + ": " + totalTime + "ms");

    return proceed;
  }
}

I think that will do the work.

Knowlton answered 19/2, 2020 at 14:27 Comment(1)
This cannot be correct simply because you change the implementation instead of the test method. The implementation is fine, it's just about how to kick-start spring-aop magic working when testing.Cabriolet
F
19

I had similar problem. My Aspect is listening on controller methods. To get it activated, importing the AnnotationAwareAspectJAutoProxyCreator made the trick:

@RunWith(SpringRunner.class)
@Import(AnnotationAwareAspectJAutoProxyCreator.class) // activate aspect
@WebMvcTest(MyController.class)
public class MyControllerTest {

    ...

}
Furan answered 15/11, 2019 at 16:42 Comment(2)
exactly what I needed!Chryso
thanks, my project use junit5, now worked.Swingletree
K
8

You have to put @EnableAspectJAutoProxy with your file @Configuration that declares the bean with @Aspect.

@Aspect
@Configuration
@EnableAspectJAutoProxy
public class TimeLoggerAspect {

  private static final Logger log = LoggerFactory.getLogger(TimeLoggerAspect.class);

  @Around("@annotation(demo.TimeLogger)")
  public Object methodTimeLogger(ProceedingJoinPoint joinPoint) 
          throws Throwable {
    long startTime = System.currentTimeMillis();

    Object proceed = joinPoint.proceed();

    long totalTime = System.currentTimeMillis() - startTime;
    log.info("Method " + joinPoint.getSignature() + ": " + totalTime + "ms");

    return proceed;
  }
}

I think that will do the work.

Knowlton answered 19/2, 2020 at 14:27 Comment(1)
This cannot be correct simply because you change the implementation instead of the test method. The implementation is fine, it's just about how to kick-start spring-aop magic working when testing.Cabriolet
M
6

Another solution that seems to work is adding AnnotationAwareAspectJAutoProxyCreator in classes of @SpringBootTest, although I am not quite certain why.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = { DemoComponent.class, 
                            TimeLoggerAspect.class,
                            AnnotationAwareAspectJAutoProxyCreator.class })
public class DemoComponentFailTest {

  @Autowired
  private DemoComponent demoComponent;

  @Test
  public void shouldLogMethodTiming() {
      demoComponent.sayHello();
  }
}
Myocarditis answered 2/7, 2019 at 9:20 Comment(1)
interestingly this is the only approach which worked for us.Cassilda
P
5

You need to start an @SpringBootApplication. However, it does not have to be the one you use to start your app in production. It can be a special one for this test only and can be in your test sources root not your src.

@SpringBootApplication
@ComponentScan(basePackageClasses = {DemoComponent.class, TimeLoggerAspect.class})
public class SpringBootTestMain {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTestMain.class, args);
    }

}

Then in your test this is the only class you need to list.

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = SpringBootTestMain.class)
public class DemoComponentFailTest {
Pother answered 26/6, 2019 at 1:37 Comment(2)
Interesting. It works, but it feels a bit wrong. I was even able to remove @ComponentScan; probably because SpringBootTestMain is in the same package as the other classes.Myocarditis
You can remove the @ComponentScan but then your whole app will start so as the app grows, it will take longer. And if there is a dependency that is unrelated but can't be created like a database or initial load then it will fail. If you can start your whole app just use @SpringBootApplication and let your regular app main start it. I thought you needed it because you had the @SpringBootTest(classes = {DemoComponent.class, TimeLoggerAspect.class}) on your test class .Pother
D
1

When I had to test an aspect, I used the approach below.

@SpringBootTest
@ContextConfiguration(classes = {MyAspectImpl.class, MyAspectTest.TestConfiguration.class})
@EnableAspectJAutoProxy
public class MyAspectTest {

  @org.springframework.boot.test.context.TestConfiguration
static class TestConfiguration {
    @Bean
    public MyAspectTestClass myAspectTestClass() {
      return new MyAspectTestClass();
    }
  }

  @Autowired
  private MyAspectTestClass target;

  @Test
  public void testCorrectlySetsPoolNameUsingMethodParameter() {
    target.testMethod();
  }

  @NoArgsConstructor
  private static class MyAspectTestClass {
    @MyAspect
    public void testMethod() {
     //Add some logic here
    }
  }
}
Derr answered 26/1, 2023 at 10:2 Comment(0)
S
0

@Import (AnnotationAwareAspectJAutoProxyCreator.class) This alone did not work for me.

I had to Include my aspect class and controller class in ContextConfiguration. And then Autowire them in the test class.

@ContextConfiguration (classes = {MyController.class, MyAspect.class})

@Autowired
MyController myController;

@Autowired
MyAspect myAspect;

Leaving it here. Thought this might help somebody someday.

Substitution answered 9/6, 2023 at 11:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.