如何对记录器中的消息执行 JUnit 断言

Posted

技术标签:

【中文标题】如何对记录器中的消息执行 JUnit 断言【英文标题】:How to do a JUnit assert on a message in a logger 【发布时间】:2010-12-22 02:39:21 【问题描述】:

我有一些被测代码调用 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,但假设这是可能的。)

【问题讨论】:

【参考方案1】:

实际上,您正在测试依赖类的副作用。对于单元测试,您只需要验证

logger.info()

使用正确的参数调用。因此使用模拟框架来模拟记录器,这将允许您测试自己的类的行为。

【讨论】:

你是如何模拟私有静态最终字段的,大多数记录器都是定义的? Powermockito?玩得开心.. Stefano:最后一个字段以某种方式被初始化,我看到了注入 Mocks 的各种方法,而不是真实的东西。可能首先需要某种程度的可测试性设计。 blog.codecentric.de/en/2011/11/… 正如 Mehdi 所说,可能使用合适的 Handler 可能就足够了, @StefanoL:如果记录器是用private static final Logger LOGGER = Logger.getLogger(someString); 定义的,您仍然可以使用Logger.getLogger(someString); 从单元测试中访问它,修改它并添加处理程序(如接受的答案)。【参考方案2】:

模拟是这里的一个选项,虽然这很困难,因为记录器通常是私有的静态最终 - 所以设置模拟记录器不是小菜一碟,或者需要修改被测类。

您可以创建一个自定义 Appender(或其他任何名称)并注册它 - 通过仅测试配置文件或运行时(在某种程度上,取决于日志框架)。 然后您可以获取该附加程序(静态地,如果在配置文件中声明,或者通过其当前引用,如果您在运行时插入它),并验证其内容。

【讨论】:

【参考方案3】:

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

另一种解决方案是手动创建一个假记录器。您必须编写假记录器(更多固定代码),但在这种情况下,我希望针对模拟框架中保存的代码进行测试的增强可读性。

我会这样做:

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

【讨论】:

【参考方案4】:

我也多次需要这个。我在下面汇总了一个小样本,您可以根据自己的需要进行调整。基本上,您创建自己的Appender 并将其添加到您想要的记录器中。如果您想收集所有内容,根记录器是一个不错的起点,但如果您愿意,可以使用更具体的记录器。完成后不要忘记删除 Appender,否则可能会造成内存泄漏。下面我在测试中完成了它,但 setUp@BeforetearDown@After 可能是更好的地方,这取决于您的需要。

此外,下面的实现将所有内容收集在内存中的 List 中。如果你记录很多,你可能会考虑添加一个过滤器来删除无聊的条目,或者将日志写入磁盘上的临时文件(提示:LoggingEventSerializable,所以你应该能够序列化事件对象,如果您的日志消息是。)

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

public class MyTest 
    @Test
    public void test() 
        final TestAppender appender = new TestAppender();
        final Logger logger = Logger.getRootLogger();
        logger.addAppender(appender);
        try 
            Logger.getLogger(MyTest.class).info("Test");
        
        finally 
            logger.removeAppender(appender);
        

        final List<LoggingEvent> log = appender.getLog();
        final LoggingEvent firstLogEntry = log.get(0);
        assertThat(firstLogEntry.getLevel(), is(Level.INFO));
        assertThat((String) firstLogEntry.getMessage(), is("Test"));
        assertThat(firstLogEntry.getLoggerName(), is("MyTest"));
    


class TestAppender extends AppenderSkeleton 
    private final List<LoggingEvent> log = new ArrayList<LoggingEvent>();

    @Override
    public boolean requiresLayout() 
        return false;
    

    @Override
    protected void append(final LoggingEvent loggingEvent) 
        log.add(loggingEvent);
    

    @Override
    public void close() 
    

    public List<LoggingEvent> getLog() 
        return new ArrayList<LoggingEvent>(log);
    

【讨论】:

这很好用。我要做的唯一改进是调用logger.getAllAppenders(),然后逐步调用appender.setThreshold(Level.OFF)(完成后重置它们!)。这样可以确保您尝试生成的“坏”消息不会出现在测试日志中,并且不会吓到下一个开发人员。 在 Log4j 2.x 中稍微复杂一些,因为您需要创建一个插件,看看这个:***.com/questions/24205093/… 谢谢。但是如果你使用 LogBack,你可以使用 ListAppender&lt;ILoggingEvent&gt; 而不是创建你自己的自定义 appender。 但这不适用于 slf4j!你知道我怎样才能改变它以适应它吗? @s.d 如果您将Logger 转换为org.apache.logging.log4j.core.Logger(接口的实现类),您将再次访问setAppender()/removeAppender()【参考方案5】:

非常感谢这些(令人惊讶的)快速而有用的答案;他们让我找到了正确的解决方案。

代码库是我想使用它,使用 java.util.logging 作为它的记录器机制,而且我觉得这些代码还不够熟悉,无法将其完全更改为 log4j 或记录器接口/外观。但根据这些建议,我“破解”了一个 j.u.l.handler 扩展,这是一种享受。

下面是一个简短的摘要。扩展java.util.logging.Handler

class LogHandler extends Handler

    Level lastLevel = Level.FINEST;

    public Level  checkLevel() 
        return lastLevel;
        

    public void publish(LogRecord record) 
        lastLevel = record.getLevel();
    

    public void close()
    public void flush()

显然,您可以从LogRecord 存储任意数量/想要/需要的数量,或者将它们全部压入堆栈直到溢出。

在准备 junit-test 时,您创建了一个 java.util.logging.Logger 并向其中添加了这样一个新的 LogHandler

@Test tester() 
    Logger logger = Logger.getLogger("my junit-test logger");
    LogHandler handler = new LogHandler();
    handler.setLevel(Level.ALL);
    logger.setUseParentHandlers(false);
    logger.addHandler(handler);
    logger.setLevel(Level.ALL);

setUseParentHandlers() 的调用是为了使普通处理程序静音,以便(对于此junit-test 运行)不会发生不必要的日志记录。做任何你的被测代码需要使用这个记录器,运行测试和 assertEquality:

    libraryUnderTest.setLogger(logger);
    methodUnderTest(true);  // see original question.
    assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() );

(当然,您可以将大部分工作转移到 @Before 方法中并进行各种其他改进,但这会使演示文稿变得混乱。)

【讨论】:

【参考方案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 name="TEST" class="com.intuit.icn.TestAppender">
    <encoder>
        <pattern>%m%n</pattern>
    </encoder>
</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 方法是在哪里定义的?!? getAppender 是 ch.qos.logback.classic.Logger 上的一个方法【参考方案7】:

另一个选项是模拟 Appender 并验证消息是否记录到此 appender。 Log4j 1.2.x 和 mockito 示例:

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

public class MyTest 

    private final Appender appender = mock(Appender.class);
    private final Logger logger = Logger.getRootLogger();

    @Before
    public void setup() 
        logger.addAppender(appender);
    

    @Test
    public void test() 
        // when
        Logger.getLogger(MyTest.class).info("Test");

        // then
        ArgumentCaptor<LoggingEvent> argument = ArgumentCaptor.forClass(LoggingEvent.class);
        verify(appender).doAppend(argument.capture());
        assertEquals(Level.INFO, argument.getValue().getLevel());
        assertEquals("Test", argument.getValue().getMessage());
        assertEquals("MyTest", argument.getValue().getLoggerName());
    

    @After
    public void cleanup() 
        logger.removeAppender(appender);
    

【讨论】:

【参考方案8】:

另一个值得一提的想法是创建一个 CDI 生产者来注入您的记录器,这样模拟变得容易。 (而且它还具有不必再声明“整个记录器语句”的优点,但这是题外话)

例子:

创建要注入的记录器:

public class CdiResources 
  @Produces @LoggerType
  public Logger createLogger(final InjectionPoint ip) 
      return Logger.getLogger(ip.getMember().getDeclaringClass());
  

限定词:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target(TYPE, METHOD, FIELD, PARAMETER)
public @interface LoggerType 

在生产代码中使用记录器:

public class ProductionCode 
    @Inject
    @LoggerType
    private Logger logger;

    public void logSomething() 
        logger.info("something");
    

在您的测试代码中测试记录器(给出一个 easyMock 示例):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

@Test
public void testTheLogger() 
   logger.info("something");
   replayAll();
   productionCode.logSomething();

【讨论】:

【参考方案9】:

受@RonaldBlaschke 解决方案的启发,我想出了这个:

