Asserting Log Messages With JUnit – 用JUnit断言日志信息

最后修改: 2020年 5月 4日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In this tutorial, we’ll look at how we can cover generated logs in JUnit testing.

在本教程中,我们将看看我们如何在JUnit测试中涵盖生成的日志

We’ll use the slf4j-api and the logback implementation and create a custom appender that we can use for log assertion.

我们将使用slf4j-apilogback实现,并创建一个我们可以用于日志断言的自定义appender

2. Maven Dependencies

2.Maven的依赖性

Before we begin, let’s add the logback dependency. As it natively implements the slf4j-api, it is automatically downloaded and injected into the project by Maven transitivity:

在开始之前,我们先添加logback依赖项。由于它原生实现了slf4j-api,它被Maven自动下载并注入项目中。

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>. 
    <version>1.2.6</version>
</dependency>

AssertJ offers very useful functions when testing, so let’s add its dependency to the project as well:

AssertJ在测试时提供了非常有用的功能,所以让我们把它的依赖性也添加到项目中。

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.15.0</version>
    <scope>test</scope>
</dependency>

3. A Basic Business Function

3.一个基本的商业功能

Now, let’s create an object that will generate logs we can base our tests on.

现在,让我们创建一个对象,该对象将生成我们可以依据的测试日志。

Our BusinessWorker object will only expose one method. This method will generate a log with the same content for each log level. Although this method isn’t that useful in the real world, it’ll serve well for our testing purposes:

我们的BusinessWorker对象将只暴露一个方法。这个方法将为每个日志级别生成一个内容相同的日志。尽管这个方法在现实世界中并不那么有用,但对于我们的测试目的来说,它将起到很好的作用。

public class BusinessWorker {
    private static Logger LOGGER = LoggerFactory.getLogger(BusinessWorker.class);

    public void generateLogs(String msg) {
        LOGGER.trace(msg);
        LOGGER.debug(msg);
        LOGGER.info(msg);
        LOGGER.warn(msg);
        LOGGER.error(msg);
    }
}

4. Testing the Logs

4.测试日志

We want to generate logs, so let’s create a logback.xml file in the src/test/resources folder. Let’s keep it as simple as possible and redirect all logs to a CONSOLE appender:

我们想要生成日志,所以让我们在src/test/resources文件夹中创建一个logback.xml文件。让我们尽可能地保持简单,把所有的日志重定向到一个CONSOLEappender。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>
                %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
            </Pattern>
        </layout>
    </appender>

    <root level="error">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

4.1. MemoryAppender

4.1.MemoryAppender

Now, let’s create a custom appender that keeps logs in memory. We’ll extend the ListAppender<ILoggingEvent> that logback offers, and we’ll enrich it with a few useful methods:

现在,让我们创建一个自定义appender,将日志保存在内存中。我们将扩展ListAppender<ILoggingEvent>logback提供,我们将用一些有用的方法来丰富它。

public class MemoryAppender extends ListAppender<ILoggingEvent> {
    public void reset() {
        this.list.clear();
    }

    public boolean contains(String string, Level level) {
        return this.list.stream()
          .anyMatch(event -> event.toString().contains(string)
            && event.getLevel().equals(level));
    }

    public int countEventsForLogger(String loggerName) {
        return (int) this.list.stream()
          .filter(event -> event.getLoggerName().contains(loggerName))
          .count();
    }

    public List<ILoggingEvent> search(String string) {
        return this.list.stream()
          .filter(event -> event.toString().contains(string))
          .collect(Collectors.toList());
    }

    public List<ILoggingEvent> search(String string, Level level) {
        return this.list.stream()
          .filter(event -> event.toString().contains(string)
            && event.getLevel().equals(level))
          .collect(Collectors.toList());
    }

    public int getSize() {
        return this.list.size();
    }

    public List<ILoggingEvent> getLoggedEvents() {
        return Collections.unmodifiableList(this.list);
    }
}

The MemoryAppender class handles a List that is automatically populated by the logging system.

MemoryAppender类处理一个List,它是由日志系统自动填充的。

It exposes a variety of methods in order to cover a wide range of test purposes:

它暴露了各种方法,以涵盖广泛的测试目的。

  • reset() – clears the list
  • contains(msg, level) – returns true only if the list contains an ILoggingEvent matching the specified content and severity level
  • countEventForLoggers(loggerName) – returns the number of ILoggingEvent generated by named logger
  • search(msg) – returns a List of ILoggingEvent matching the specific content
  • search(msg, level) – returns a List of ILoggingEvent matching the specified content and severity level
  • getSize() – returns the number of ILoggingEvents
  • getLoggedEvents() – returns an unmodifiable view of the ILoggingEvent elements

4.2. Unit Test

4.2.单位测试

Next, let’s create a JUnit test for our business worker.

接下来,让我们为我们的业务工作者创建一个JUnit测试。

We’ll declare our MemoryAppender as a field and programmatically inject it into the log system. Then, we’ll start the appender.

我们将声明我们的MemoryAppender为一个字段,并以编程方式将其注入到日志系统中。然后,我们将启动appender。

For our tests, we’ll set the level to DEBUG:

对于我们的测试,我们将设置级别为DEBUG

@Before
public void setup() {
    Logger logger = (Logger) LoggerFactory.getLogger(LOGGER_NAME);
    memoryAppender = new MemoryAppender();
    memoryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
    logger.setLevel(Level.DEBUG);
    logger.addAppender(memoryAppender);
    memoryAppender.start();
}

Now we can create a simple test where we instantiate our BusinessWorker class and call the generateLogs method. We can then make assertions on the logs that it generates:

现在我们可以创建一个简单的测试,将我们的BusinessWorker类实例化并调用generateLogs方法。然后我们可以对其生成的日志进行断言。

@Test
public void test() {
    BusinessWorker worker = new BusinessWorker();
    worker.generateLogs(MSG);
        
    assertThat(memoryAppender.countEventsForLogger(LOGGER_NAME)).isEqualTo(4);
    assertThat(memoryAppender.search(MSG, Level.INFO).size()).isEqualTo(1);
    assertThat(memoryAppender.contains(MSG, Level.TRACE)).isFalse();
}

This test uses three features of the MemoryAppender:

这个测试使用了MemoryAppender的三个特性。

  • Four logs have been generated — one entry per severity should be present, with the trace level filtered
  • Only one log entry with the content message with the level severity of INFO
  • No log entry is present with content message and severity TRACE

If we plan to use the same instance of this class inside the same test class when generating a lot of logs, the memory usage will creep up. We can invoke the MemoryAppender.clear() method before each test to free memory and avoid OutOfMemoryException.

如果我们计划在生成大量的日志时,在同一个测试类里面使用这个类的同一个实例,那么内存的使用量就会逐渐增加。我们可以在每次测试前调用MemoryAppender.clear()方法来释放内存,避免OutOfMemoryException

In this example, we’ve reduced the scope of the retained logs to the LOGGER_NAME package, which we defined as “com.baeldung.junit.log“. We could potentially retain all logs with LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME), but we should avoid this whenever possible as it can consume a lot of memory.

在这个例子中,我们将保留日志的范围缩小到LOGGER_NAME包,我们将其定义为”com.baeldung.junit.log“。我们有可能用LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)来保留所有的日志,但我们应该尽可能避免这样做,因为这样会消耗大量的内存

5. Conclusion

5.总结

With this tutorial, we’ve demonstrated how to cover log generation in our unit tests.

通过本教程,我们已经演示了如何在单元测试中涵盖日志生成

As always, the code can be found over on GitHub.

一如既往,代码可以在GitHub上找到over