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

串联运行测试时,作用域代理Bean的@MockBean似乎被忽略

问题分析与解决方案

你遇到的问题本质是会话作用域Bean的代理机制和@WebMvcTest测试环境的特殊行为之间的冲突,导致测试串联运行时,代理有时会实例化真实的MyScopedBean而非使用你定义的@MockBean。

为什么会出现这种情况?

  1. 会话作用域的代理逻辑:你的scopedBean配置了ScopedProxyMode.TARGET_CLASS,Spring会生成一个CGLIB代理类。在正常运行环境中,这个代理会从当前HTTP会话中获取Bean实例;但在@WebMvcTest的精简测试环境中,默认没有真实的HTTP会话上下文。当代理无法找到会话中的Bean时,它会尝试创建一个真实的MyScopedBean实例,而不是使用你定义的Mock。
  2. 测试上下文复用:Spring Boot测试默认会复用配置相同的测试上下文,当多个测试方法共享同一上下文时,@MockBean的存根可能被覆盖,或者代理的目标实例在测试之间发生了切换,导致部分测试中代理指向了真实Bean。
  3. @MockBean的替换逻辑:@MockBean会替换Spring上下文中的目标Bean,但对于带代理的作用域Bean,它只能替换代理的目标对象,而代理本身的作用域逻辑仍然存在。如果代理在测试中无法找到Mock实例(比如没有会话上下文),就会 fallback 到创建真实Bean。

具体解决方案

方案1:为测试添加会话上下文 + 重置Mock状态

在每个测试方法中,显式为MockMvc请求添加HTTP会话,同时在测试前重置Mock的存根,确保测试之间的状态隔离:

@WebMvcTest(MyController.class)
@Import({ActualValidator.class, SecurityConfig.class})
class MyControllerTest {
    @MockBean
    private MyUserDetailsService userDetailsService;
    @MockBean
    private ISomeService someService;
    @MockBean
    private MyScopedBean scopedBean;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        // 重置Mock,清除上一个测试的存根
        Mockito.reset(scopedBean);
    }

    @Test
    void testA() throws Exception {
        when(scopedBean.getAValue()).thenReturn("myvalue");
        when(scopedBean.matches(any(UUID.class))).thenReturn(false);

        mockMvc.perform(get("/")
                // 显式添加会话上下文,让代理能找到Mock实例
                .sessionAttr("scopedBean", scopedBean)
                .param("id", UUID.randomUUID().toString()))
            .andExpect(view().name("something"));
    }

    @Test
    void testB() throws Exception {
        when(scopedBean.getAnotherValue()).thenReturn("othervalue");
        when(scopedBean.matches(any(UUID.class))).thenReturn(true);

        mockMvc.perform(get("/")
                .sessionAttr("scopedBean", scopedBean)
                .param("id", UUID.randomUUID().toString()))
            .andExpect(view().name("somethingelse"));
    }
}

方案2:显式定义带代理的Mock Bean(推荐)

创建一个测试配置类,直接定义带会话作用域和代理的Mock Bean,让Spring直接使用这个代理后的Mock,避免 fallback 到真实Bean:

@TestConfiguration
public class TestScopedBeanConfig {
    @Bean(name = "scopedBean")
    @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public MyScopedBean scopedBean() {
        // 直接创建Mock并包装在会话作用域代理中
        return Mockito.mock(MyScopedBean.class);
    }
}

然后在测试类中导入这个配置,移除原来的@MockBean MyScopedBean:

@WebMvcTest(MyController.class)
@Import({ActualValidator.class, SecurityConfig.class, TestScopedBeanConfig.class})
class MyControllerTest {
    @MockBean
    private MyUserDetailsService userDetailsService;
    @MockBean
    private ISomeService someService;

    // 现在直接注入代理后的Mock Bean
    @Autowired
    private MyScopedBean scopedBean;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        Mockito.reset(scopedBean);
    }

    // 测试方法和之前一样,不需要额外添加sessionAttr(代理会处理会话逻辑)
    @Test
    void testA() throws Exception {
        when(scopedBean.getAValue()).thenReturn("myvalue");
        when(scopedBean.matches(any(UUID.class))).thenReturn(false);

        mockMvc.perform(get("/")
                .param("id", UUID.randomUUID().toString()))
            .andExpect(view().name("something"));
    }
}

方案3:强制每个测试重建上下文(兜底方案)

如果上述方案都不生效,可以用@DirtiesContext让每个测试方法都重新创建上下文,彻底隔离测试状态,但会牺牲测试速度:

@WebMvcTest(MyController.class)
@Import({ActualValidator.class, SecurityConfig.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class MyControllerTest {
    // 测试代码和原来一致
}

验证方法

你可以在控制器中继续打印scopedBean的toString,应用上述方案后,所有测试应该都会输出Mock的标识(比如MyScopedBean$MockitoMock$xxxxxx),而不再出现真实Bean的MyScopedBean(valueA=null, ...)格式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 17:07:37