You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中AuditFilter(OncePerRequestFilter)单元测试实现问询

搞定AuditFilter与异步AuditLogManager的JUnit测试方案

别慌!针对你的AuditFilter(继承OncePerRequestFilter)和带@Async注解的AuditLogManager测试需求,我给你梳理一套完整的JUnit测试方案,从单元测试到集成测试,再到Maven构建配置,一步到位~

一、先搞定测试依赖

首先确保你的pom.xml里包含了必要的测试依赖,Spring Boot项目可以直接用spring-boot-starter-test,它已经整合了JUnit 5、Mockito、Spring Test等工具:

<dependencies>
    <!-- 核心测试依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <!-- 排除JUnit 4,避免版本冲突 -->
            <exclusion>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

二、AuditFilter的单元测试

过滤器的单元测试重点是验证请求处理前后的逻辑,以及finally块里的异步调用是否一定会执行(哪怕请求抛出异常)。我们用Mockito模拟依赖,直接调用过滤器的核心方法:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import static org.mockito.Mockito.*;

class AuditFilterTest {

    // 模拟AuditLogManager依赖
    @Mock
    private AuditLogManager auditLogManager;

    // 待测试的过滤器,自动注入mock的auditLogManager
    @InjectMocks
    private AuditFilter auditFilter;

    @Mock
    private HttpServletRequest request;

    @Mock
    private HttpServletResponse response;

    @Mock
    private FilterChain filterChain;

    @BeforeEach
    void setUp() {
        // 初始化Mockito注解
        MockitoAnnotations.openMocks(this);
        // 初始化过滤器参数:审计模式BASIC,监听/api/**路径
        auditFilter = new AuditFilter(auditLogManager, "BASIC", List.of("/api/**"));
    }

    @Test
    void doFilterInternal_ShouldTriggerAuditLog_AfterRequestProcessing() throws ServletException, IOException {
        // 模拟FilterChain正常执行
        doNothing().when(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));

        // 调用过滤器核心方法
        auditFilter.doFilterInternal(request, response, filterChain);

        // 验证FilterChain的doFilter被执行一次
        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
        // 验证异步审计日志方法被调用(根据你的AuditInfo调整参数匹配器)
        verify(auditLogManager, times(1)).log(any(AuditInfo.class), eq("BASIC"));
    }

    @Test
    void doFilterInternal_ShouldTriggerAuditLog_EvenIfRequestThrowsException() throws ServletException, IOException {
        // 模拟FilterChain抛出异常
        doThrow(new ServletException("测试异常")).when(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));

        // 调用过滤器方法,捕获异常
        try {
            auditFilter.doFilterInternal(request, response, filterChain);
        } catch (ServletException ignored) {}

        // 关键验证:哪怕请求抛出异常,finally块里的审计日志仍会执行
        verify(auditLogManager, times(1)).log(any(AuditInfo.class), eq("BASIC"));
    }
}

注意:如果你的过滤器里对Request/Response做了包装(比如wrappedRequest),测试时要模拟包装类的方法返回值,或者用真实的包装实例。

三、AuditLogManager的异步方法测试

因为用到了@Async,测试时需要确保Spring的异步上下文被加载,同时要等待异步方法执行完成再验证结果。这里分两种场景:

1. 单元测试(模拟持久化层)

用Mockito模拟数据持久化依赖,验证异步方法是否正确调用了持久化逻辑:

import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.scheduling.annotation.EnableAsync;
import static org.mockito.Mockito.*;

@SpringBootTest
@EnableAsync // 必须启用异步支持,否则@Async注解不生效
class AuditLogManagerUnitTest {

    // 模拟数据持久化仓库
    @Mock
    private AuditRepository auditRepository;

    // 待测试的AuditLogManager
    @InjectMocks
    private AuditLogManager auditLogManager;

