【问题标题】:What is the best way to unit-test SLF4J log messages?对 SLF4J 日志消息进行单元测试的最佳方法是什么?
【发布时间】:2011-06-06 17:15:40
【问题描述】:

我正在使用 slf4j,我想对我的代码进行单元测试,以确保在特定条件下生成警告/错误日志消息。我宁愿这些是严格的单元测试,所以我宁愿不必从文件中提取日志配置来测试是否生成了日志消息。我使用的模拟框架是 Mockito。

【问题讨论】:

  • 由于 SLF4J 只是其他日志记录实现的“门面”,你不能单独对其进行单元测试,你还必须指定你正在使用的实现。
  • @darioo - 不正确。我可以在我的类中添加一个 setter 以从测试中传入记录器,然后传入一个模拟的 Logger 实例并验证是否进行了适当的日志调用。我只是希望得到一个更优雅的解决方案,而不是添加一个仅用于测试并使我的 Logger 实例非最终的 set 方法。
  • 顺便说一句,一般优秀的“Growing Object Oriented Software”一书中有一章是关于日志单元测试的。它并不完全令人信服,但它肯定是经过深思熟虑的,值得一读 (amazon.co.uk/Growing-Object-Oriented-Software-Guided-Signature/…)
  • 嗨,我发现下面的文章mincong.io/2020/02/02/logback-test-logging-event 对您的问题很有帮助

标签: unit-testing mocking mockito slf4j


【解决方案1】:

为了在不依赖特定实现(例如 log4j)的情况下测试 slf4j,您可以提供自己的 slf4j 日志实现,如 this SLF4J FAQ 中所述。您的实现可以记录已记录的消息,然后由您的单元测试询问以进行验证。

slf4j-test 包正是这样做的。它是一个内存中的 slf4j 日志记录实现,提供检索记录的消息的方法。

【讨论】:

  • 使用lidalia的slf4j-test包的完整示例可以在这里找到:github.com/jaegertracing/jaeger-client-java/pull/378/files。诚然,他们的文档也非常棒。
  • 这不再适用于 slf4j-api 版本 1.8 或更高版本,因为:“计划 Jigsaw (Java 9)、slf4j-api 版本 1.8.x 及更高版本的出现使用 ServiceLoader 机制。早期版本SLF4J 依赖于 slf4j-api 不再支持的静态绑定器机制。” (见slf4j.org/codes.html)。
【解决方案2】:

创建测试规则:

    import ch.qos.logback.classic.Logger;
    import ch.qos.logback.classic.spi.ILoggingEvent;
    import ch.qos.logback.core.read.ListAppender;
    import org.junit.rules.TestRule;
    import org.junit.runner.Description;
    import org.junit.runners.model.Statement;
    import org.slf4j.LoggerFactory;
    
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class LoggerRule implements TestRule {
    
      private final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
      private final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
    
      @Override
      public Statement apply(Statement base, Description description) {
        return new Statement() {
          @Override
          public void evaluate() throws Throwable {
            setup();
            base.evaluate();
            teardown();
          }
        };
      }
    
      private void setup() {
        logger.addAppender(listAppender);
        listAppender.start();
      }
    
      private void teardown() {
        listAppender.stop();
        listAppender.list.clear();
        logger.detachAppender(listAppender);
      }
    
      public List<String> getMessages() {
        return listAppender.list.stream().map(e -> e.getMessage()).collect(Collectors.toList());
      }
    
      public List<String> getFormattedMessages() {
        return listAppender.list.stream().map(e -> e.getFormattedMessage()).collect(Collectors.toList());
      }
    
    }

然后使用它:

    @Rule
    public final LoggerRule loggerRule = new LoggerRule();
    
    @Test
    public void yourTest() {
        // ...
        assertThat(loggerRule.getFormattedMessages().size()).isEqualTo(2);
    }




-- 2021 年 10 月扩展的 JUnit 5 -----

日志捕获:

public class LogCapture {

  private ListAppender<ILoggingEvent> listAppender = new ListAppender<>();

  LogCapture() {
  }

  public String getFirstFormattedMessage() {
    return getFormattedMessageAt(0);
  }

  public String getLastFormattedMessage() {
    return getFormattedMessageAt(listAppender.list.size() - 1);
  }

  public String getFormattedMessageAt(int index) {
    return getLoggingEventAt(index).getFormattedMessage();
  }