public class Log4JTester extends ExternalResource 
    TestAppender appender;

    @Override
    protected void before() 
        appender = new TestAppender();
        final Logger rootLogger = Logger.getRootLogger();
        rootLogger.addAppender(appender);
    

    @Override
    protected void after() 
        final Logger rootLogger = Logger.getRootLogger();
        rootLogger.removeAppender(appender);
    

    public void assertLogged(Matcher<String> matcher) 
        for(LoggingEvent event : appender.events) 
            if(matcher.matches(event.getMessage())) 
                return;
            
        
        fail("No event matches " + matcher);
    

    private static class TestAppender extends AppenderSkeleton 

        List<LoggingEvent> events = new ArrayList<LoggingEvent>();

        @Override
        protected void append(LoggingEvent event) 
            events.add(event);
        

        @Override
        public void close() 

        

        @Override
        public boolean requiresLayout() 
            return false;
        
    


... 它允许你这样做:

@Rule public Log4JTester logTest = new Log4JTester();

@Test
public void testFoo() 
     user.setStatus(Status.PREMIUM);
     logTest.assertLogged(
        stringContains("Note added to account: premium customer"));

你可以让它以更聪明的方式使用 hamcrest,但我已经把它留在这里了。

【讨论】:

【参考方案10】:

使用 Jmockit (1.21) 我能够编写这个简单的测试。 该测试确保只调用一次特定的 ERROR 消息。

@Test
public void testErrorMessage() 
    final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger( MyConfig.class );

    new Expectations(logger) 
        //make sure this error is happens just once.
        logger.error( "Something went wrong..." );
        times = 1;
    ;

    new MyTestObject().runSomethingWrong( "aaa" ); //SUT that eventually cause the error in the log.    

【讨论】:

【参考方案11】:

模拟 Appender 可以帮助捕获日志行。 查找示例:http://clearqa.blogspot.co.uk/2016/12/test-log-lines.html

// Fully working test at: https://github.com/njaiswal/logLineTester/blob/master/src/test/java/com/nj/Utils/UtilsTest.java

@Test
public void testUtilsLog() throws InterruptedException 

    Logger utilsLogger = (Logger) LoggerFactory.getLogger("com.nj.utils");

    final Appender mockAppender = mock(Appender.class);
    when(mockAppender.getName()).thenReturn("MOCK");
    utilsLogger.addAppender(mockAppender);

    final List<String> capturedLogs = Collections.synchronizedList(new ArrayList<>());
    final CountDownLatch latch = new CountDownLatch(3);

    //Capture logs
    doAnswer((invocation) -> 
        LoggingEvent loggingEvent = invocation.getArgumentAt(0, LoggingEvent.class);
        capturedLogs.add(loggingEvent.getFormattedMessage());
        latch.countDown();
        return null;
    ).when(mockAppender).doAppend(any());

    //Call method which will do logging to be tested
    Application.main(null);

    //Wait 5 seconds for latch to be true. That means 3 log lines were logged
    assertThat(latch.await(5L, TimeUnit.SECONDS), is(true));

    //Now assert the captured logs
    assertThat(capturedLogs, hasItem(containsString("One")));
    assertThat(capturedLogs, hasItem(containsString("Two")));
    assertThat(capturedLogs, hasItem(containsString("Three")));

【讨论】:

【参考方案12】:

就我而言,您可以通过使用JUnitMockito 来简化您的测试。 我为此提出以下解决方案:

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

这就是为什么我们对不同消息数量

的测试具有很好的灵活性

【讨论】:

为了不重复几乎相同的代码块,我想补充一点,几乎 1to1 对我来说适用于 Log4j2。只需将导入更改为“org.apache.logging.log4j.core”,将 logger 转换为“org.apache.logging.log4j.core.Logger”,添加when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); 并更改 LoggingEvent -> LogEvent【参考方案13】:

使用下面的代码。我在我的 spring 集成测试中使用相同的代码,我使用 log back 进行日志记录。使用方法 assertJobIsScheduled 断言打印在日志中的文本。

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;

private Logger rootLogger;
final Appender mockAppender = mock(Appender.class);

