Enterprise Java

How to Test a Spring AOP Aspect

1. Introduction

Aspect-Oriented Programming (AOP) is one of programming paradigms that separates cross-cutting concerns as aspects. It complements Object-Oriented Programming (OOP) by enabling the encapsulation of behaviors that affect multiple classes into reusable aspects. Spring AOP framework supports AOP with AspectJ annotations. In this example, I will create a custom aspect with several advices including tracking the method execution time around the advice. Then I will demonstrate the Spring AOP test aspect with both unit and integration tests via a spring bean. The spring bean’s method’s execution will be weaved with the desired Aspect’s advice.

2. Setup

In this step, I will create a gradle project with spring-boot-starter, spring-boot-starter-aop, AspectJ, and Junit libraries.

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.0'
	id 'io.spring.dependency-management' version '1.1.5'
}

group = 'org.zheng.demo'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-aop'

    // AspectJ weaver
    implementation 'org.aspectj:aspectjweaver:1.9.22.1'
	
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

3. Source Code

3.1 Spring AOP Aspect

Spring AOP supports method execution join points for spring beans. It relies on the annotations defined by the aspectjweaver library. Spring AOP includes the following key concepts:

  • @Aspect: defines the aspect component that encapsulates a concern that cuts across multiple classes, such as logging, security, or transaction management. Make sure to include both @Component and @Aspect annotations.
  • Join Point: A point in the execution of a program, such as method execution or object creation, where an aspect can be applied.
  • @Pointcut: An expression that matches join points and determines whether an advice should be applied at that join point. Pointcuts can be based on method names, annotations, or other attributes.
  • @Before, @After, @AfterReturning, and @AfterThrowing Advices: Advice is the action taken by an aspect at a particular join point.

In this step, I will create a MyAopExample.java class with the above annotations.

MyAopExample.java

package org.zheng.demo.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAopExample {

	@After("execution(* org.zheng.demo.service.*.*(..))")
	public void afterAdvice() {
		System.out.println("AOP_afterAdvise is executed.");
	}

	@AfterReturning(pointcut = "afterReturningPC()", returning = "retV")
	public void afterReturningAdvice(String retV) {
		System.out.println("AOP_afterReturningAdvise is executed. " + retV);
	}

	@Pointcut("execution(* org.zheng.demo.service.*.*(..))")
	public void afterReturningPC() {
	}

	@AfterThrowing(pointcut = "matchingAll()", throwing = "e")
	public void afterThrowingAdvice(RuntimeException e) {
		Thread.setDefaultUncaughtExceptionHandler((t, e1) -> System.out.println("Caught " + e1.getMessage()));
		System.out.println("AOP_afterThrowingAdvice is executed");
	}

	@Before("execution(* org.zheng.demo.service.MyService.myMethod(..))")
	public void beforeAdvice() {
		System.out.println("AOP_beforeAdvice is executed");
	}

	@Around("execution(* org.zheng.demo.service.*.*(..))")
	public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
		long startTime = System.currentTimeMillis();

		Object proceed = joinPoint.proceed();

		long endTime = System.currentTimeMillis();
		long executionTime = endTime - startTime;

		System.out.println("AOP_aroundAdvice " + joinPoint.getSignature() + " executed in " + executionTime + "ms");
		return proceed;
	}

	@Pointcut(value = "execution(public * org.zheng..*.*(..))")
	public void matchingAll() {
	}

}
  • Line 13, 14: define a spring aspect component.
  • Line 17: define an @After advice for any service’s any method at the “org.zheng.demo.service” package.
  • Line 22: define an @AfterReturning advice for any service’s any method at the “org.zheng.demo.service” package.
  • Line 27: define a @Pointcut for any service’s any method at the “org.zheng.demo.service” package.
  • Line 31: define a @AfterThrowing advice for matchingAll() pointcut.
  • Line 37: define a @Before advice for org.zheng.demo.service.MyService.myMethod.
  • Line 42: defines a @Around advice to track the method’s execution time.
  • Line 55: defines a @Pointcut for matchingAll().

3.2 Spring Service

In this step, I will create a MyService.java class which annotates with the @Service annotation and has two methods.

MyService.java

package org.zheng.demo.service;

import org.springframework.stereotype.Service;

@Service
public class MyService {

	public void myMethod() {
		System.out.println("Executing myMethod");
	}

	public String returnData(String input) {
		System.out.printf("Executing returnData input=%s\n", input);
		Integer.parseInt(input);
		return input.toUpperCase();
	}
}
  • Line 14: the Integer.parseInt will throw an exception when the input is not a numeric string.

3.3 Spring Boot Application

