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

发布于 2024-10-11 10:29:49 字数 116 浏览 8 评论 0原文

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

I'm using slf4j and I want to unit test my code to make sure that warn/error log messages are generated under certain conditions. I'd rather these be strict unit tests, so I'd prefer not to have to pull up logging configuration from a file in order to test that the log messages are generated. The mocking framework I'm using is Mockito.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(19

清旖 2024-10-18 10:29:50

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

Similar to @Zsolt, you can mock log4j Appender and set it on the Logger, then verify calls to Appender.doAppend(). This allows you to test without having to modify the real code.

眼趣 2024-10-18 10:29:50

您可以尝试另一个库来支持简单的模拟 slf4j 记录器 - slf4j-mock,您的代码 cen看起来像:

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/

You can try another library to support easy mocking slf4j loggers - slf4j-mock, your code cen be look as:

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

As you see you don't need any special steps, in test code. You need only add dependency to library in your project.

More examples and instructions at:

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

明明#如月 2024-10-18 10:29:50

这是我的实现(Java 11+,Junit 5,在 ClassWithALogger 中使用 Lombok 的 @Slf4j),我的要求是在测试之间重置记录器。

// because the specific imports matter:
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.slf4j.LoggerFactory;
import static org.assertj.core.api.Assertions.assertThat;

private ListAppender<ILoggingEvent> listAppender;
private final Logger staticLogger = (Logger) LoggerFactory.getLogger(ClassWithALogger.class);

@BeforeEach
public void init() {
    listAppender = new ListAppender<>();
    listAppender.start();
    staticLogger.addAppender(listAppender);
}

@AfterEach
public void tearDown() {
    staticLogger.detachAppender(listAppender);
    listAppender.stop();
}

@Test
void myTest() {
    // given
    // when
    // then
    Set<String> messages = listAppender.list.stream()
            .map(ILoggingEvent::getFormattedMessage)
            .collect(Collectors.toSet());
    assertThat(messages).contains("the message I expect to be logged");
}

This is my implementation (Java 11+, Junit 5, using Lombok's @Slf4j in the ClassWithALogger), my requirements being for the logger to be reset between tests.

// because the specific imports matter:
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.slf4j.LoggerFactory;
import static org.assertj.core.api.Assertions.assertThat;

private ListAppender<ILoggingEvent> listAppender;
private final Logger staticLogger = (Logger) LoggerFactory.getLogger(ClassWithALogger.class);

@BeforeEach
public void init() {
    listAppender = new ListAppender<>();
    listAppender.start();
    staticLogger.addAppender(listAppender);
}

@AfterEach
public void tearDown() {
    staticLogger.detachAppender(listAppender);
    listAppender.stop();
}

@Test
void myTest() {
    // given
    // when
    // then
    Set<String> messages = listAppender.list.stream()
            .map(ILoggingEvent::getFormattedMessage)
            .collect(Collectors.toSet());
    assertThat(messages).contains("the message I expect to be logged");
}
一向肩并 2024-10-18 10:29:50

只需使用普通的 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));

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

Just use plain Mockito and some reflection logic to mock it:

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

with setFinalStatic method 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);
 }    

Then just execute the to be tested code and verify - e.g. the following verifies that the Logger.warn method was called twice:

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

Please note that setting the final fields via reflection does not work in all cases. I was for example not able to get it working if multiple testcases were trying to modify it.

夜灵血窟げ 2024-10-18 10:29:50

我需要在不使用 ch.qos.logback 的情况下让它工作,因为这与我的主类中 log4j2 的使用相冲突。

https://codingcraftsman 致敬。 wordpress.com/2015/04/28/log4j2-mocking-with-mockito-and-junit/ 展示如何做到这一点。

对我来说改变的事情是将模拟中的记录器从 org.apache.logging.log4j.core.Logger 转换为记录器,我能够向它添加一个附加程序,但它没有不要破坏我的其余代码。

将他的代码粘贴到此处以保证答案的完整性。

