如何在日志记录器中对消息进行JUnit断言

310

我有一些测试代码,它调用Java记录器来报告其状态。 在JUnit测试代码中,我想验证是否已经在记录器中正确地生成了日志条目。以下是大致的代码:

methodUnderTest(bool x){
    if(x)
        logger.info("x happened")
}

@Test tester(){
    // perhaps setup a logger first.
    methodUnderTest(true);
    assertXXXXXX(loggedLevel(),Level.INFO);
}

我认为可以通过一个特别适应的日志记录器(或处理程序,或格式化程序)来实现这一点,但我更希望重用已经存在的解决方案。(而且,说实话,我不清楚如何从日志记录器获取logRecord,但假设那是可能的。)

33个回答

9

哇,我不确定为什么这么难。我发现我无法使用上面的任何代码示例,因为我正在使用log4j2 over slf4j。这是我的解决方案:

public class SpecialLogServiceTest {

  @Mock
  private Appender appender;

  @Captor
  private ArgumentCaptor<LogEvent> captor;

  @InjectMocks
  private SpecialLogService specialLogService;

  private LoggerConfig loggerConfig;

  @Before
  public void setUp() {
    // prepare the appender so Log4j likes it
    when(appender.getName()).thenReturn("MockAppender");
    when(appender.isStarted()).thenReturn(true);
    when(appender.isStopped()).thenReturn(false);

    final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
    final Configuration config = ctx.getConfiguration();
    loggerConfig = config.getLoggerConfig("org.example.SpecialLogService");
    loggerConfig.addAppender(appender, AuditLogCRUDService.LEVEL_AUDIT, null);
  }

  @After
  public void tearDown() {
    loggerConfig.removeAppender("MockAppender");
  }

  @Test
  public void writeLog_shouldCreateCorrectLogMessage() throws Exception {
    SpecialLog specialLog = new SpecialLogBuilder().build();
    String expectedLog = "this is my log message";

    specialLogService.writeLog(specialLog);

    verify(appender).append(captor.capture());
    assertThat(captor.getAllValues().size(), is(1));
    assertThat(captor.getAllValues().get(0).getMessage().toString(), is(expectedLog));
  }
}

这个编译通过了,这已经比其他解决方案好了,但是 Appender 似乎没有被使用:Wanted but not invoked: appender.append(<Capturing argument>); 即使 log.info 方法确实被调用了。 - AdrienW
1
@AdrienW 我很高兴它仍然可以编译(三年后),但是尽管有警告,它是否仍然能够正常工作呢? - Dagmar
不行 :/ 因为 appender.append 没有被调用,我无法捕获参数。没有 verify 行它可以运行,但是它什么也不做... 无论如何,感谢您花时间在三年后回复!如果我找到一个好的解决方案,我会在这里评论或回答 - AdrienW
1
@AdrienW 感谢您的回复。不幸的是,我已经不再参与这个项目了,所以无法检查版本或查看它是否仍然适用于我。我唯一能建议的是检查 loggerConfig 是否被正确设置 - 也许包名有误?祝你好运! - Dagmar
1
我通过这个解决方案使其工作:https://dev59.com/WIfca4cB1Zd3GeqPmbMe#31836674(我仍然不知道您的解决方案为什么在我的项目上不起作用,但现在我有一个可以工作的解决方案了。再次感谢您的帮助!) - AdrienW
2
@AdrienW 感谢分享你的解决方案,我很高兴你现在已经搞定了! - Dagmar

6
最简单的方式
  @ExtendWith(OutputCaptureExtension.class)
  class MyTestClass { 
    
          @Test
          void my_test_method(CapturedOutput output) {
               assertThat(output).contains("my test log.");
          }
  }

它只记录所有写操作到标准输出(由appender记录的整个消息和相应的patternlayout),无法获取日志级别。 - Saad Benbouzid
1
当你需要匹配记录器的内容时,它非常有用。它能够胜任我的工作。 - Siddharth Aadarsh

6

