知識庫 / Spring RSS 訂閱

如何測試 Spring AOP 方面

Spring,Testing
HongKong
6
11:12 AM · Dec 06 ,2025

1. 概述

面向切面編程 (AOP) 通過將橫切關注點分離為基本單元(稱為方面),從而改進程序設計。Spring AOP 幫助我們輕鬆實現方面。

AOP 方面與其它軟件組件沒有不同。它們需要不同的測試來驗證其正確性。在本教程中,我們將學習如何對 Spring AOP 方面進行單元測試和集成測試。

2. 什麼是 AOP?

AOP 是一種編程範式,補充面向對象編程 (OOP),用於模塊化橫切關注點,即跨越應用程序主要功能的函數。 一個類是 OOP 中的基本單元,而一個方面是 AOP 中的基本單元。 記錄和事務管理是典型的橫切關注點。

一個方面由兩個組件組成。一個是在定義橫切關注點邏輯,另一個是指定在應用程序執行期間應應用的斷點表達式。

下表提供了常見 AOP 術語的概述:

doj:field="col6"> doj:field="col8"> doj:field="col10"> doj:field="col12"> doj:field="col14">
術語 描述
關注點 應用程序的特定功能。
橫切關注點跨越應用程序多個部分的功能。
方面AOP 的基本單元,包含建議和斷點表達式以實現橫切關注點。
建議橫切關注點中要調用的特定邏輯。
斷點選擇將應用建議的連接點的表達式。
連接點應用程序的執行點,例如方法。

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.3.2</version>
</dependency>

在日誌部分,我們選擇 SLF4J 作為 API 以及 SLF4J Simple 提供程序用於日誌實現。SLF4J 是一種面紗(facade),它提供了一個在不同日誌實現之間統一的 API。

因此,我們在 SLF4J APISLF4J Simple 提供程序 依賴項在我們的 pom.xml 中:

<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 類簡潔明瞭,僅包含一個建議,即 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;
    }
}

The @Around 標註指示建議(advice)logExecutionTime() 在切入點(pointcut expression)execution(…)定義的周圍執行。在 Spring AOP 中,切入點始終是一個方法。

4. 方面單元測試

從單元測試角度來看,我們僅測試方面內部的邏輯,而不依賴任何外部因素,包括 Spring 應用上下文。 在這個例子中,我們使用 Mockito 模擬 joinPoint 和日誌記錄器,然後將 Mock 對象注入到我們的測試方面中。

單元測試類使用 @ExtendsWith(MockitoExtension.class) 標註,以啓用 JUnit 5 中 Mockito 的功能。它會自動初始化 Mock 對象並將其注入到使用 @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() 方法在切面中被調用一次。同時,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 實例,而不是使用 new ArraySorting() 創建實例。這確保了 Spring AOP 在目標類上被激活。最後,它斷言緩衝區中是否包含日誌。

6. 結論

在本文中,我們探討了 AOP 的基本概念,並學習瞭如何在目標類上使用 Spring AOP 方面。我們還通過使用單元測試和集成測試來驗證方面的正確性。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.