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


当前回答

正如前面提到的,您可以使用mock框架。为此,您必须在类中公开记录器(尽管我可能更倾向于使其包私有而不是创建公共setter)。

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

我会这样做:

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

其他回答

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

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

解决方案是这样的:

Define a log4j appender as an ExternalResource rule 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(); } } Define a test that use your ExternalResource rule 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的一部分

您可能要测试两件事。

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

这两件事实际上是不同的,所以可以分别测试。然而,测试第二个(消息文本)问题很大,我建议不要这样做。消息文本的测试最终将包括检查一个文本字符串(预期的消息文本)是否与日志记录代码中使用的文本字符串相同,或者可以简单地从文本字符串派生出来。

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

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

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

The class of the logging object should provide a suitable internal API, which your business object can use to express the event that has occurred using objects of your domain model, not text strings. The implementation of your logging class is responsible for producing text representations of those domain objects, and rendering a suitable text description of the event, then forwarding that text message to the low level logging framework (such as JUL, log4j or slf4j). Your business logic is responsible only for calling the correct methods of the internal API of your logger class, passing the correct domain objects, to describe the actual events that occurred. Your concrete logging class implements an interface, which describes the internal API your business logic may use. Your class(es) that implements business logic and must perform logging has a reference to the logging object to delegate to. The class of the reference is the abstract interface. Use dependency injection to set up the reference to the logger.

然后,您可以通过创建一个模拟记录器(它实现了内部日志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);
    }
 }

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

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

logger.info()

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