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

Spring Boot中Filter请求拦截的单元测试问题求助

问题:Spring Boot请求限流Filter的单元测试困境

场景说明

我用Spring Boot的Filter实现了请求次数限制,但编写单元测试时遇到了麻烦。核心需求是mock requestCountsPerIpAddress.get(clientIpAddress)的调用,验证当请求数超过阈值时返回限流结果,但尝试了@Mock、mock Filter类等多种方式都没成功。

实现的Filter代码

@Component
public class RequestFilter implements Filter {

  @Value("${chatGPT.max.request.per.second}")
  private int MAX_REQUESTS_PER_SECOND;

  private final LoadingCache<String, Integer> requestCountsPerIpAddress;

  public RequestFilter() {
    super();
    requestCountsPerIpAddress = Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.SECONDS)
        .build(key -> 0);
  }

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
      throws IOException, ServletException {
    var httpServletResponse = (HttpServletResponse) servletResponse;
    var clientIpAddress = getClientIP((HttpServletRequest) servletRequest);
    if(isMaximumRequestsPerSecondExceeded(clientIpAddress)) {
      httpServletResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
      httpServletResponse.getWriter().write("Too many requests");
      return;
    }

    filterChain.doFilter(servletRequest, servletResponse);
  }

  // RequestFilterMaxRequestExceededTest
  public boolean isMaximumRequestsPerSecondExceeded(String clientIpAddress){
    boolean max_request_exceeded = true;
    var requests = requestCountsPerIpAddress.get(clientIpAddress);
    if(Objects.isNull(requests)) {
      // if there are no requests tracked yet, requests to be initialized
      requests = 0;
    }
    if(requests > MAX_REQUESTS_PER_SECOND) {
        return max_request_exceeded;
    }
    requests++;
    requestCountsPerIpAddress.put(clientIpAddress, requests);
    return !max_request_exceeded;
  }

  public String getClientIP(HttpServletRequest request) {
    String xfHeader = request.getHeader("X-Forwarded-For");
    if (xfHeader == null){
      return request.getRemoteAddr();
    }
    return xfHeader.split(",")[0];
  }
}

测试目标

我希望验证:当请求数i>9时(假设MAX_REQUESTS_PER_SECOND设为9),调用isMaximumRequestsPerSecondExceeded会返回true。我的测试代码如下:

@DisplayName("---")
@Test
void requestFilterMaxRequestExceededTest() {
  for (int i=1; i < 12; i++) {
    when(filter.requestCountsPerIpAddress.get(clientIpAddress)).thenReturn(i);
    if (i>9) {
      assertThat(filter.isMaximumRequestsPerSecondExceeded("testIp"))
          .isTrue();
    }
  }
}

解决思路

1. 重构Filter,通过构造注入解耦缓存

原来的Filter在构造方法里直接实例化Caffeine缓存,导致无法替换为mock对象。修改成构造注入的方式,同时保留无参构造供Spring容器使用:

@Component
public class RequestFilter implements Filter {

  @Value("${chatGPT.max.request.per.second}")
  private int MAX_REQUESTS_PER_SECOND;

  private final LoadingCache<String, Integer> requestCountsPerIpAddress;

  // 新增带参数的构造方法,用于测试注入mock缓存
  public RequestFilter(LoadingCache<String, Integer> requestCountsPerIpAddress) {
    this.requestCountsPerIpAddress = requestCountsPerIpAddress;
  }

  // 保留无参构造,供Spring自动装配
  public RequestFilter() {
    this.requestCountsPerIpAddress = Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.SECONDS)
        .build(key -> 0);
  }

  // 其余方法保持不变...
}

2. 编写单元测试,mock LoadingCache

使用Mockito创建缓存的mock实例,注入到Filter中,同时手动设置阈值参数:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;
import com.github.benmanes.caffeine.cache.LoadingCache;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class RequestFilterTest {

  @Mock
  private LoadingCache<String, Integer> mockRequestCache;

  @Test
  @DisplayName("请求数超过阈值时触发限流")
  void requestFilterMaxRequestExceededTest() {
    // 初始化Filter,传入mock缓存
    RequestFilter filter = new RequestFilter(mockRequestCache);
    // 手动设置限流阈值(因为@Value在单元测试中不会自动注入)
    filter.MAX_REQUESTS_PER_SECOND = 9;
    String clientIp = "testIp";

    for (int i = 1; i < 12; i++) {
      // mock缓存的get方法返回当前循环的请求数
      when(mockRequestCache.get(clientIp)).thenReturn(i);
      
      boolean isExceeded = filter.isMaximumRequestsPerSecondExceeded(clientIp);
      
      if (i > 9) {
        assertThat(isExceeded).isTrue();
      } else {
        assertThat(isExceeded).isFalse();
      }
    }
  }
}

3. 额外提示

  • 如果不想修改Filter构造方法,也可以用反射直接设置requestCountsPerIpAddress私有字段为mock对象,但构造注入是更优雅的解耦方案,利于长期维护。
  • 原Filter的限流逻辑有个小细节:当请求数等于阈值时,仍会允许请求并计数+1。如果预期是阈值为9时,第9次请求就触发限流,需要把判断条件从requests > MAX_REQUESTS_PER_SECOND改成requests >= MAX_REQUESTS_PER_SECOND。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 15:32:16