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


当前回答

模拟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")));
}

其他回答

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

logger.info()

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

如果您正在使用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的一部分

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

但是,如果您没有做太花哨的事情,您应该能够将它强制转换为实现类org.apache.logging.log4j.core。记录器,它公开了这些方法。

这里有一个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);

简单的方法

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

另一个值得提及的想法是创建一个CDI生成器来注入记录器,这样模拟就变得容易了,尽管这是一个较老的主题。(而且它还提供了不必再声明“整个logger语句”的优势,但这已经跑题了)

例子:

创建要注入的记录器:

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