编程

如何测试 Spring AOP 切面

90 2024-05-05 01:18:00

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 方面。我们还研究了单元测试和集成测试的测试方面,以验证这些方面的正确性。