    @Test
    void log_ShouldCallRepositorySave_Asynchronously() throws InterruptedException {
        // 构造测试用的审计信息
        AuditInfo auditInfo = new AuditInfo();
        auditInfo.setUserId("test-user-001");
        auditInfo.setRequestUrl("/api/orders");

        // 调用异步日志方法
        auditLogManager.log(auditInfo, "BASIC");

        // 验证异步方法在1秒内调用了仓库的save方法
        verify(auditRepository, timeout(1000)).save(any(AuditLog.class));
    }
}

2. 集成测试(真实持久化验证)

如果要验证异步方法是否真的把数据写入数据库,可以用CountDownLatch等待异步任务完成:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
// 可以指定测试用的数据库(比如H2内存库)
@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:h2:mem:testdb",
    "spring.datasource.driverClassName=org.h2.Driver",
    "spring.jpa.hibernate.ddl-auto=create-drop"
})
class AuditLogManagerIntegrationTest {

    @Autowired
    private AuditLogManager auditLogManager;

    @Autowired
    private AuditRepository auditRepository;

    @Test
    void log_ShouldPersistAuditData_Asynchronously() throws InterruptedException {
        // 创建CountDownLatch,等待异步任务完成
        CountDownLatch latch = new CountDownLatch(1);

        // 构造测试数据
        AuditInfo auditInfo = new AuditInfo();
        auditInfo.setUserId("test-user-001");
        auditInfo.setRequestUrl("/api/orders");

        // 调用异步方法,这里假设你的log方法执行完成后会触发latch.countDown()
        // 如果没有回调,可以在调用后直接查询数据库,用latch等待
        auditLogManager.log(auditInfo, "BASIC");

        // 最多等待3秒,确保异步任务完成
        boolean isCompleted = latch.await(3, TimeUnit.SECONDS);
        assertTrue(isCompleted, "异步审计日志任务执行超时");

        // 验证数据库中存在该记录
        AuditLog savedLog = auditRepository.findByUserId("test-user-001");
        assertNotNull(savedLog);
        assertEquals("/api/orders", savedLog.getRequestUrl());
    }
}

四、集成测试:过滤器+异步管理器全流程验证

用Spring的MockMvc模拟真实HTTP请求,验证整个审计流程是否正常工作:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.mockito.Mockito.*;

@SpringBootTest
@AutoConfigureMockMvc // 自动配置MockMvc
class AuditFilterIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private AuditLogManager auditLogManager;

    @Test
    void requestToAuditedUrl_ShouldTriggerAuditLog() throws Exception {
        // 模拟一个符合审计路径的请求
        mockMvc.perform(get("/api/orders"))
                .andExpect(status().isOk());

        // 验证异步审计方法被调用
        verify(auditLogManager, timeout(1000)).log(any(AuditInfo.class), eq("BASIC"));
    }

    @Test
    void requestToNonAuditedUrl_ShouldNotTriggerAuditLog() throws Exception {
        // 模拟一个不在审计路径内的请求
        mockMvc.perform(get("/public/home"))
                .andExpect(status().isOk());

        // 验证审计方法未被调用
        verify(auditLogManager, never()).log(any(AuditInfo.class), anyString());
    }
}

五、Maven构建时自动执行测试

确保你的pom.xml里的maven-surefire-plugin配置正确,这样执行mvn clean install或mvn test时会自动运行所有测试:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.1.2</version>
            <configuration>
                <!-- 指定测试类的命名规则,默认是*Test.java -->
                <includes>
                    <include>**/*Test.java</include>
                </includes>
            </configuration>
        </plugin>
    </plugins>
</build>

最后几个小提示

  • 异步测试一定要用timeout或CountDownLatch等待任务完成,否则测试会提前结束导致验证失败。
  • 单元测试聚焦单个类的逻辑,集成测试验证整个流程的正确性。
  • 可以用H2内存数据库做集成测试,避免污染生产环境数据。

内容的提问来源于stack exchange,提问作者Azttrtabv tens

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:46:33