import java.io.IOException;
  
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
  
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class ExceptionLoggerTest {
  @Mock
  private Appender mockAppender;
  
  private List<LogEvent> capturedEvents = new ArrayList<>();
  
  private Logger logger;
  
  @Before
  public void setup() {
    // prepare the appender so Log4j likes it
    when(mockAppender.getName()).thenReturn("MockAppender");
    when(mockAppender.isStarted()).thenReturn(true);
    when(mockAppender.isStopped()).thenReturn(false);
  
    // when append is called, convert the event to 
    // immutable and add it to the event list
    doAnswer(answerVoid((LogEvent event) -> 
                  capturedEvents.add(event.toImmutable()))
      .when(mockAppender).append(any());
  
    logger = (Logger)LogManager.getLogger(ExceptionLogger.class);
    logger.addAppender(mockAppender);
    logger.setLevel(Level.INFO);
  }
  
  @After /** as before */
  
  @Test
  public void loggingIsCaptured() {
    logger.error("What an error");
    verifyErrorMessages("What an error");
  }
  
  // inspecting messages involves just using the list of captured events
  private void verifyErrorMessages(String ... messages) {
    assertThat(capturedEvents.size(), is(messages.length));
  
    int i=0;
    for(LogEvent loggingEvent:capturedEvents) {
      assertEquals(messages[i++], 
        loggingEvent.getMessage().getFormattedMessage());
    }
  }
}

I needed to get this to work without using ch.qos.logback because that was conflicting with the usage of log4j2 in my main classes.

Hat tip to https://codingcraftsman.wordpress.com/2015/04/28/log4j2-mocking-with-mockito-and-junit/ for showing how to do it.

The thing that made a difference for me was casting the logger in the mocks to the logger from org.apache.logging.log4j.core.Logger, I was able to add an appender to it and it didn't break the rest of my code.

Pasting his code here for answer completeness.

import java.io.IOException;
  
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
  
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class ExceptionLoggerTest {
  @Mock
  private Appender mockAppender;
  
  private List<LogEvent> capturedEvents = new ArrayList<>();
  
  private Logger logger;
  
  @Before
  public void setup() {
    // prepare the appender so Log4j likes it
    when(mockAppender.getName()).thenReturn("MockAppender");
    when(mockAppender.isStarted()).thenReturn(true);
    when(mockAppender.isStopped()).thenReturn(false);
  
    // when append is called, convert the event to 
    // immutable and add it to the event list
    doAnswer(answerVoid((LogEvent event) -> 
                  capturedEvents.add(event.toImmutable()))
      .when(mockAppender).append(any());
  
    logger = (Logger)LogManager.getLogger(ExceptionLogger.class);
    logger.addAppender(mockAppender);
    logger.setLevel(Level.INFO);
  }
  
  @After /** as before */
  
  @Test
  public void loggingIsCaptured() {
    logger.error("What an error");
    verifyErrorMessages("What an error");
  }
  
  // inspecting messages involves just using the list of captured events
  private void verifyErrorMessages(String ... messages) {
    assertThat(capturedEvents.size(), is(messages.length));
  
    int i=0;
    for(LogEvent loggingEvent:capturedEvents) {
      assertEquals(messages[i++], 
        loggingEvent.getMessage().getFormattedMessage());
    }
  }
}
青瓷清茶倾城歌 2024-10-18 10:29:50

我创建了一个库来帮助单元测试断言某些消息是否记录在各种日志级别。

https://github.com/nalbion/test-appender

...
import io.github.nalbion.TestAppender;
import org.junit.jupiter.api.Test;

class MyTest {
    private final TestAppender testAppender = new TestAppender(true);

    @BeforeEach
    void setup() {
        testAppender.start();
    }

    @Test
    void myTest() {
        // When
        new MyApplication().doSomething();

        // Then
        // Check all logs match a multi-line string
        testAppender.assertLogs("""
                Hello World!
                My application calls log.info() twice.""");

        // Check that only some lines are logged at WARN
        testAppender.assertLogs(Level.WARN, "My application calls log.info() twice.");

        // Check that at least one of the lines matches a Predicate
        testAppender.assertAnyLogs(
                TestAppender.atLogLevel(Level.INFO)
                        .and(e -> e.getFormattedMessage().matches("Hello .*!"))
        );

        testAppender.assertNoLogs(e -> e.getFormattedMessage().matches("Password: .*!"));
    }

    @Test
    void dynamicValues() {
        // When
        logger.info("The time is {}", new SimpleDateFormat("HH:mm").format(new Date()));
        
        // Then
        // Handle dynamic values - date/time, random values
        testAppender.assertLogs(line -> line.replaceFirst("\\b\\d{1,2}:\\d{2}\\b", "hh:mm"),
                "The time is hh:mm");     
    }
}

I've created a library to help unit tests to assert that certain messages are/not logged at various log levels.

https://github.com/nalbion/test-appender

...
import io.github.nalbion.TestAppender;
import org.junit.jupiter.api.Test;

class MyTest {
    private final TestAppender testAppender = new TestAppender(true);

    @BeforeEach
    void setup() {
        testAppender.start();
    }

    @Test
    void myTest() {
        // When
        new MyApplication().doSomething();

        // Then
        // Check all logs match a multi-line string
        testAppender.assertLogs("""
                Hello World!
                My application calls log.info() twice.""");

        // Check that only some lines are logged at WARN
        testAppender.assertLogs(Level.WARN, "My application calls log.info() twice.");

        // Check that at least one of the lines matches a Predicate
        testAppender.assertAnyLogs(
                TestAppender.atLogLevel(Level.INFO)
                        .and(e -> e.getFormattedMessage().matches("Hello .*!"))
        );

        testAppender.assertNoLogs(e -> e.getFormattedMessage().matches("Password: .*!"));
    }

    @Test
    void dynamicValues() {
        // When
        logger.info("The time is {}", new SimpleDateFormat("HH:mm").format(new Date()));
        
        // Then
        // Handle dynamic values - date/time, random values
        testAppender.assertLogs(line -> line.replaceFirst("\\b\\d{1,2}:\\d{2}\\b", "hh:mm"),
                "The time is hh:mm");     
    }
}
意犹 2024-10-18 10:29:50

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

新答案:

这是 Gradle 包:

  testImplementation 'com.portingle:slf4jtesting:1.2.0'

Maven 链接:

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

Germane 代码:(

下面的导入和私有方法将进入 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));
}

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