正如其他人所提到的,您可以使用模拟框架。为使其工作,您必须在类中公开记录器(尽管我可能更喜欢将其设置为包私有而不是创建公共setter)。

另一种解决方案是手动创建虚拟记录器。您必须编写虚拟记录器(更多的夹具代码),但在这种情况下,我更喜欢测试的可读性胜过模拟框架保存的代码。

我会做这样的事情:

class FakeLogger implements ILogger {
    public List<String> infos = new ArrayList<String>();
    public List<String> errors = new ArrayList<String>();

    public void info(String message) {
        infos.add(message);
    }

    public void error(String message) {
        errors.add(message);
    }
}

class TestMyClass {
    private MyClass myClass;        
    private FakeLogger logger;        

    @Before
    public void setUp() throws Exception {
        myClass = new MyClass();
        logger = new FakeLogger();
        myClass.logger = logger;
    }

    @Test
    public void testMyMethod() {
        myClass.myMethod(true);

        assertEquals(1, logger.infos.size());
    }
}

6

我为logback做了以下工作。

我创建了一个TestAppender类:

public class TestAppender extends AppenderBase<ILoggingEvent> {

    private Stack<ILoggingEvent> events = new Stack<ILoggingEvent>();

    @Override
    protected void append(ILoggingEvent event) {
        events.add(event);
    }

    public void clear() {
        events.clear();
    }

    public ILoggingEvent getLastEvent() {
        return events.pop();
    }
}

然后,在我的 TestNG 单元测试类的父类中,我创建了一个方法:

protected TestAppender testAppender;

@BeforeClass
public void setupLogsForTesting() {
    Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
    testAppender = (TestAppender)root.getAppender("TEST");
    if (testAppender != null) {
        testAppender.clear();
    }
}

我有一个在src/test/resources中定义的logback-test.xml文件,并且我添加了一个测试appender:

<appender name="TEST" class="com.intuit.icn.TestAppender">
    <encoder>
        <pattern>%m%n</pattern>
    </encoder>
</appender>

并将此 appender 添加到根 appender:

<root>
    <level value="error" />
    <appender-ref ref="STDOUT" />
    <appender-ref ref="TEST" />
</root>

现在,通过继承我的父测试类,我可以获取附加程序并获取最后记录的消息,并验证消息、级别和可抛出项。

ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");

我看不到getAppender方法在哪里定义的?!? - bioinfornatics
getAppender是ch.qos.logback.classic.Logger上的一个方法。 - kfox

4
请注意,在Log4J 2.x中,公共接口org.apache.logging.log4j.Logger不包括setAppender()removeAppender()方法。
但是,如果您没有进行太复杂的操作,您应该能够将其强制转换为实现类org.apache.logging.log4j.core.Logger,该类公开了这些方法。
这里有一个使用MockitoAssertJ的示例:
// Import the implementation class rather than the API interface
import org.apache.logging.log4j.core.Logger;

// Cast logger to implementation class to get access to setAppender/removeAppender
Logger log = (Logger) LogManager.getLogger(MyClassUnderTest.class);

// Set up the mock appender, stubbing some methods Log4J needs internally
Appender appender = mock(Appender.class);
when(appender.getName()).thenReturn("Mock Appender");
when(appender.isStarted()).thenReturn(true);

log.addAppender(appender);
try {
    new MyClassUnderTest().doSomethingThatShouldLogAnError();
} finally {
    log.removeAppender(appender);
}

// Verify that we got an error with the expected message
ArgumentCaptor<LogEvent> logEventCaptor = ArgumentCaptor.forClass(LogEvent.class);
verify(appender).append(logEventCaptor.capture());
LogEvent logEvent = logEventCaptor.getValue();
assertThat(logEvent.getLevel()).isEqualTo(Level.ERROR);
assertThat(logEvent.getMessage().getFormattedMessage()).contains(expectedErrorMessage);

4
Here is the sample code to mock log, irrespective of the version used for junit or sping, springboot.

import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
import org.mockito.ArgumentMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.Test;

