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

如何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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 15:47:37