对SLF4J日志消息进行单元测试的最佳方法是什么?

时间:2011-01-10 18:45:04

标签: unit-testing mocking mockito slf4j

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

12 个答案:

答案 0 :(得分:17)

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

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

答案 1 :(得分:10)

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

如果您从appender回拨测试工具,则可以查看已记录的消息

答案 2 :(得分:8)

SLF4J的更好的测试实现在并发测试执行的环境中非常有效https://github.com/portingle/slf4jtesting

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

我决定将我的话放入代码中,结果就是git repo。

答案 3 :(得分:5)

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

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);
}

答案 4 :(得分:2)

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

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

答案 5 :(得分:1)

创建测试规则:

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);
}

答案 6 :(得分:1)

使用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");
    }
}

TestClass

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/以获取更多详细信息

答案 7 :(得分:0)

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

答案 8 :(得分:0)

这是我的方法。

首先,我允许注入记录器。但是我也提供了默认值:

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 */
    }
}

然后我在内存记录器中写了一个非常基本的内容

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));
}

带着一粒盐拿代码,想法就在那里。我没有编译代码。

答案 9 :(得分:0)

我知道这个问题发布已经有一段时间了,但是我遇到了类似的问题,我的解决方案可能会有所帮助。按照@Zsolt提出的解决方案,我们使用附加器,更具体地说是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项目中都行不通。

答案 10 :(得分:0)

只需使用普通的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方法beeing

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));

请注意,并非在所有情况下都无法通过反射设置最终字段。例如,如果多个测试用例试图对其进行修改,我将无法使其正常运行。

答案 11 :(得分:0)

您可以尝试使用另一个库来支持简单的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/