In this step, I will create a SpringAopExampleApplication.java which includes @EnableAspectJAutoProxy. Please note, spring boot automatically discovers if you have missed it. Recommended added for a clarity purpose.

SpringAopExampleApplication.java

package org.zheng.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringAopExampleApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringAopExampleApplication.class, args);
	}
}
  • Line 8: @EnableAspectJAutoProxy is used to enable AOP.

4. Spring AOP Test Aspect

4.1 Unit Tests

In this step, I will create a MyAspectUnitTest.java class which mocked the AOP’s proceed and getSignature methods.

MyAspectUnitTest.java

package org.zheng.demo;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.zheng.demo.aop.MyAopExample;

@ExtendWith(MockitoExtension.class)
public class MyAspectUnitTest {
	@Mock
	private ProceedingJoinPoint proceedingJoinPoint;

	@InjectMocks
	private MyAopExample testAspect;

	@Test
	public void testLogExecutionTime() throws Throwable {
		// Given
		when(proceedingJoinPoint.proceed()).thenAnswer(invocation -> {
			Thread.sleep(100); // Simulate method execution time
			return null;
		});

		when(proceedingJoinPoint.getSignature()).thenReturn(mock(MethodSignature.class));

		// When
		testAspect.logExecutionTime(proceedingJoinPoint);

		// Then
		verify(proceedingJoinPoint, times(1)).proceed();
		// Additional assertions can be made to check the log output if needed
	}
}

Run the Junit test and capture the output:

MyAspectUnitTest Output

AOP_aroundAdvice Mock for MethodSignature, hashCode: 219638321 executed in 109ms

4.2 Integration Tests

In this step, I will create a MyAspectIntegrationTest.java class which verifies that Spring service’s methods are weaved with the MyAopExample‘s advice defined at step 3.1 based on the matching join points.

MyAspectIntegrationTest.java

package org.zheng.demo;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.zheng.demo.service.MyService;

@SpringBootTest
public class MyAspectIntegrationTest {

	@Autowired
	MyService myService;

	@Test
	void test_aop_before_after() {
		myService.myMethod();
	}

	@Test
	void test_aop_afterReturning() {
		String ret = myService.returnData("123");
		assertEquals("123", ret);
	}

	@Test
	void test_aop_afterThrowing() {
		NumberFormatException ex = assertThrows(NumberFormatException.class, () -> {
			myService.returnData("mary");
		});

		assertEquals("For input string: \"mary\"", ex.getMessage());
	}
}

Run the Junit test and capture the output. As you can see from the output, the five advice methods: before, after, around, afterThrowing, afterReturning were wearved as expected.

MyAspectIntegrationTest Output

2024-06-15T13:04:33.577-05:00  INFO 40688 --- [spring-aop-example] [           main] org.zheng.demo.MyAspectIntegrationTest   : Started MyAspectIntegrationTest in 1.35 seconds (process running for 2.425)
Executing returnData input=123
AOP_afterReturningAdvise is executed. 123
AOP_afterAdvise is executed.
AOP_aroundAdvice String org.zheng.demo.service.MyService.returnData(String) executed in 1ms
Executing returnData input=mary
AOP_afterThrowingAdvice is executed
AOP_afterAdvise is executed.
AOP_beforeAdvice is executed
Executing myMethod
AOP_afterAdvise is executed.
AOP_aroundAdvice void org.zheng.demo.service.MyService.myMethod() executed in 0ms

Line 3, 4: both afterReturning and after advice methods are weaved as they match the pointcut expression.

Run the tests and capture the status as the following screenshot:

Figure 1. Test Results

5. Conclusion

In this example, I created an AOP aspect with aspectJ annotations and weaved it into Spring bean’s methods. I also demonstrated the spring AOP test aspect via both unit and integration tests. As you saw in this example, there are few benefits of AOP:

  • Modularity: separates cross-cutting concerns from the main business logic, making code easier to maintain and understand.
  • Reusability: aspects can be reused across different parts of the application.
  • Maintainability: changes to cross-cutting concerns (like logging or security) can be made in one place.

6. Download

This was an example of a gradle project which created a Spring AOP Aspect and tested it via both unit and integration tests.

Download
You can download the full source code of this example here: How to Test a Spring AOP Aspect

Mary Zheng

Mary graduated from the Mechanical Engineering department at ShangHai JiaoTong University. She also holds a Master degree in Computer Science from Webster University. During her studies she has been involved with a large number of projects ranging from programming and software engineering. She worked as a lead Software Engineer in the telecommunications sector where she led and worked with others to design, implement, and monitor the software solution.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button