如何使用JUnit和Log4j测试日志记录中是否产生了警告?

58
我正在测试一种方法,当出现错误并返回null时记录警告信息。
类似于:
private static final Logger log = Logger.getLogger(Clazz.class.getName());
....
if (file == null || !file.exists()) {
  // if File not found
  log.warn("File not found: "+file.toString());
} else if (!file.canWrite()) {
  // if file is read only
  log.warn("File is read-only: "+file.toString());
} else {
  // all checks passed, can return an working file.
  return file;
}
return null;

我希望使用JUnit进行测试,以确保在所有情况下(例如文件未找到、文件只读)都会发出警告并返回null。
有什么想法吗?
谢谢,阿萨夫:-)

更新

Aaron答案的实现(加上Peter的备注):

public class UnitTest {
...

@BeforeClass
public static void setUpOnce() {
  appenders = new Vector<Appender>(2);
  // 1. just a printout appender:
  appenders.add(new ConsoleAppender(new PatternLayout("%d [%t] %-5p %c - %m%n")));
  // 2. the appender to test against:
  writer = new StringWriter();
  appenders.add(new WriterAppender(new PatternLayout("%p, %m%n"),writer));
}

@Before
public void setUp() {
  // Unit Under Test:
  unit = new TestUnit();
  // setting test appenders:
  for (Appender appender : appenders) {
    TestUnit.log.addAppender(appender);
  }
  // saving additivity and turning it off:
  additivity = TestUnit.log.getAdditivity();
  TestUnit.log.setAdditivity(false);
}

@After
public void tearDown() {
  unit = null;
  for (Appender appender : appenders) {
    TestUnit.log.removeAppender(appender);
  }
  TestUnit.log.setAdditivity(additivity);
}

@Test
public void testGetFile() {
  // start fresh:
  File file;
  writer.getBuffer().setLength(0);

  // 1. test null file:
  System.out.println(" 1. test null file.");
  file = unit.getFile(null);
  assertNull(file);
  assertTrue(writer.toString(), writer.toString().startsWith("WARN, File not found"));
  writer.getBuffer().setLength(0);

  // 2. test empty file:
  System.out.println(" 2. test empty file.");
  file = unit.getFile("");
  assertNull(file);
  assertTrue(writer.toString(), writer.toString().startsWith("WARN, File not found"));
  writer.getBuffer().setLength(0);
}

感谢大家,
谢谢。

2
第一个if块中有一个错误,如果file == null为真,则log.warn中的file.toString()将会抛出异常。 - Bert F
2
如果您正在使用log4j2,请不要陷入尝试实现此处提供的原始log4j解决方案的困境。以下是对我有效的方法: https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ - Skystrider
如果您正在使用log4j,但仍然找不到解决方案,请访问https://codingcraftsman.wordpress.com/2015/04/28/log4j2-mocking-with-mockito-and-junit/。我调试了测试,并发现Logger实例与代码中的Logger不同,因此没有写入日志事件。运行时的Logger是org.apache.logging.log4j.core.Logger,所以我必须明确指定要使用的Logger。我必须使用org.apache.logging.log4j.LogManager来获取它。 还必须使用org.apache.logging.log4j.core.Appender。 在断言日志之前,还进行了Thread.sleep。 - undefined
6个回答

45
在单元测试的设置中:
  1. 获取相同的日志记录器
  2. 将其设置为非累加模式
  3. 添加一个记录消息到列表中的 appender:
public class TestAppender extends AppenderSkeleton {
    public List<String> messages = new ArrayList<String>();

    public void doAppend(LoggingEvent event) {
        messages.add( event.getMessage().toString() );
    }
}
  • 将附加器添加到记录器上

  • 现在您可以调用您的代码。 测试后,您将在列表中找到所有日志消息。 如果需要,添加日志级别(messages.add( event.getLevel() + " " + event.getMessage() );)。

    tearDown() 中再次删除附加器并启用可添加性。


    1
    最好的方法是将消息制作成LoggingEvents列表 - 这样您可以按照自己的方式处理它们,而不必处理字符串表示形式。 - PaulJWilliams
    @Visage:在我看来,比较字符串可以使测试更易读。 - Asaf
    你是对的 - 但你应该在功能运行后再进行比较。存储LoggingEvents并不妨碍您在Asserts中使用它们的toString方法,而仅存储它们的字符串表示可能会丢失数据。 - PaulJWilliams
    4
    在调用doAppend()后,没有任何东西可以阻止event发生改变。toString()将确保您获得快照。 - Aaron Digulla
    如果您正在使用log4j2,请不要陷入尝试实施此处提供的原始log4j的解决方案中。以下是我使用的解决方案: https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ - Skystrider

    22

    使用Mockito,您可以在最小的样板代码下测试测试期间发生的记录日志,以下是一个简单的示例:

    @RunWith(MockitoJUnitRunner.class)
    public class TestLogging {
    
      @Mock AppenderSkeleton appender;
      @Captor ArgumentCaptor<LoggingEvent> logCaptor;
    
    
      @Test
      public void test() {
        Logger.getRootLogger().addAppender(appender);
    
        ...<your test code here>...
    
        verify(appender).doAppend(logCaptor.capture());
        assertEquals("Warning message should have been logged", "Caution!", logCaptor.getValue().getRenderedMessage());
      }
    }
    

