如何Mock@Slf4j注解生成的Logger?Mockito v4+JUnit5下日志验证失败问题排查
问题原因分析
你的测试失败核心原因是**MockedStatic<LoggerFactory>的生命周期太短**。在@BeforeAll的try-with-resources块中创建的静态mock对象,会在setUpLogger()方法执行完毕后自动关闭(这是try-with-resources的特性)。等到测试方法testSlf4j()执行时,LoggerFactory.getLogger()已经不再返回你mock的logger实例,LoggerEx里的log字段还是使用真实的SLF4J Logger,自然不会和你的mock logger产生任何交互。
解决方案1:延长静态Mock的生命周期到测试方法执行期间
把MockedStatic<LoggerFactory>作为测试类的字段,在@BeforeEach中初始化,@AfterEach中关闭,确保整个测试过程中LoggerFactory的mock一直生效:
public class LoggerExTest { // 建议避免用Test作为类名,防止和JUnit注解混淆 @Mock private Logger logger; @InjectMocks private LoggerEx exceptionLogger; private AutoCloseable closeable; private MockedStatic<LoggerFactory> loggerFactoryMock; // 类级别静态mock字段 @Mock private LogEntryFactory mockLogEntryFactory; @Mock private HttpServletRequest mockHttpServletRequest; @Mock private LogEntry mockLogEntry; @BeforeEach public void setup() { closeable = MockitoAnnotations.openMocks(this); // 初始化LoggerFactory的静态mock,绑定到类字段 loggerFactoryMock = mockStatic(LoggerFactory.class); // 精准指定要mock的类,避免影响其他Logger初始化 loggerFactoryMock.when(() -> LoggerFactory.getLogger(LoggerEx.class)) .thenReturn(logger); when(mockLogEntryFactory.createLogEntry(eq("SomeString"))) .thenReturn(mockLogEntry); when(mockHttpServletRequest.getAttribute(eq("SomeRequestId"))) .thenReturn("requestId"); // 其他初始化代码... } @AfterEach public void cleanUp() throws Exception { closeable.close(); loggerFactoryMock.close(); // 手动关闭静态mock } @Test public void testSlf4j() { final ExpectedException e = new PersonalizedNotFoundException(); exceptionLogger.logException((Exception) e, HttpStatus.NOT_FOUND); final String expectedLogMsg = "Error causing 4xx response - statusCode:404"; verify(logger, times(1)).info(expectedLogMsg, (Exception) e); } }
关键注意点:
- 不要用
@BeforeAll初始化静态mock:LoggerEx的log是静态字段,会在类加载时初始化,必须在测试对象准备好后、测试方法执行前覆盖LoggerFactory的行为,@BeforeEach是最合适的时机。 - 精准指定目标类:用
LoggerFactory.getLogger(LoggerEx.class)代替any(Class.class),避免意外mock其他类的Logger。
解决方案2:通过反射直接替换静态log字段
Lombok的@Slf4j会生成private static final Logger log字段,我们可以通过反射绕过final修饰符,直接把mock的Logger注入进去:
public class LoggerExTest { @Mock private Logger logger; @InjectMocks private LoggerEx exceptionLogger; private AutoCloseable closeable; @Mock private LogEntryFactory mockLogEntryFactory; @Mock private HttpServletRequest mockHttpServletRequest; @Mock private LogEntry mockLogEntry; @BeforeEach public void setup() throws NoSuchFieldException, IllegalAccessException { closeable = MockitoAnnotations.openMocks(this); // 反射获取LoggerEx的静态log字段 Field logField = LoggerEx.class.getDeclaredField("log"); logField.setAccessible(true); // 绕过final修饰符限制 Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(logField, logField.getModifiers() & ~Modifier.FINAL); // 注入mock的logger logField.set(null, logger); when(mockLogEntryFactory.createLogEntry(eq("SomeString"))) .thenReturn(mockLogEntry); when(mockHttpServletRequest.getAttribute(eq("SomeRequestId"))) .thenReturn("requestId"); // 其他初始化代码... } @AfterEach public void cleanUp() throws Exception { closeable.close(); } @Test public void testSlf4j() { final ExpectedException e = new PersonalizedNotFoundException(); exceptionLogger.logException((Exception) e, HttpStatus.NOT_FOUND); final String expectedLogMsg = "Error causing 4xx response - statusCode:404"; verify(logger, times(1)).info(expectedLogMsg, (Exception) e); } }
关键注意点:
- 这种方法依赖Lombok生成的字段名
log,虽然Lombok变更该字段名的概率极低,但如果后续有版本更新需要注意适配。 - 反射操作需要处理异常,代码相对繁琐,但不需要管理静态mock的生命周期。
额外小建议
- 验证日志时可以用
ArgumentMatchers.eq()明确匹配参数,比如verify(logger, times(1)).info(eq(expectedLogMsg), eq(e));,避免因参数匹配逻辑模糊导致的验证失败。
内容的提问来源于stack exchange,提问作者Guru_Clef
相关产品推荐
相关产品推荐

