Spring Boot中AuditFilter(OncePerRequestFilter)单元测试实现问询
别慌!针对你的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