import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class MyTest {
  private static Logger logger = LoggerFactory.getLogger(MyTest.class);

    @Test
    public void testSomething() {
    ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
    final Appender mockAppender = mock(Appender.class);
    when(mockAppender.getName()).thenReturn("MOCK");
    root.addAppender(mockAppender);

    //... do whatever you need to trigger the log

    verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
      @Override
      public boolean matches(final Object argument) {
        return ((LoggingEvent)argument).getFormattedMessage().contains("Hey this is the message I want to see");
      }
    }));
  }
}

1
这对我有用。对我来说,不需要线路'when(mockAppender.getName()).thenReturn("MOCK")'。 - Mayank Raghav

3

在类的实现中,您不需要依赖于硬编码的静态全局日志记录器,您可以在默认构造函数中提供一个默认的日志记录器,然后使用特定的构造函数设置对提供的日志记录器的引用。

class MyClassToTest {
    private final Logger logger;
    
    public MyClassToTest() {
      this(SomeStatic.logger);
    };
    
    MyClassToTest(Logger logger) {
      this.logger = logger;
    };
    
    public void someOperation() {
        logger.warn("warning message");
        // ...
    };
};

class MyClassToTestTest {
    
    @Test
    public warnCalled() {
        Logger loggerMock = mock(Logger.class);
        MyClassTest myClassToTest = new MyClassToTest(logger);
        myClassToTest.someOperation();
        verify(loggerMock).warn(anyString());
    };
}

3

请查看这个库 https://github.com/Hakky54/log-captor

在你的 Maven 文件中引入此库的参考:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>logcaptor</artifactId>
    <version>2.5.0</version>
    <scope>test</scope>
</dependency>

在Java代码测试方法中,您应该包括以下内容:
LogCaptor logCaptor = LogCaptor.forClass(MyClass.class);

 // do the test logic....

assertThat(logCaptor.getLogs()).contains("Some log to assert");


1
假设您在服务中有以下行以进行测试:public class YourService { private static final Logger logger = LogManager.getLogger(YourService.class);还要不要忘记添加以下依赖项以进行assertThat测试: ... org.assertj assertj-core 3.19.0 test - Vifier Lockla

3

对于我来说,你可以使用JUnit和Mockito来简化你的测试。我为此提出以下解决方案:

import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.spi.LoggingEvent;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.times;

@RunWith(MockitoJUnitRunner.class)
public class MyLogTest {
    private static final String FIRST_MESSAGE = "First message";
    private static final String SECOND_MESSAGE = "Second message";
    @Mock private Appender appender;
    @Captor private ArgumentCaptor<LoggingEvent> captor;
    @InjectMocks private MyLog;

    @Before
    public void setUp() {
        LogManager.getRootLogger().addAppender(appender);
    }

    @After
    public void tearDown() {
        LogManager.getRootLogger().removeAppender(appender);
    }

    @Test
    public void shouldLogExactlyTwoMessages() {
        testedClass.foo();

        then(appender).should(times(2)).doAppend(captor.capture());
        List<LoggingEvent> loggingEvents = captor.getAllValues();
        assertThat(loggingEvents).extracting("level", "renderedMessage").containsExactly(
                tuple(Level.INFO, FIRST_MESSAGE)
                tuple(Level.INFO, SECOND_MESSAGE)
        );
    }
}

这就是为什么我们在测试中具有良好的灵活性,可以处理不同数量的消息

1
为了避免重复编写几乎相同的代码块,我想在Log4j2中添加几乎1对1适用于我的代码。只需更改导入为“org.apache.logging.log4j.core”,将日志记录器转换为“org.apache.logging.log4j.core.Logger”,添加以下内容:when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender");并将LoggingEvent更改为LogEvent。 - Aliaksei Yatsau

2

1
该网站的链接已经无法访问,如果您仍然知道如何操作,请编辑您的帖子以便其他人受益。 - hd84335
归档 @ https://web.archive.org/web/20200927012213/https://www.baeldung.com/junit-asserting-logs - sam-6174

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接