如何测试 Spring AOP 切面
1. 概述
面向切面编程(AOP)通过将横切关注点从主要应用程序逻辑中分离为一个基本单元(称为切面)来改进程序设计。Spring AOP 是一个帮助我们轻松实现切面的框架。
AOP 切面与其他软件组件一样,需要不同的测试来验证其正确性。在本教程中,我们将学习如何对 SpringAOP 切面进行单元和集成测试。
2. 什么是 AOP?
AOP 是一种编程范式,它是面向对象编程(OOP)的一种补充,用以模块化横切关注点,横切关注点是跨越主要应用程序的函数。类是 OOP 中的基本单元,而切面你是 AOP 中的基本单位。日志记录和事务管理是横切关注点的典型例子。
一个切面由两个组件组成。一个是定义横切的逻辑的通知(advice),而另一个是指定在应用程序执行期间应何时应用逻辑的切入点。
下表提供了常见 AOP 术语的概览:
术语 | 描述 |
---|---|
关注点(Concern) | 应用的特定功能。 |
横切关注点(Cross-cutting concern) | 跨应用程序多个部分的特定功能。 |
切面(Aspect) | AOP 的基础单元,其包含通知和实现横切关注点的切入点。 |
通知(Advice) | 在横切关注点中调用的具体逻辑 |
切入点(Pointcut) | 选择使用通知的连接点的表达式 |
连接点(Join Point) | 应用的执行点,比如方法。 |
3. 执行时间日志
在本节中,让我们创建一个切面示例,记录连接点周围的执行时间。
3.1. Maven 依赖性
Java 中有不同的 AOP 框架,如 Spring AOP 和 AspectJ。在本教程中,我们将使用 Spring AOP,并在 pom.xml 中包含以下依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>3.2.5</version>
</dependency>
对于日志记录部分,我们选择 SLF4J 作为 API 和 SLF4J Simple 提供者,作为日志实现。SLF4J 是一个在不同的日志实现中提供统一的 API 的门面。
因此,我们还在 pom.xml 中包含了 SLF4J API 和 SLF4J simple 提供者依赖项:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.13</version>
</dependency>
3.2. 执行时间切面
ExecutionTimeAspect
类很简单,只包含一个通知(advice) logExecutionTime()
。我们用 @Aspect
和 @Component
注释类,将其声明为一个切面,并使 Spring 能够管理它:
@Aspect
@Component
public class ExecutionTimeAspect {
private Logger log = LoggerFactory.getLogger(ExecutionTimeAspect.class);
@Around("execution(* com.baeldung.unittest.ArraySorting.sort(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long t = System.currentTimeMillis();
Object result = joinPoint.proceed();
log.info("Execution time=" + (System.currentTimeMillis() - t) + "ms");
return result;
}
}
@Around
注释表示通知 logExecutionTime()
围绕切入点表达式 execution(…)
定义的目标连接点运行。在 Spring AOP 中,连接点始终是一个方法。
4.切面单元测试
从单元测试的角度来看,我们只测试切面内部的逻辑,没有任何依赖关系,包括 Spring 应用上下文。在这个例子中,我们使用 Mockito 来模拟 joinPoint 和 logger,然后将模拟注入我们的测试切面。
单元测试类用 @ExtendesWith(MockitoExtension.class)
进行注释,以启用 JUnit5 的 Mockito 功能。它自动初始化 mocks,并将它们注入到我们的测试单元中,该测试单元用 @InjectMocks
注释:
@ExtendWith(MockitoExtension.class)
class ExecutionTimeAspectUnitTest {
@Mock
private ProceedingJoinPoint joinPoint;
@Mock
private Logger logger;
@InjectMocks
private ExecutionTimeAspect aspect;
@Test
void whenExecuteJoinPoint_thenLoggerInfoIsCalled() throws Throwable {
when(joinPoint.proceed()).thenReturn(null);
aspect.logExecutionTime(joinPoint);
verify(joinPoint, times(1)).proceed();
verify(logger, times(1)).info(anyString());
}
}
在这个测试用例中,我们预期在切面中调用 joinPoint.proceed()
方法一次。此外,logger 的 info()
方法也应该被调用一次,以记录执行时间。
为了更准确地验证日志消息,我们可以使用 ArgumentCaptor
类来捕获日志消息。这使我们能够断言以 “Execution time=” 开头生成的消息:
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
verify(logger, times(1)).info(argumentCaptor.capture());
assertThat(argumentCaptor.getValue()).startsWith("Execution time=");
5. 切面集成测试
从集成测试的角度来看,我们需要类实现 ArraySorting
通过切入点表达式将我们的通知应用到目标类。sort()
方法简单地调用静态方法Collections.sort()
对列表进行排序:
@Component
public class ArraySorting {
public <T extends Comparable<? super T>> void sort(List<T> list) {
Collections.sort(list);
}
}
你可能会有疑问:为什么我们不将通知应用于 Collections.sort()
静态方法呢?这是 Spring AOP 的一个限制,它不能在静态方法上工作。Spring AOP 创建动态代理来拦截方法调用。这种机制要求在目标对象上调用实际方法,而静态方法则可以在没有对象的情况下调用。如果我们需要拦截静态方法,我们必须采用另一个支持编译时编织的 AOP 框架,如 AspectJ。
在集成测试中,我们需要 Spring 应用上下文来创建一个代理对象,以拦截目标方法并应用通知。我们用 @SpringBootTest
注释集成测试类,以加载启用 AOP 和依赖注入功能的应用上下文:
@SpringBootTest
class ExecutionTimeAspectIntegrationTest {
@Autowired
private ArraySorting arraySorting;
private List<Integer> getRandomNumberList(int size) {
List<Integer> numberList = new ArrayList<>();
for (int n=0;n<size;n++) {
numberList.add((int) Math.round(Math.random() * size));
}
return numberList;
}
@Test
void whenSort_thenExecutionTimeIsPrinted() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream originalSystemOut = System.out;
System.setOut(new PrintStream(baos));
arraySorting.sort(getRandomNumberList(10000));
System.setOut(originalSystemOut);
String logOutput = baos.toString();
assertThat(logOutput).contains("Execution time=");
}
}
测试方法可分为三个部分。最初,它将输出流重定向到专用缓冲区,以便稍后进行断言。随后,它调用 sort()
方法,该方法调用切面中的通知。很重要的一点是,通过 @Autowired
注入 ArraySorting
实例,而不是用新的 ArraySorting()
实例化实例。这确保了 Spring AOP 在目标类上被激活。最后,它断言日志是否存在于缓冲区中。
6. 总结
在本文中,我们讨论了 AOP 的基本概念,并了解了如何在目标类上使用 Spring AOP 方面。我们还研究了单元测试和集成测试的测试方面,以验证这些方面的正确性。