I have a new answer that I will post at the top in this post (My "old" answer is still at the bottom of this post) (At the time of writing my "old" answer was a "0", so no harm, no foul! )

Newer answer:

Here is the Gradle Package:

  testImplementation 'com.portingle:slf4jtesting:1.2.0'

Maven Link:

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

Germane Code:

(below imports and private method would go in 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;
    }

============================= OLD ANSWER BELOW .. DO NOT USE================

Below is my previous answer. I changed my below code ... to use the above package after I discovered it (the above package).

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

Then in my unit tests, I can do one of two things:

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

or similar to the above, but do a cast on the custom 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));
}

Take the code with a grain of salt, the ideas are there. I didn't compile the code.

娇妻 2024-10-18 10:29:50

我专门为此用例创建了 LogCaptor ,它应该能够轻松捕获日志,而无需例如,需要嘲笑。它可以捕获由 SLF4J、Log4J2、Java Util Logging、JBoss Logging、Google Flogger 生成的日志以及带有 Lombok 注释的日志。请参阅下面的单元测试示例代码片段:

示例情况:

public class FooService {

    private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);

    public void sayHello() {
        LOGGER.warn("Congratulations, you are pregnant!");
    }

}

使用 LogCaptor 的示例单元测试:

import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class FooServiceTest {

    @Test
    public void sayHelloShouldLogWarnMessage() {
        LogCaptor logCaptor = LogCaptor.forClass(FooService.class);

        FooService fooService = new FooService();
        fooService.sayHello();

        assertThat(logCaptor.getWarnLogs())
            .contains("Congratulations, you are pregnant!");
    }
}

我不太确定是否应该在此处发布此内容,因为它也可以被视为推广“我的库”的一种方式“但我认为这对于面临同样挑战的开发人员可能会有所帮助。

I have created LogCaptor specifically for this use case, which should do the trick of easily capturing logs without the need for mocking for example. It can capture logs generated by SLF4J, Log4J2, Java Util Logging, JBoss Logging, Google Flogger and with Lombok annotations. See below for an example code snippet with the unit test:

Example situation:

public class FooService {

    private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);

    public void sayHello() {
        LOGGER.warn("Congratulations, you are pregnant!");
    }

}

Example unit test with usage of LogCaptor:

import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class FooServiceTest {

    @Test
    public void sayHelloShouldLogWarnMessage() {
        LogCaptor logCaptor = LogCaptor.forClass(FooService.class);

        FooService fooService = new FooService();
        fooService.sayHello();

        assertThat(logCaptor.getWarnLogs())
            .contains("Congratulations, you are pregnant!");
    }
}

I wasn't quite sure if I should post this here, because it could also be seen as a way to promote "my library" but I thought it could be helpful for developers who have the same challenges.

墨离汐 2024-10-18 10:29:50

我知道这个问题发布已经有一段时间了,但我刚刚遇到了类似的问题,我的解决方案可能会有所帮助。按照 @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 项目中都不起作用。

I know it's been a while since this question was posted but I just came across a similar issue and my solution may help. Along the lines of the solution proposed by @Zsolt, we use an appender, more specifically Logback's ListAppender. Showing the code and configurations here (Groovy code but can be easily ported to Java):

Groovy class for log access:

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

Sample logback.xml config:

<?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>

Test:

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

I use this in a SpringBoot project with Groovy+Spock, though I can't see why this wouldn't work in any Java project with Logback.

若水般的淡然安静女子 2024-10-18 10:29:50

我尝试使用 slf4j-test,但发现在 eclipse 中运行测试时,我在类路径上有几个 slf4j 的实现,阻止了 slf4j-test。他们的文档(https://www.slf4j.org/codes.html#multiple_bindings )建议一些 Maven 配置,可以在通过 Maven 运行测试时整理测试类路径,但这在通过 Eclipse 运行测试时对我没有帮助。

最后,我实现了自己的 LogAppender(由 StringBuffer 支持),将其附加到我的 slf4j 记录器(由 logback 提供),并能够编写我的测试。

@Before
public void setUp() {
...
    ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) (LoggerFactory
        .getLogger(AdviserDashboardExceptionHandler.class));
    appender = new StringBufferAppender();
    logger.addAppender(appender);
  }

  @After
  public void clearLogger() {
    appender.clear();
  }

  @Test
  public void loggingTest() {
...
    assertThat(appender.getOutput(), containsString("some expected text"));
  }

I tried using slf4j-test, but found, when running the test in eclipse, that I had several implementations of slf4j on the classpath, blocking slf4j-test. Their documentation (https://www.slf4j.org/codes.html#multiple_bindings) suggests some maven config that would sort out the test classpath when running tests via maven, but this didn't help me when running tests via eclipse.

In the end I implemented my own LogAppender (backed by a StringBuffer), attached that to my slf4j logger (provided by logback), and was able to write my test.

@Before
public void setUp() {
...
    ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) (LoggerFactory
        .getLogger(AdviserDashboardExceptionHandler.class));
    appender = new StringBufferAppender();
    logger.addAppender(appender);
  }

  @After
  public void clearLogger() {
    appender.clear();
  }

  @Test
  public void loggingTest() {
...
    assertThat(appender.getOutput(), containsString("some expected text"));
  }
吃不饱 2024-10-18 10:29:50

slf4j 使用三个流,System.out、System.err 和文件,

在此代码中我仅测试 System.err printStream 使用:
System.out 也是可能的..

class PersonRepositoryImplTest {
  private PersonRepository personRepository;

  private PrintStream systemErr;
  private ByteArrayOutputStream testErr;

  @BeforeEach
  void setUp() {
    systemErr = System.err;
    testErr = new ByteArrayOutputStream();
    System.setErr(new PrintStream(testErr));
  }

  @AfterEach
  void tearDown() {
    System.setErr(systemErr);
  }

  @Test
  void delete() {
    personRepository.delete(1L);

    String logMessages = testErr.toString();
    System.setErr(systemErr);
    System.err.print(logMessages);
    Assertions.assertTrue(logMessages.contains("Person id=1 not found."));
 }
}

slf4j use three stream, System.out, System.err, and file

in this code i test only System.err printStream use:
it possible for System.out too..

class PersonRepositoryImplTest {
  private PersonRepository personRepository;

  private PrintStream systemErr;
  private ByteArrayOutputStream testErr;

  @BeforeEach
  void setUp() {
    systemErr = System.err;
    testErr = new ByteArrayOutputStream();
    System.setErr(new PrintStream(testErr));
  }