@Before
public void setUp() throws Exception 
    initMocks(this);
    when(mockAppender.getName()).thenReturn("MOCK");
    rootLogger = (Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
    rootLogger.addAppender(mockAppender);


private void assertJobIsScheduled(final String matcherText) 
    verify(mockAppender).doAppend(argThat(new ArgumentMatcher() 
        @Override
        public boolean matches(final Object argument) 
            return ((LoggingEvent)argument).getFormattedMessage().contains(matcherText);
        
    ));

【讨论】:

【参考方案14】:

对于 log4j2,解决方案略有不同,因为 AppenderSkeleton 不再可用。此外,如果您期望多条日志消息,则使用 Mockito 或类似库来创建带有 ArgumentCaptor 的 Appender 将不起作用,因为 MutableLogEvent 在多条日志消息上被重用。我为 log4j2 找到的最佳解决方案是:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;

private static MockedAppender mockedAppender;
private static Logger logger;

@Before
public void setup() 
    mockedAppender.message.clear();


/**
 * For some reason mvn test will not work if this is @Before, but in eclipse it works! As a
 * result, we use @BeforeClass.
 */
@BeforeClass
public static void setupClass() 
    mockedAppender = new MockedAppender();
    logger = (Logger)LogManager.getLogger(MatchingMetricsLogger.class);
    logger.addAppender(mockedAppender);
    logger.setLevel(Level.INFO);


@AfterClass
public static void teardown() 
    logger.removeAppender(mockedAppender);


@Test
public void test() 
    // do something that causes logs
    for (String e : mockedAppender.message) 
        // add asserts for the log messages
    


private static class MockedAppender extends AbstractAppender 

    List<String> message = new ArrayList<>();

    protected MockedAppender() 
        super("MockedAppender", null, null);
    

    @Override
    public void append(LogEvent event) 
        message.add(event.getMessage().getFormattedMessage());
    

【讨论】:

【参考方案15】:

如果您使用java.util.logging.Logger,这篇文章可能会很有帮助,它会创建一个新的处理程序并对日志输出进行断言: http://octodecillion.com/blog/jmockit-test-logging/

【讨论】:

【参考方案16】:

您可能正在尝试测试两件事。

当我的程序的操作员感兴趣的事件发生时,我的程序是否会执行适当的日志记录操作,这可以通知操作员该事件。 当我的程序执行日志记录操作时,它产生的日志消息的文本是否正确。

这两个东西实际上是不同的东西,所以可以分开测试。但是,测试第二个(消息文本)非常有问题,我建议完全不要这样做。消息文本的测试最终将包括检查一个文本字符串(预期的消息文本)是否与日志代码中使用的文本字符串相同,或者可以从这些文本字符串中轻松派生出来。

这些测试根本不测试程序逻辑,它们只测试一个资源(字符串)是否等同于另一个资源。 测试很脆弱;即使是对日志消息格式的微小调整也会破坏您的测试。 测试与日志界面的国际化(翻译)不兼容。测试假设只有一种可能的消息文本,因此只有一种可能的人类语言。

请注意,让您的程序代码(可能是实现一些业务逻辑)直接调用文本日志接口是一种糟糕的设计(但不幸的是非常常见)。负责业务逻辑的代码也决定了一些日志记录策略和日志消息的文本。它将业务逻辑与用户界面代码混合在一起(是的,日志消息是程序用户界面的一部分)。这些东西应该分开。

因此我建议业务逻辑不要直接生成日志消息的文本。而是让它委托给一个日志对象。

日志记录对象的类应提供合适的内部 API,您的业务对象可以使用该 API 来表达使用域模型对象而不是文本字符串发生的事件。 日志记录类的实现负责生成这些域对象的文本表示,并呈现事件的合适文本描述,然后将该文本消息转发到低级日志记录框架(例如 JUL、log4j 或 slf4j) . 您的业务逻辑只负责调用记录器类的内部 API 的正确方法,传递正确的域对象,以描述发生的实际事件。 您的具体日志记录类 implementsinterface,它描述了您的业务逻辑可能使用的内部 API。 实现业务逻辑且必须执行日志记录的类具有对要委托给的日志记录对象的引用。引用的类是抽象的interface。 使用依赖注入来设置对记录器的引用。

然后,您可以通过创建一个模拟记录器来测试您的业务逻辑类是否正确地将事件告知记录接口,该记录器实现内部记录 API,并在测试的设置阶段使用依赖注入。

像这样:

 public class MyService // The class we want to test
    private final MyLogger logger;

    public MyService(MyLogger logger) 
       this.logger = Objects.requireNonNull(logger);
    

    public void performTwiddleOperation(Foo foo) // The method we want to test
       ...// The business logic
       logger.performedTwiddleOperation(foo);
    
 ;

 public interface MyLogger 
    public void performedTwiddleOperation(Foo foo);
    ...
 ;

 public final class MySl4jLogger: implements MyLogger 
    ...

    @Override
    public void performedTwiddleOperation(Foo foo) 
       logger.info("twiddled foo " + foo.getId());
    
 

 public final void MyProgram 
    public static void main(String[] argv) 
       ...
       MyLogger logger = new MySl4jLogger(...);
       MyService service = new MyService(logger);
       startService(service);// or whatever you must do
       ...
    
 

 public class MyServiceTest 
    ...

    static final class MyMockLogger: implements MyLogger 
       private Food.id id;
       private int nCallsPerformedTwiddleOperation;
       ...

       @Override
       public void performedTwiddleOperation(Foo foo) 
          id = foo.id;
          ++nCallsPerformedTwiddleOperation;
       

       void assertCalledPerformedTwiddleOperation(Foo.id id) 
          assertEquals("Called performedTwiddleOperation", 1, nCallsPerformedTwiddleOperation);
          assertEquals("Called performedTwiddleOperation with correct ID", id, this.id);
       
    ;

    @Test
    public void testPerformTwiddleOperation_1() 
       // Setup
       MyMockLogger logger = new MyMockLogger();
       MyService service = new MyService(logger);
       Foo.Id id = new Foo.Id(...);
       Foo foo = new Foo(id, 1);

       // Execute
       service.performedTwiddleOperation(foo);

       // Verify
       ...
       logger.assertCalledPerformedTwiddleOperation(id);
    
 

【讨论】:

【参考方案17】:

如果我只想看到记录了一些字符串(而不是验证太脆弱的确切日志语句),我所做的就是将 StdOut 重定向到缓冲区,执行包含,然后重置 StdOut:

PrintStream original = System.out;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer));