  public LoggingEvent getLoggingEvent() {
    return getLoggingEventAt(0);
  }

  public LoggingEvent getLoggingEventAt(int index) {
    return (LoggingEvent) listAppender.list.get(index);
  }

  public List<LoggingEvent> getLoggingEvents() {
    return listAppender.list.stream().map(e -> (LoggingEvent) e).collect(Collectors.toList());
  }

  public void setLogFilter(Level logLevel) {
    listAppender.clearAllFilters();
    listAppender.addFilter(buildLevelFilter(logLevel));
  }

  public void clear() {
    listAppender.list.clear();
  }

  void start() {
    setLogFilter(Level.INFO);
    listAppender.start();
  }

  void stop() {
    if (listAppender == null) {
      return;
    }

    listAppender.stop();
    listAppender.list.clear();
    listAppender = null;
  }

  ListAppender<ILoggingEvent> getListAppender() {
    return listAppender;
  }

  private Filter<ILoggingEvent> buildLevelFilter(Level logLevel) {
    LevelFilter levelFilter = new LevelFilter();
    levelFilter.setLevel(logLevel);
    levelFilter.setOnMismatch(FilterReply.DENY);
    levelFilter.start();

    return levelFilter;
  }

}

LogCaptureExtension:

public class LogCaptureExtension implements ParameterResolver, AfterTestExecutionCallback {

  private Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);

  private LogCapture logCapture;

  @Override
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
    return parameterContext.getParameter().getType() == LogCapture.class;
  }

  @Override
  public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
    logCapture = new LogCapture();

    setup();

    return logCapture;
  }

  @Override
  public void afterTestExecution(ExtensionContext context) {
    teardown();
  }

  private void setup() {
    logger.addAppender(logCapture.getListAppender());
    logCapture.start();
  }

  private void teardown() {
    if (logCapture == null || logger == null) {
      return;
    }

    logger.detachAndStopAllAppenders();
    logCapture.stop();
  }

}

然后使用它:

@ExtendWith(LogCaptureExtension.class)
public class SomeTest {

  @Test
  public void sometest(LogCapture logCapture)  {
    // do test here

    assertThat(logCapture.getLoggingEvents()).isEmpty();
  }

  // ...
}

【讨论】:

  • 这是一个聪明的解决方案!
  • 这是一个非常酷的通用解决方案。我遇到了依赖地狱,不得不使用 slf4j-test 方法在数百个不同的地方排除 logback-classic,但这可以优雅地解决它,而无需诉诸反射或更改生产代码。
  • 任何 JUnit5 等价物?
【解决方案3】:

我认为您可以使用自定义附加程序解决您的问题。创建一个实现 org.apache.log4j.Appender 的测试 appender,并将你的 appender 设置在 log4j.properties 中,并在执行测试用例时加载它。

如果您从 appender 回调测试工具,您可以检查记录的消息

【讨论】:

  • 如能提供一些代码示例将不胜感激。
  • 但是这个例子似乎不适用于slf4j!
【解决方案4】:

一个更好的 SLF4J 测试实现在并发测试执行的环境中运行良好是https://github.com/portingle/slf4jtesting

我参与了一些关于 slf4j 日志测试以及现有测试方法在并发测试执行方面的局限性的讨论。

我决定把我的话写成代码,结果就是 git repo。

【讨论】:

  • 希望很快会有第三个答案出现在拥有更好的 SLF4j 测试实现的人那里...... ;)
  • Imo,这不是一个好的实用程序。它仅在 Logger 在构造函数中创建时才有效。但谁这样做呢? Logger 通常是一个常量:private static final Logger LOGGER = LoggerFactory.getLogger(Example.class)。该实用程序无法处理常量记录器,因此无法处理正常用例。
【解决方案5】:

对于 JUnit 5,在Create a test rule 中创建实现上述andrew-feng 提供的解决方案的扩展:

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.stream.Collectors;

public class LoggerExtension implements BeforeEachCallback, AfterEachCallback {

    private final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
    private final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);

    @Override
    public void afterEach(ExtensionContext extensionContext) throws Exception {
        listAppender.stop();
        listAppender.list.clear();
        logger.detachAppender(listAppender);
    }

    @Override
    public void beforeEach(ExtensionContext extensionContext) throws Exception {
        logger.addAppender(listAppender);
        listAppender.start();
    }

    public List<String> getMessages() {
        return listAppender.list.stream().map(e -> e.getMessage()).collect(Collectors.toList());
    }

    public List<String> getFormattedMessages() {
        return listAppender.list.stream().map(e -> e.getFormattedMessage()).collect(Collectors.toList());
    }

}