    2
    很好。在我的logback版本(1.1.7)中缺少AppenderSkeleton,所以我使用了Appender<ILoggingEvent> appender... - mike rodent
    1
    ILoggingEvent不是.NET接口吗?这是一个Java问题。 - Stealth Rabbi

    10

    这篇文章中的示例很有帮助,但我觉得有点令人困惑。
    因此,我添加了一个简化版本,并进行了一些小修改。

    • 我将我的appender添加到根记录器中。

    这样,假设默认情况下是可加性的,我就不需要担心由于记录器层次结构而丢失我的事件。请确保这符合您的log4j.properties文件配置。

    • 我重写了append而不是doAppend。

    AppenderSkeleton中的Append处理级别过滤,因此我不想错过它。
    如果级别正确,doAppend将调用append。

    public class TestLogger {
        @Test
        public void test() {
            TestAppender testAppender = new TestAppender();
    
            Logger.getRootLogger().addAppender(testAppender);
            ClassUnderTest.logMessage();
            LoggingEvent loggingEvent = testAppender.events.get(0);
            //asset equals 1 because log level is info, change it to debug and 
            //the test will fail
            assertTrue("Unexpected empty log",testAppender.events.size()==1);               
            assertEquals("Unexpected log level",Level.INFO,loggingEvent.getLevel());
            assertEquals("Unexpected log message"
                            ,loggingEvent.getMessage().toString()
                            ,"Hello Test");
        }
    
        public static class TestAppender extends AppenderSkeleton{
            public List<LoggingEvent> events = new ArrayList<LoggingEvent>();
            public void close() {}
            public boolean requiresLayout() {return false;}
            @Override
            protected void append(LoggingEvent event) {
                          events.add(event);
              }     
        }
    
        public static class ClassUnderTest {
            private static final Logger LOGGER = 
                Logger.getLogger(ClassUnderTest.class);
            public static void logMessage(){
              LOGGER.info("Hello Test");
              LOGGER.debug("Hello Test");
            }
        }
    }
    

    log4j.properties

    log4j.rootCategory=INFO, CONSOLE
    log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
    log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
    log4j.appender.CONSOLE.layout.ConversionPattern=%d %p  [%c] - %m%n
    # un-comment this will fail the test   
    #log4j.logger.com.haim.logging=DEBUG
    

    这对我很有效。我喜欢这种解决方案的改编。 - Skystrider
    1
    在log4j2.x版本中,我们怎样编写等效的代码? - PAA

    7

    除了Aaron的解决方案外,另一种方法是使用附加的StringWriter配置WriterAppender。在测试结束时,您可以验证日志输出字符串的内容。

    这种方法实现起来比较容易(不需要自定义代码),但是在检查结果方面不够灵活,因为您只能获得纯文本格式的输出。在某些情况下,这可能会使验证输出结果比Aaron的解决方案更加困难。


    2

    不要直接调用log4j,而是在你的类中使用一个受保护的方法。

    例如:

    protected void log(String message, Level level)
    {
        //delegates to log4j
    }
    

    然后创建一个测试类的子类,覆盖这个方法,以便您可以验证它是否按预期调用。

    class MyTest extends <class under test>
    {
        boolean somethingLogged = false;
        protected void log(String message, Level level)
        {
            somethingLogged = true;
        }
    }
    

    然后基于somethingLogged进行断言。您可以在覆盖的测试方法中添加条件逻辑,根据预期的消息/级别进行测试。

    您还可以进一步记录所有调用,然后搜索记录的消息,或检查它们是否按正确顺序记录等...


    3
    作为最后的选择这样做是可以的,但最好使用现有的Log4J功能——这样更加便携和可重用。 - Péter Török
    10倍,但我希望尽可能减少对测试的干扰。此外,您的解决方案非常冗长,需要一个中央日志记录设施和大量编码。在我看来。 - Asaf
    2
    我不建议在生产代码中编写受保护的方法来测试某些东西。应该寻找另一种解决方案。 - Tzen
    我找到了这个论坛。加入的Java代码中没有导入。我该如何找出使用了哪个框架? - Sofiane

    1

    我将Haim的回答改编成更符合RAII原则的形式:

    public static class TestAppender extends AppenderSkeleton {
        @Override
        protected void append(LoggingEvent event) {
            messages.add(event.getRenderedMessage());
        }
    
        @Override
        public void close() { }
    
        @Override
        public boolean requiresLayout() { return false; }
    
        protected final List<String> messages = new ArrayList<>();
    }
    
    static class LogGuard implements AutoCloseable {
        protected final TestAppender appender;
    
        LogGuard(Level level) {
            appender = new TestAppender();
            appender.setThreshold(level);
            Logger.getRootLogger().addAppender(appender);
        }
    
        @Override
        public void close() throws Exception {
            Logger.getRootLogger().removeAppender(appender);
        }
    }
    

    然后使用方法就是简单的:

    try (LogGuard log = new LogGuard(Level.WARN)) { // if you want WARN or higher
        // do what causes the logging
       Assert.assertTrue(log.appender.messages.stream().anyMatch(m -> m.equals("expected"));
    }
    

    我们如何在log4j2.x中实现相同的功能? - PAA

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接