// Do something that logs

assertTrue(buffer.toString().contains(myMessage));
System.setOut(original);

【讨论】:

我用java.util.logging 试过这个(虽然我用了System.setErr(new PrintStream(buffer));,因为它记录到stderr),但它不起作用(缓冲区保持为空)。如果我直接使用System.err.println("foo"),它可以工作,所以我假设日志系统保留它自己对输出流的引用,它从System.err 获取,所以我对System.setErr(..) 的调用对日志输出没有影响,因为它发生在日志系统初始化之后。【参考方案18】:

哇。我不确定这为什么这么难。我发现我无法使用上面的任何代码示例,因为我使用的是 log4j2 而不是 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(&lt;Capturing argument&gt;); 尽管log.info 方法确实被调用了 @AdrienW 我很高兴它仍然可以编译(3 年后),但是尽管有警告,它仍然可以工作吗? @AdrienW 感谢您的回复。不幸的是,我不再从事这个项目,所以我无法检查版本或查看它是否仍然对我有用。我唯一能建议的就是检查 loggerConfig 的设置是否正确——也许包名是错误的?祝你好运! 我让它与这个解决方案一起工作:***.com/a/31836674/5018771(我仍然不知道究竟是什么导致您的解决方案无法在我的项目上运行,但现在我有一个可以解决的问题,再次感谢您停止通过!) @AdrienW 感谢您分享您的解决方案,我很高兴您现在可以使用它!【参考方案19】:

Log4J2 的 API 略有不同。此外,您可能正在使用它的异步附加程序。我为此创建了一个锁定的附加程序:

    public static class LatchedAppender extends AbstractAppender implements AutoCloseable 

    private final List<LogEvent> messages = new ArrayList<>();
    private final CountDownLatch latch;
    private final LoggerConfig loggerConfig;

    public LatchedAppender(Class<?> classThatLogs, int expectedMessages) 
        this(classThatLogs, null, null, expectedMessages);
    
    public LatchedAppender(Class<?> classThatLogs, Filter filter, Layout<? extends Serializable> layout, int expectedMessages) 
        super(classThatLogs.getName()+"."+"LatchedAppender", filter, layout);
        latch = new CountDownLatch(expectedMessages);
        final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
        final Configuration config = ctx.getConfiguration();
        loggerConfig = config.getLoggerConfig(LogManager.getLogger(classThatLogs).getName());
        loggerConfig.addAppender(this, Level.ALL, ThresholdFilter.createFilter(Level.ALL, null, null));
        start();
    

    @Override
    public void append(LogEvent event) 
        messages.add(event);
        latch.countDown();
    

    public List<LogEvent> awaitMessages() throws InterruptedException 
        assertTrue(latch.await(10, TimeUnit.SECONDS));
        return messages;
    

    @Override
    public void close() 
        stop();
        loggerConfig.removeAppender(this.getName());
    