  @AfterEach
  void tearDown() {
    System.setErr(systemErr);
  }

  @Test
  void delete() {
    personRepository.delete(1L);

    String logMessages = testErr.toString();
    System.setErr(systemErr);
    System.err.print(logMessages);
    Assertions.assertTrue(logMessages.contains("Person id=1 not found."));
 }
}
空城旧梦 2024-10-18 10:29:49

创建测试规则:

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

----- JUnit 5 with Extension Oct 2021 -----

LogCapture:

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.filter.LevelFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.read.ListAppender;
import ch.qos.logback.core.spi.FilterReply;

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

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:

import ch.qos.logback.classic.Logger;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.slf4j.LoggerFactory;

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

  // ...
}

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

Then use it:

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

----- JUnit 5 with Extension Oct 2021 -----

LogCapture:

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.filter.LevelFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.read.ListAppender;
import ch.qos.logback.core.spi.FilterReply;

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

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:

import ch.qos.logback.classic.Logger;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.slf4j.LoggerFactory;

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

}

then use it:

@ExtendWith(LogCaptureExtension.class)
public class SomeTest {

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

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

  // ...
}
薄荷梦 2024-10-18 10:29:49

要在不依赖特定实现(例如 log4j)的情况下测试 slf4j,您可以提供自己的 slf4j 日志记录实现,如 此 SLF4J 常见问题解答。您的实现可以记录已记录的消息,然后由单元测试询问以进行验证。

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

For testing slf4j without relying on a specific implementation (such as log4j), you can provide your own slf4j logging implementation as described in this SLF4J FAQ. Your implementation can record the messages that were logged and then be interrogated by your unit tests for validation.

The slf4j-test package does exactly this. It's an in-memory slf4j logging implementation that provides methods for retrieving logged messages.

断爱 2024-10-18 10:29:49

对于 JUnit 5,创建一个扩展来实现上面 andrew-feng 提供的解决方案://stackoverflow.com/a/50868216/5468311">创建测试规则

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

然后使用它:

class SomeTestClass {
    
    @RegisterExtension
    public LoggerExtension loggerExtension = new LoggerExtension();
            
    @Test
    public void yourTest() {
        // ...
        assertThat(loggerExtension.getFormattedMessages().size()).isEqualTo(2);
    }
}

For JUnit 5, create an extension that implements the solution provided by andrew-feng above in 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());
    }
}

Then use it:

class SomeTestClass {
    
    @RegisterExtension
    public LoggerExtension loggerExtension = new LoggerExtension();
            
    @Test
    public void yourTest() {
        // ...
        assertThat(loggerExtension.getFormattedMessages().size()).isEqualTo(2);
    }
}
幽蝶幻影 2024-10-18 10:29:49

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

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

I think you could solve your problem with a custom appender. Create a test appender which implements the org.apache.log4j.Appender, and set your appender in the log4j.properties and load it when you execute test cases.

If you call back to the test harness from that appender you can check the logged messages

一个人的旅程 2024-10-18 10:29:49

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

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

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

A better test implementation of SLF4J that works really well in an environment with concurrent test execution is https://github.com/portingle/slf4jtesting

I've chimed in on a few discussion on slf4j log testing and the limitations of existing test approaches when it comes to concurrent test execution.

I decided to put my words into code and that git repo is the result.

两仪 2024-10-18 10:29:49

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

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

Instead of mocking SLF4J you could place the important logging calls you need to test inside their own methods which you can mock more easily.

If you really want to mock SLF4J, I would bet you could create your own provider for it that would allow you to supply a mock logger from the SLF4J side instead of injecting one in your service objects.

白馒头 2024-10-18 10:29:49

使用 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/ 了解更多详情

Using slf4j-test can remove lot of workarounds discussed above

pom.xml

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

Sample class

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

    }

}

Refer http://projects.lidalia.org.uk/slf4j-test/ for more details

半﹌身腐败 2024-10-18 10:29:49

这个解决方案已经在这个绝妙答案以及这个评论,但因为我不认为它本身是答案,将其添加到此处作为社区 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");
  }
}

This solution has been mentioned already before in this groovy answer as well as in this comment, but as I don't see it as an answer itself, adding it here as a community wiki answer.

So JUnit5 solution using logback listappender:

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");
  }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文