串联运行测试时,作用域代理Bean的@MockBean似乎被忽略
问题分析与解决方案
你遇到的问题本质是会话作用域Bean的代理机制和@WebMvcTest测试环境的特殊行为之间的冲突,导致测试串联运行时,代理有时会实例化真实的MyScopedBean而非使用你定义的@MockBean。
为什么会出现这种情况?
- 会话作用域的代理逻辑:你的
scopedBean配置了ScopedProxyMode.TARGET_CLASS,Spring会生成一个CGLIB代理类。在正常运行环境中,这个代理会从当前HTTP会话中获取Bean实例;但在@WebMvcTest的精简测试环境中,默认没有真实的HTTP会话上下文。当代理无法找到会话中的Bean时,它会尝试创建一个真实的MyScopedBean实例,而不是使用你定义的Mock。 - 测试上下文复用:Spring Boot测试默认会复用配置相同的测试上下文,当多个测试方法共享同一上下文时,
@MockBean的存根可能被覆盖,或者代理的目标实例在测试之间发生了切换,导致部分测试中代理指向了真实Bean。 @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
相关产品推荐
相关产品推荐