像这样使用它:

        try (LatchedAppender appender = new LatchedAppender(ClassUnderTest.class, 1)) 

        ClassUnderTest.methodThatLogs();
        List<LogEvent> events = appender.awaitMessages();
        assertEquals(1, events.size());
        //more assertions here

    //appender removed

【讨论】:

【参考方案20】:

这是一个简单高效的 Logback 解决方案。 它不需要添加/创建任何新类。 它依赖于 ListAppender :一个白盒 logback appender,其中日志条目被添加到 public List 字段中,我们可以使用它来进行断言。

这是一个简单的例子。

Foo 类:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Foo 

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

    public void doThat() 
        LOGGER.info("start");
        //...
        LOGGER.info("finish");
    

FooTest 类:

import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;

public class FooTest 

    @Test
    void doThat() throws Exception 
        // get Logback Logger 
        Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);

        // create and start a ListAppender
        ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
        listAppender.start();

        // add the appender to the logger
        // addAppender is outdated now
        fooLogger.addAppender(listAppender);

        // call method under test
        Foo foo = new Foo();
        foo.doThat();

        // JUnit assertions
        List<ILoggingEvent> logsList = listAppender.list;
        assertEquals("start", logsList.get(0)
                                      .getMessage());
        assertEquals(Level.INFO, logsList.get(0)
                                         .getLevel());

        assertEquals("finish", logsList.get(1)
                                       .getMessage());
        assertEquals(Level.INFO, logsList.get(1)
                                         .getLevel());
    

JUnit 断言听起来不太适合断言列表元素的某些特定属性。 匹配器/断言库如 AssertJ 或 Hamcrest 看起来更好:

使用 AssertJ 会是:

import org.assertj.core.api.Assertions;

Assertions.assertThat(listAppender.list)
          .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
          .containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));

【讨论】:

如果您记录错误,如何阻止测试失败? @Ghilteras 我不确定是否理解。记录错误不应使您的测试失败。你解释什么? 另外,请记住不要mock 正在测试的类。你需要用new操作符来实例化它 调用start() 方法是必要的,这似乎有点愚蠢,但它是 - 不要忘记将它包含在您的测试中,否则它将不起作用。 在我的情况下,appender 的list 是空的,直到我得到根记录器(不是类记录器):Logger logger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME)【参考方案21】:

如果您使用的是 log4j2,https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ 的解决方案允许我断言消息已记录。

解决办法是这样的:

将 log4j appender 定义为 ExternalResource 规则

public class LogAppenderResource extends ExternalResource 

private static final String APPENDER_NAME = "log4jRuleAppender";

/**
 * Logged messages contains level and message only.
 * This allows us to test that level and message are set.
 */
private static final String PATTERN = "%-5level %msg";

private Logger logger;
private Appender appender;
private final CharArrayWriter outContent = new CharArrayWriter();

public LogAppenderResource(org.apache.logging.log4j.Logger logger) 
    this.logger = (org.apache.logging.log4j.core.Logger)logger;


@Override
protected void before() 
    StringLayout layout = PatternLayout.newBuilder().withPattern(PATTERN).build();
    appender = WriterAppender.newBuilder()
            .setTarget(outContent)
            .setLayout(layout)
            .setName(APPENDER_NAME).build();
    appender.start();
    logger.addAppender(appender);


@Override
protected void after() 
    logger.removeAppender(appender);


public String getOutput() 
    return outContent.toString();
    

定义使用您的 ExternalResource 规则的测试

public class LoggingTextListenerTest 

    @Rule public LogAppenderResource appender = new LogAppenderResource(LogManager.getLogger(LoggingTextListener.class)); 
    private LoggingTextListener listener = new LoggingTextListener(); //     Class under test

    @Test
    public void startedEvent_isLogged() 
    listener.started();
    assertThat(appender.getOutput(), containsString("started"));
    