然后使用它:

@RegisterExtension
public LoggerExtension loggerExtension = new LoggerExtension();

@Test
public void yourTest() {
    // ...
    assertThat(loggerExtension.getFormattedMessages().size()).isEqualTo(2);
}

【讨论】:

  • 或者您可以在测试开始时使用以下语句: Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); ListAppender listAppender = new ListAppender(); logger.addAppender(listAppender); listAppender.start();然后在测试结束时断言: List logsList = listAppender.list; assertEquals(...,logsList.get(1))
【解决方案6】:

您可以将需要在他们自己的方法中测试的重要日志调用放置在您可以更轻松地模拟的方法中,而不是模拟 SLF4J。

如果你真的想模拟 SLF4J,我敢打赌你可以为它创建自己的提供程序,这将允许你从 SLF4J 端提供一个模拟记录器,而不是在你的服务对象中注入一个。

【讨论】:

    【解决方案7】:

    使用 slf4j-test 可以消除上面讨论的许多变通方法

    pom.xml

     <dependency>
           <groupId>uk.org.lidalia</groupId>
           <artifactId>slf4j-test</artifactId>
           <version>1.2.0</version>
     </dependency>
    

    示例类

    @Slf4j
    public class SampleClass {
    
        public void logDetails(){
            log.info("Logging");
        }
    }
    

    测试类

    import org.junit.Test;
    import uk.org.lidalia.slf4jtest.TestLogger;
    import uk.org.lidalia.slf4jtest.TestLoggerFactory;
    
    import static java.util.Arrays.asList;
    import static org.hamcrest.Matchers.is;
    import static org.junit.Assert.assertThat;
    import static uk.org.lidalia.slf4jtest.LoggingEvent.info;
    
    public class SampleClassTest {
    
        TestLogger logger = TestLoggerFactory.getTestLogger(SampleClass.class);
    
        @Test
        public void testLogging(){
            SampleClass sampleClass = new SampleClass();
            //Invoke slf4j logger
            sampleClass.logDetails();
    
            assertThat(logger.getLoggingEvents(), is(asList(info("Logging"))));
    
        }
    
    }
    

    更多详情请参考http://projects.lidalia.org.uk/slf4j-test/

    【讨论】:

    • 为了使它工作,用户应该添加以下行:``` ImmutableList loggingEvents = logger.getLoggingEvents(); ImmutableList arguments = loggingEvents.get(0).getArguments();字符串实际 = MessageFormatter.arrayFormat(loggingEvents.get(0).getMessage(), arguments.toArray()).getMessage(); ```
    【解决方案8】:

    与@Zsolt 类似,您可以模拟log4j Appender 并将其设置在Logger 上,然后验证对Appender.doAppend() 的调用。这使您无需修改​​真实代码即可进行测试。

    【讨论】:

    • 我不知道为什么这被否决了。这正是 Mockito 的用途,即模拟难以测试的合作者。这个答案已经存活了近 6 年而没有被否决。
    【解决方案9】:

    您可以尝试使用另一个库来支持轻松模拟 slf4j 记录器 - slf4j-mock,您的代码应该如下所示:

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.InjectMocks;
    import org.mockito.Mock;
    import org.mockito.Mockito;
    import org.mockito.junit.MockitoJUnitRunner;
    import org.slf4j.Logger;
    
    @RunWith(MockitoJUnitRunner.class)
    public class JUnit4ExampleTest {
    
        private static final String INFO_TEST_MESSAGE = "info log test message from JUnit4";
    
        @Mock
        Logger logger;
    
        @InjectMocks
        Example sut;
    
        @Test
        public void logInfoShouldBeLogged() {
    
            // when
            sut.methodWithLogInfo(INFO_TEST_MESSAGE);
    
            // then
            Mockito.verify(logger).info(INFO_TEST_MESSAGE);
            Mockito.verifyNoMoreInteractions(logger);
        }
    }
    

    如您所见,在测试代码中不需要任何特殊步骤。您只需在项目中添加对库的依赖即可。

    更多示例和说明:

    https://www.simplify4u.org/slf4j-mock/

    【讨论】:

      【解决方案10】:

      我有一个新答案,我将在这篇文章的顶部发布(我的“旧”答案仍在这篇文章的底部)(在撰写本文时,我的“旧”答案是“0”,所以没有伤害,没有犯规!)

      较新的答案:

      这是 Gradle 包:

        testImplementation 'com.portingle:slf4jtesting:1.1.3'
      

      Maven 链接:

      https://mvnrepository.com/artifact/com.portingle/slf4jtesting

      德语代码:

      (下面的导入和私有方法将进入 MyTestClass(.java))

      import static org.junit.Assert.assertNotNull;
      
      import slf4jtest.LogLevel;
      import slf4jtest.Settings;
      import slf4jtest.TestLogger;
      import slf4jtest.TestLoggerFactory;
      
      
      
      @Test
      public void myFirstTest() {
      
      
          org.slf4j.Logger unitTestLogger = this.getUnitTestLogger();
          ISomethingToTestObject testItem = new SomethingToTestObject (unitTestLogger);
          SomeReturnObject obj = testItem.myMethod("arg1");
          assertNotNull(wrapper);
      
          /* now here you would find items in the unitTestLogger */
      
          assertContains(unitTestLogger, LogLevel.DebugLevel, "myMethod was started");
      
      }
      
      // render nicer errors
      private void assertContains(TestLogger unitTestLogger, LogLevel logLev, String expected) throws Error {
          if (!unitTestLogger.contains(logLev, expected)) {
              throw new AssertionError("expected '" + expected + "' but got '" + unitTestLogger.lines() + "'");
          }
      }
      
      // render nicer errors
      private void assertNotContains(TestLogger unitTestLogger, LogLevel logLev, String expected) throws Error {
          if (unitTestLogger.contains(logLev, expected)) {
              throw new AssertionError("expected absence of '" + expected + "' but got '" + unitTestLogger.lines() + "'");
          }
      }
      
      
      
          private TestLogger getUnitTestLogger() {
              TestLoggerFactory loggerFactory = Settings.instance()
                      .enableAll() // necessary as by default only ErrorLevel is enabled
                      .buildLogging();
      
              TestLogger returnItem = loggerFactory.getLogger(MyTestClasss.class.getName());
              assertNotNull(returnItem);
              return returnItem;
          }
      

      ==============================下面的旧答案..不要使用========== ======

      以下是我之前的回答。在我发现它(上面的包)之后,我改变了我的下面的代码......以使用上面的包。

      So here is my method.
      
      First, I allow the logger to be injected.  But I provide a default as well:
      
      ```java
      package com.mycompany.myproject;
      
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      public class MyCoolClass { //implements IMyCoolClass {
      
          private static final String PROCESS_STARTED = "Process started. (key='%1$s')";
      
          private final Logger logger;
      
          public MyCoolClass() {
              this(LoggerFactory.getLogger(MyCoolClass.class));
          }
      
          public MyCoolClass(Logger lgr) {
              this.logger = lgr;
          }
      
          public doSomething(int key)
          {
              logger.info(String.format(PROCESS_STARTED, key));
              /*now go do something */
          }
      }
      
      
      Then I wrote a very basic in memory logger
      
      
      ```java
      import org.slf4j.Marker;
      
      import java.util.ArrayList;
      import java.util.Collection;
      
      public class InMemoryUnitTestLogger implements org.slf4j.Logger {
      
          public Collection<String> informations = new ArrayList<String>();
          public Collection<String> errors = new ArrayList<String>();
          public Collection<String> traces = new ArrayList<String>();
          public Collection<String> debugs = new ArrayList<>();
          public Collection<String> warns = new ArrayList<>();
      
          public Collection<String> getInformations() {
              return informations;
          }
      
          public Collection<String> getErrors() {
              return errors;
          }
      
          public Collection<String> getTraces() {
              return traces;
          }
      
          public Collection<String> getDebugs() {
              return debugs;
          }
      
          public Collection<String> getWarns() {
              return warns;
          }
      
      
          @Override
          public String getName() {
              return "FakeLoggerName";
          }
      
          @Override
          public boolean isTraceEnabled() {
              return false;
          }
      
          @Override
          public boolean isTraceEnabled(Marker marker) {
              return false;
          }
      
          @Override
          public boolean isDebugEnabled() {
              return false;
          }
      
          @Override
          public boolean isDebugEnabled(Marker marker) {
              return false;
          }
      
          @Override
          public boolean isWarnEnabled(Marker marker) {
              return false;
          }
      
          @Override
          public boolean isInfoEnabled(Marker marker) {
              return false;
          }
      
          @Override
          public boolean isWarnEnabled() {
              return false;
          }
      
          @Override
          public boolean isErrorEnabled(Marker marker) {
              return false;
          }
      
          @Override
          public boolean isInfoEnabled() {
              return false;
          }
      
          @Override
          public boolean isErrorEnabled() {
              return false;
          }
      
          @Override
          public void trace(String s) {
              this.internalTrace(s);
          }
      
          @Override
          public void trace(String s, Object o) {
              this.internalTrace(s);
          }
      
          @Override
          public void trace(String s, Object o, Object o1) {
              this.internalTrace(s);
          }
      
          @Override
          public void trace(String s, Object... objects) {
              this.internalTrace(s);
          }
      
          @Override
          public void trace(String s, Throwable throwable) {
              this.internalTrace(s);
          }
      
      
          @Override
          public void trace(Marker marker, String s) {
              this.internalTrace(s);
          }
      
          @Override
          public void trace(Marker marker, String s, Object o) {
              this.internalTrace(s);
          }
      
          @Override
          public void trace(Marker marker, String s, Object o, Object o1) {
              this.internalTrace(s);
          }
      
          @Override
          public void trace(Marker marker, String s, Object... objects) {
              this.internalTrace(s);
          }
      
          @Override
          public void trace(Marker marker, String s, Throwable throwable) {
              this.internalTrace(s);
          }
      
          @Override
          public void debug(String s) {
              this.internalDebug(s);
          }
      
          @Override
          public void debug(String s, Object o) {
              this.internalDebug(s);
          }
      
          @Override
          public void debug(String s, Object o, Object o1) {
              this.internalDebug(s);
          }
      
          @Override
          public void debug(String s, Object... objects) {
              this.internalDebug(s);
          }
      
          @Override
          public void debug(String s, Throwable throwable) {
              this.internalDebug(s);
          }
      
          @Override
          public void debug(Marker marker, String s) {
              this.internalDebug(s);
          }
      
          @Override
          public void debug(Marker marker, String s, Object o) {
              this.internalDebug(s);
          }
      
          @Override
          public void debug(Marker marker, String s, Object o, Object o1) {
              this.internalDebug(s);
          }
      
          @Override
          public void debug(Marker marker, String s, Object... objects) {
              this.internalDebug(s);
          }
      
          @Override
          public void debug(Marker marker, String s, Throwable throwable) {
              this.internalDebug(s);
          }
      
          public void info(String s) {
              this.internalInfo(s);
          }
      
          @Override
          public void info(String s, Object o) {
              this.internalInfo(s);
          }
      
          @Override
          public void info(String s, Object o, Object o1) {
              this.internalInfo(s);
          }
      
          @Override
          public void info(String s, Object... objects) {
              this.internalInfo(s);
          }
      
          @Override
          public void info(String s, Throwable throwable) {
              this.internalInfo(s);
          }
      
          @Override
          public void info(Marker marker, String s) {
              this.internalInfo(s);
          }
      
          @Override
          public void info(Marker marker, String s, Object o) {
              this.internalInfo(s);
          }
      
          @Override
          public void info(Marker marker, String s, Object o, Object o1) {
              this.internalInfo(s);
          }
      
          @Override
          public void info(Marker marker, String s, Object... objects) {
              this.internalInfo(s);
          }
      
          @Override
          public void info(Marker marker, String s, Throwable throwable) {
              this.internalInfo(s);
          }
      
          public void error(String s) {
              this.internalError(s);
          }
      
          @Override
          public void error(String s, Object o) {
              this.internalError(s);
          }
      
          @Override
          public void error(String s, Object o, Object o1) {
              this.internalError(s);
          }
      
          @Override
          public void error(String s, Object... objects) {
              this.internalError(s);
          }
      
          @Override
          public void error(String s, Throwable throwable) {
              this.internalError(s);
          }
      
          @Override
          public void error(Marker marker, String s) {
              this.internalError(s);
          }
      
          @Override
          public void error(Marker marker, String s, Object o) {
              this.internalError(s);
          }
      
          @Override
          public void error(Marker marker, String s, Object o, Object o1) {
              this.internalError(s);
          }
      
          @Override
          public void error(Marker marker, String s, Object... objects) {
              this.internalError(s);
          }
      
          @Override
          public void error(Marker marker, String s, Throwable throwable) {
              this.internalError(s);
          }
      
          public void warn(String s) {
              this.internalWarn(s);
          }
      
          @Override
          public void warn(String s, Object o) {
              this.internalWarn(s);
          }
      
          @Override
          public void warn(String s, Object... objects) {
              this.internalWarn(s);
          }
      
          @Override
          public void warn(String s, Object o, Object o1) {
              this.internalWarn(s);
          }
      
          @Override
          public void warn(String s, Throwable throwable) {
              this.internalWarn(s);
          }
      
          @Override
          public void warn(Marker marker, String s) {
              this.internalWarn(s);
          }
      
          @Override
          public void warn(Marker marker, String s, Object o) {
              this.internalWarn(s);
          }
      
          @Override
          public void warn(Marker marker, String s, Object o, Object o1) {
              this.internalWarn(s);
          }
      
          @Override
          public void warn(Marker marker, String s, Object... objects) {
              this.internalWarn(s);
          }
      
          @Override
          public void warn(Marker marker, String s, Throwable throwable) {
              this.internalWarn(s);
          }
      
          private void internalDebug(String s) {
              System.out.println(s);
              this.debugs.add(s);
          }
      
          private void internalInfo(String msg) {
              System.out.println(msg);
              this.informations.add(msg);
          }
      
          private void internalTrace(String msg) {
              //??System.out.println(msg);
              this.traces.add(msg);
          }
      
      
          private void internalWarn(String msg) {
              System.err.println(msg);
              this.warns.add(msg);
          }
      
          private void internalError(String msg) {
              System.err.println(msg);
              this.errors.add(msg);
          }
      

      然后在我的单元测试中,我可以做以下两件事之一:

      private ByteArrayOutputStream setupSimpleLog(Logger lgr) {
          ByteArrayOutputStream pipeOut = new ByteArrayOutputStream();
          PrintStream pipeIn = new PrintStream(pipeOut);
          System.setErr(pipeIn);
          return pipeOut;
      }
      
      private Logger getSimpleLog() {
          Logger lgr = new InMemoryUnitTestLogger();
          return lgr;
      }
      
      
      private void myTest() {
      
      
          Logger lgr = getSimpleLog();
          ByteArrayOutputStream pipeOut = this.setupSimpleLog(lgr);
      
          MyCoolClass testClass = new MyCoolClass(lgr);
          int myValue = 333;
          testClass.doSomething(myValue);
      
          String findMessage = String.format(MyCoolClass.PROCESS_STARTED, myValue);
          String output = new String(pipeOut.toByteArray());
          assertTrue(output.contains(findMessage));
      }
      

      或与上述类似,但在自定义 Logger 上进行强制转换

      private void myTest() {
      
      
          Logger lgr = getSimpleLog();
          MyCoolClass testClass = new MyCoolClass(lgr);
          int myValue = 333;
          testClass.doSomething(myValue);
      
          String findMessage = String.format(MyCoolClass.PROCESS_STARTED, myValue);
          InMemoryUnitTestLogger castLogger = (InMemoryUnitTestLogger)lgr;
          /* now check the exact subcollection for the message) */
          assertTrue(castLogger.getInfos().contains(findMessage));
      }
      

      对代码持保留态度,想法就在那里。我没有编译代码。

      【讨论】:

        【解决方案11】:

        我知道这个问题发布已经有一段时间了,但我刚刚遇到了一个类似的问题,我的解决方案可能会有所帮助。按照@Zsolt 提出的解决方案,我们使用了一个appender,更具体地说是Logback 的ListAppender。在此处显示代码和配置(Groovy 代码,但可以轻松移植到 Java):

        用于日志访问的 Groovy 类:

        import ch.qos.logback.classic.Logger
        import ch.qos.logback.classic.spi.LoggingEvent
        import ch.qos.logback.core.read.ListAppender
        import org.slf4j.LoggerFactory
        
        class LogAccess {
        
            final static String DEFAULT_PACKAGE_DOMAIN = Logger.ROOT_LOGGER_NAME
            final static String DEFAULT_APPENDER_NAME = 'LIST'
            final List<LoggingEvent> list
        
            LogAccess(String packageDomain = DEFAULT_PACKAGE_DOMAIN, String appenderName = DEFAULT_APPENDER_NAME) {
                Logger logger = (Logger) LoggerFactory.getLogger(packageDomain)
                ListAppender<LoggingEvent> appender = logger.getAppender(appenderName) as ListAppender<LoggingEvent>
                if (appender == null) {
                    throw new IllegalStateException("'$DEFAULT_APPENDER_NAME' appender not found. Did you forget to add 'logback.xml' to the resources folder?")
                }
                this.list = appender.list
                this.clear()
            }
        
            void clear() {
                list.clear()
            }
        
            boolean contains(String logMessage) {
                return list.reverse().any { it.getFormattedMessage() == logMessage }
            }
        
            @Override
            String toString() {
                list.collect { it. getFormattedMessage() }
            }
        }
        
        

        示例 logback.xml 配置:

        <?xml version="1.0" encoding="UTF-8"?>
        <configuration>
            <!-- These 2 'includes' tags ensure regular springboot console logging works as usual -->
            <!-- See https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-configure-logback-for-logging -->
            <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
            <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
            <appender name="LIST" class="ch.qos.logback.core.read.ListAppender"/>
            <root level="INFO">
                <appender-ref ref="CONSOLE" />
                <appender-ref ref="LIST" />
            </root>
        </configuration>
        

        测试:

        LogAccess log = new LogAccess()
        def expectedLogEntry = 'Expected Log Entry'
        assert !log.contains(expectedLogEntry)
        methodUnderTest()
        assert log.contains(expectedLogEntry)
        

        我在带有 Groovy+Spock 的 SpringBoot 项目中使用它,但我不明白为什么这在任何带有 Logback 的 Java 项目中都不起作用。

        【讨论】:

          【解决方案12】:

          只需使用普通的 Mockito 和一些反射逻辑来模拟它:

          // Mock the Logger
          Logger mock = Mockito.mock(Logger.class);
          // Set the Logger to the class you want to test. 
          // Since this is often a private static field you have to 
          // hack a little bit: (Solution taken from https://stackoverflow.com/a/3301720/812093)
          setFinalStatic(ClassBeeingTested.class.getDeclaredField("log"), mock);
          

          使用 setFinalStatic 方法蜜蜂

          public static void setFinalStatic(Field field, Object newValue) throws Exception {
              field.setAccessible(true);
          
              Field modifiersField = Field.class.getDeclaredField("modifiers");
              modifiersField.setAccessible(true);
              modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
          
              field.set(null, newValue);
           }    
          

          然后只需执行要测试的代码并验证 - 例如下面验证 Logger.warn 方法被调用了两次:

              ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
              Mockito.verify(mock,Mockito.atLeastOnce()).warn(argumentCaptor.capture());
              List<String> allValues = argumentCaptor.getAllValues();
              assertEquals(2, allValues.size());
              assertEquals("myFirstExpectedMessage", allValues.get(0));
              assertEquals("mySecondExpectedMessage", allValues.get(1));
          

          请注意,通过反射设置最终字段并非在所有情况下都有效。例如,如果多个测试用例试图修改它,我就无法让它工作。

          【讨论】:

            【解决方案13】:

            in this groovy answercomment 之前已经提到过这个解决方案,但由于我不认为它本身就是一个答案,所以在此处添加它作为社区 wiki 答案。

            所以使用 logback listappender 的 JUnit5 解决方案:

            import static org.assertj.core.api.Assertions.assertThat;
            
            import ch.qos.logback.classic.Logger;
            import ch.qos.logback.classic.spi.ILoggingEvent;
            import ch.qos.logback.core.read.ListAppender;
            import org.junit.jupiter.api.BeforeEach;
            import org.junit.jupiter.api.Test;
            import org.slf4j.LoggerFactory;
            
            public class LoggingTest {
              private final ClassToTest sut = new ClassToTest();
            
              private ListAppender<ILoggingEvent> listAppender;
            
              @BeforeEach
              void init() {
                final var log = (Logger) LoggerFactory.getLogger(ClassToTest.class);
            
                listAppender = new ListAppender<>();
                listAppender.start();
            
                log.addAppender(listAppender);
              }
            
              @Test
              public void testLogging() {
                sut.doSomethingThatLogs()
                String message = listAppender.list.get(0).getFormattedMessage();
                assertThat(message).contains("this message should be logged");
              }
            }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2019-02-12
              • 1970-01-01
              • 2010-09-07
              • 1970-01-01
              • 2010-09-27
              • 2019-03-20
              • 2010-09-05
              相关资源
              最近更新 更多