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

Jon*_*Jon 177 java logging junit assert

我有一些测试代码,它调用Java记录器来报告其状态.在JUnit测试代码中,我想验证在此记录器中是否输入了正确的日志.以下内容:

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

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

我想这可以通过特殊改编的记录器(或处理程序或格式化程序)来完成,但我更愿意重用已经存在的解决方案.(而且,说实话,我不清楚如何从记录器获取logRecord,但假设这是可能的.)

Ron*_*hke 137

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

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 但这对 slf4j 不起作用!你知道我怎样才能改变它来处理它吗? (5认同)
  • 这非常有效.我要做的唯一改进是调用`logger.getAllAppenders()`,然后逐步调用每个调用`appender.setThreshold(Level.OFF)`(并在完成后重置它们!).这可以确保您尝试生成的"错误"消息不会显示在测试日志中,并使下一个开发人员感到震惊. (4认同)
  • 在 Log4j 2.x 中稍微复杂一些,因为您需要创建一个插件,请看一下:http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2 (3认同)
  • @sd 如果将 `Logger` 转换为 `org.apache.logging.log4j.core.Logger` (接口的实现类),您将再次访问 `setAppender()/removeAppender()`。 (3认同)
  • 谢谢你。但如果您使用 LogBack,则可以使用“ListAppender&lt;ILoggingEvent&gt;”,而不是创建自己的自定义附加程序。 (2认同)

dav*_*xxx 36

这是一个简单而有效的Logback解决方案.
它不需要添加/创建任何新类.
它依赖于ListAppender:一个whitebox 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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());
    }
}
Run Code Online (Sandbox Code Playgroud)

JUnit断言听起来不太适合断言列表元素的某些特定属性.
AscherJ或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));
Run Code Online (Sandbox Code Playgroud)

  • 以防万一找不到 `addAppender` 方法,您需要将 fooLogger 转换为一个实现类,例如 `ch.qos.logback.classic.Logger`。然后你就有了`addAppender`方法 (11认同)
  • 就我而言,附加程序的“列表”为空,直到我获得根记录器(不是类记录器):“Logger logger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME)” (3认同)

Jon*_*Jon 34

非常感谢这些(令人惊讶的)快速而有用的答案; 他们让我以正确的方式解决问题.

代码库是我想要使用它,使用java.util.logging作为其记录器机制,我觉得在这些代码中没有足够的东西完全改变它到log4j或记录器接口/外观.但基于这些建议,我"抨击"一个julhandler扩展,并作为一种享受.

简短摘要如下.延伸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(){}
}
Run Code Online (Sandbox Code Playgroud)

显然,您可以根据需要存储/想要/需要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);
Run Code Online (Sandbox Code Playgroud)

调用setUseParentHandlers()是为了使正常处理程序静音,以便(对于此junit-test运行)不会发生不必要的日志记录.无论您的测试代码需要使用此记录器,运行测试和断言均衡:

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

(当然,你会将这项工作的大部分内容转移到一个@Before方法中并进行各种其他改进,但这会使这个演示文稿变得混乱.)


aem*_*aem 17

对于 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这就是我一直在寻找的答案(尽管问题与 spring boot 无关)! (5认同)
  • 哇!就是这个。无需使用记录器的静态模拟。捕获实际输出。 (3认同)
  • @Ram 记录器被拦截还是打印在控制台上的实际日志字符串被拦截并不重要。如果记录器配置为打印到控制台,则无论哪种方式都可以工作。 (2认同)

djn*_*jna 15

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

logger.info()

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

  • 您如何模拟大多数记录器定义的私有静态最终字段?威力摩托?玩得开心.. (8认同)
  • @StefanoL:如果使用“private static final Logger LOGGER = Logger.getLogger(someString);”定义记录器,您仍然可以使用“Logger.getLogger(someString);”从单元测试中访问它,修改它并添加处理程序(正如已接受的答案中所示)。 (2认同)

Mar*_*cin 15

另一种选择是模拟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);
    }
}
Run Code Online (Sandbox Code Playgroud)


Boz*_*zho 10

模拟是一个选项,虽然它很难,因为记录器通常是私有静态最终 - 因此设置模拟记录器不会是小菜一碟,或者需要修改被测试的类.

您可以创建自定义Appender(或其任何名称)并注册它 - 通过仅测试配置文件或运行时(在某种程度上,取决于日志记录框架).然后你可以得到那个appender(静态地,如果在配置文件中声明,或者通过它的当前引用,如果你正在插入运行时),并验证它的内容.


jos*_*eph 8

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

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());
    }
}
Run Code Online (Sandbox Code Playgroud)


sli*_*lim 7

受@ 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;
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

...允许你这样做:

@Rule public Log4JTester logTest = new Log4JTester();

@Test
public void testFoo() {
     user.setStatus(Status.PREMIUM);
     logTest.assertLogged(
        stringContains("Note added to account: premium customer"));
}
Run Code Online (Sandbox Code Playgroud)

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


Dag*_*mar 7

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

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));
  }
}
Run Code Online (Sandbox Code Playgroud)

  • @AdrienW 感谢您分享您的解决方案,我很高兴您现在可以使用它了! (2认同)

小智 6

最简单的方法

  @ExtendWith(OutputCaptureExtension.class)
  class MyTestClass { 
    
          @Test
          void my_test_method(CapturedOutput output) {
               assertThat(output).contains("my test log.");
          }
  }
Run Code Online (Sandbox Code Playgroud)


Arn*_*sch 5

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

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

我会做这样的事情:

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());
    }
}
Run Code Online (Sandbox Code Playgroud)


kfo*_*fox 5

这是我为 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

然后在我的 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

<appender name="TEST" class="com.intuit.icn.TestAppender">
    <encoder>
        <pattern>%m%n</pattern>
    </encoder>
</appender>
Run Code Online (Sandbox Code Playgroud)

并将这个 appender 添加到根 appender:

<root>
    <level value="error" />
    <appender-ref ref="STDOUT" />
    <appender-ref ref="TEST" />
</root>
Run Code Online (Sandbox Code Playgroud)

现在,在从父测试类扩展的测试类中,我可以获取 appender 并记录最后一条消息并验证消息、级别、可抛出。

ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
Run Code Online (Sandbox Code Playgroud)


Hak*_*n54 5

我也遇到了同样的挑战并最终到达了这个页面。虽然我回答这个问题已经晚了 11 年,但我想也许它对其他人仍然有用。我发现davidxxxLogback 和 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!");
    }

}
Run Code Online (Sandbox Code Playgroud)

使用 LogCaptor 的示例单元测试:

public class FooServiceTest {

    private LogCaptor logCaptor = LogCaptor.forClass(FooService.class);

    @Test
    public void logInfoAndWarnMessages() {
        FooService fooService = new FooService();
        fooService.sayHello();

        assertThat(logCaptor.getWarnLogs())
            .contains("Congratulations, you are pregnant!");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 退出易于使用且样板代码更少!对我来说很好的解决方案。谢谢! (2认同)
  • @Readren 刚刚添加了它 (2认同)