不要忘记将 log4j2.xml 作为 src/test/resources 的一部分

【讨论】:

【参考方案22】:
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");
      
    ));
  

【讨论】:

这对我有用。我不需要行'when(mockAppender.getName()).thenReturn("MOCK")'。【参考方案23】:

我为 log4j 回答了类似的问题,请参阅 how-can-i-test-with-junit-that-a-warning-was-logged-with-log4

这是较新的示例,使用 Log4j2(使用 2.11.2 测试)和 junit 5;

    package com.whatever.log;

    import org.apache.logging.log4j.Level;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.core.Logger;
    import org.apache.logging.log4j.core.*;
    import org.apache.logging.log4j.core.appender.AbstractAppender;
    import org.apache.logging.log4j.core.config.Configuration;
    import org.apache.logging.log4j.core.config.LoggerConfig;
    import org.apache.logging.log4j.core.config.plugins.Plugin;
    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
    import org.apache.logging.log4j.core.config.plugins.PluginElement;
    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;

    import java.util.ArrayList;
    import java.util.List;
    import static org.junit.Assert.*;

class TestLogger 

    private TestAppender testAppender;
    private LoggerConfig loggerConfig;
    private final Logger logger = (Logger)
            LogManager.getLogger(ClassUnderTest.class);

    @Test
    @DisplayName("Test Log Junit5 and log4j2")
    void test() 
        ClassUnderTest.logMessage();
        final LogEvent loggingEvent = testAppender.events.get(0);
        //asset equals 1 because log level is info, change it to debug and
        //the test will fail
        assertTrue(testAppender.events.size()==1,"Unexpected empty log");
        assertEquals(Level.INFO,loggingEvent.getLevel(),"Unexpected log level");
        assertEquals(loggingEvent.getMessage().toString()
                ,"Hello Test","Unexpected log message");
    

    @BeforeEach
    private void setup() 
        testAppender = new TestAppender("TestAppender", null);

        final LoggerContext context = logger.getContext();
        final Configuration configuration = context.getConfiguration();

        loggerConfig = configuration.getLoggerConfig(logger.getName());
        loggerConfig.setLevel(Level.INFO);
        loggerConfig.addAppender(testAppender,Level.INFO,null);
        testAppender.start();
        context.updateLoggers();
    

    @AfterEach
    void after()
        testAppender.stop();
        loggerConfig.removeAppender("TestAppender");
        final LoggerContext context = logger.getContext();
        context.updateLoggers();
    

    @Plugin( name = "TestAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
    static class TestAppender extends AbstractAppender 

        List<LogEvent> events = new ArrayList();

        protected TestAppender(String name, Filter filter) 
            super(name, filter, null);
        

        @PluginFactory
        public static TestAppender createAppender(
                @PluginAttribute("name") String name,
                @PluginElement("Filter") Filter filter) 
            return new TestAppender(name, filter);
        

        @Override
        public void append(LogEvent event) 
            events.add(event);
        
    

    static class ClassUnderTest 
        private static final Logger LOGGER =  (Logger) LogManager.getLogger(ClassUnderTest.class);
        public static void logMessage()
            LOGGER.info("Hello Test");
            LOGGER.debug("Hello Test");
        
    

使用以下 maven 依赖项

 <dependency>
 <artifactId>log4j-core</artifactId>
  <packaging>jar</packaging>
  <version>2.11.2</version>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>

【讨论】:

我试过了,在 loggerConfig = configuration.getLoggerConfig(logger.getName()); 行的 setup 方法中出现错误错误是无法访问 org.apache.logging.log4j.spi.LoggerContextShutdownEnabled 的 org.apache.logging.log4j.spi.LoggerContextShutdownEnabled 类文件未找到 我查看了代码并做了一些小改动,但它对我有用。我建议您检查依赖关系并确保所有导入都正确 你好,Haim。我最终实现了 logback 解决方案......但我认为你是对的,为了实现那个我必须清理我对另一个 log4j 版本所做的导入。【参考方案24】:

请注意,在 Log4J 2.x 中,公共接口 org.apache.logging.log4j.Logger 不包括 setAppender()removeAppender() 方法。

但如果您没有做任何花哨的事情,您应该能够将其转换为实现类org.apache.logging.log4j.core.Logger,它确实公开了这些方法。

以下是Mockito 和AssertJ 的示例:

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

【讨论】:

【参考方案25】:

对于 Junit 5 (Jupiter),Spring 的 OutputCaptureExtension 非常有用。它从 Spring Boot 2.2 开始可用,并且在 spring-boot-test 工件中可用。

示例(取自 javadoc):

@ExtendWith(OutputCaptureExtension.class)
class MyTest 
    @Test
    void test(CapturedOutput output) 
        System.out.println("ok");
        assertThat(output).contains("ok");
        System.err.println("error");
    

    @AfterEach
    void after(CapturedOutput output) 
        assertThat(output.getOut()).contains("ok");
        assertThat(output.getErr()).contains("error");
    

【讨论】:

我认为日志语句不同于getOut()getErr() 这是我一直在寻找的答案(尽管问题与弹簧靴无关)! @Ram 不管是记录器被截获还是控制台上打印的实际日志记录字符串被截获都没有关系。如果记录器被配置为打印到控制台,它可以工作。【参考方案26】:

我也遇到了同样的问题,最后来到了这个页面。尽管我回答这个问题已经晚了 11 年,但我想也许它对其他人仍然有用。我发现 davidxxx 的答案与 Logback 和 ListAppander 非常有用。我对多个项目使用了相同的配置,但是当我需要更改某些内容时,复制/粘贴它并维护所有版本并不是那么有趣。我认为用它做一个图书馆并回馈社区会更好。它适用于 SLFJ4、Log4j、Log4j2、Java Util Logging 和 Lombok 注释。请在此处查看:LogCaptor 了解详细示例以及如何将其添加到您的项目中。

示例情况:

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

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

【讨论】:

放弃易于使用和更少的样板代码!对我来说很好的解决方案。谢谢! 您能添加导入吗? @Readren 刚刚添加了它【参考方案27】:

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

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

【讨论】:

【参考方案28】:

就我而言,我解决了与以下相同的问题:

Logger root = (Logger) LoggerFactory.getLogger(CSVTasklet.class); //CSVTasklet is my target class
    final Appender mockAppender = mock(Appender.class);
    root.addAppender(mockAppender); 

verify(mockAppender).doAppend(argThat((ArgumentMatcher) argument -> ((LoggingEvent) argument).getMessage().contains("No projects."))); // I checked "No projects." in the log

【讨论】:

【参考方案29】:

检查这个库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");

【讨论】:

假设你的服务中有这一行要测试:java public class YourService private static final Logger logger = LogManager.getLogger(YourService.class); 另外不要忘记为 assertThat 添加这个依赖项:java &lt;!-- Logger unit test solution--&gt; ... &lt;dependency&gt; &lt;groupId&gt;org.assertj&lt;/groupId&gt; &lt;artifactId&gt;assertj-core&lt;/artifactId&gt; &lt;version&gt;3.19.0&lt;/version&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; 【参考方案30】:

通过添加 Appender 进行单元测试并不能真正测试 Logger 的配置。所以,我认为这是单元测试没有带来那么多价值但集成测试带来很多价值的独特案例之一(尤其是如果您的日志记录有一些审计目的)

为了为其创建集成测试,让我们假设您正在使用简单的ConsoleAppender 运行并想要测试其输出。然后,您应该测试消息是如何从System.out 写入到它自己的ByteArrayOutputStream 中的。

从这个意义上说,我会执行以下操作(我使用的是 JUnit 5):

public class Slf4jAuditLoggerTest 

    private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();

    @BeforeEach
    public void beforeEach() 
        System.setOut(new PrintStream(outContent));
    

这样,您可以简单地测试它的输出:

    @Test
    public void myTest() 
        // Given...
        // When...
        // Then
        assertTrue(outContent.toString().contains("[INFO] My formatted string from Logger"));
    

如果您这样做,您将为您的项目带来更多价值,并且不需要使用内存中的实现、创建新的 Appender 或其他任何方式。

【讨论】:

以上是关于如何对记录器中的消息执行 JUnit 断言的主要内容,如果未能解决你的问题,请参考以下文章

Junit 单元测试

如何使用 JUnit 断言元素包含 Selenium 中的文本

JUnit5 中的 assertAll 与多个断言

Junit——Assert断言

JUnit断言失败[重复]

JUnit的各种断言