[Java] 如何在记录器中对消息进行JUnit声明


Answers

非常感谢这些(令人惊讶的)快速和有益的答案; 他们为我的解决方案提供了正确的方式。

代码库是我想要使用它,使用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(){}
}

显然,你可以像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);

setUseParentHandlers()的调用是为了setUseParentHandlers()正常的处理程序,所以(对于这个junit测试运行)不会发生不必要的日志记录。 做任何你的代码测试需要使用这个记录器,运行测试和assertEquality:

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

(当然,你会将大部分工作转移到@Before方法中,并进行其他改进,但这会使这个表示变得混乱。)

Question

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




使用下面的代码。 我为我的弹簧集成测试使用相同的代码,我使用日志记录进行日志记录。 使用方法assertJobIsScheduled来声明日志中打印的文本。

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;

private Logger rootLogger;
final Appender mockAppender = mock(Appender.class);

@Before
public void setUp() throws Exception {
    initMocks(this);
    when(mockAppender.getName()).thenReturn("MOCK");
    rootLogger = (Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
    rootLogger.addAppender(mockAppender);
}

private void assertJobIsScheduled(final String matcherText) {
    verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
        @Override
        public boolean matches(final Object argument) {
            return ((LoggingEvent)argument).getFormattedMessage().contains(matcherText);
        }
    }));
}



使用Jmockit(1.21)我能够编写这个简单的测试。 测试确保只调用一次特定的ERROR消息。

@Test
public void testErrorMessage() {
    final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger( MyConfig.class );

    new Expectations(logger) {{
        //make sure this error is happens just once.
        logger.error( "Something went wrong..." );
        times = 1;
    }};

    new MyTestObject().runSomethingWrong( "aaa" ); //SUT that eventually cause the error in the log.    
}



正如其他人所说,你可以使用一个模拟框架。 为了做到这一点,你必须在你的课堂上暴露记录器(尽管我会优先考虑将它打包为私有的,而不是创建公共设置器)。

另一种解决方案是手工制作假记录器。 你必须编写假记录器(更多的夹具代码),但在这种情况下,我希望增强的可读性对测试框架保存的代码进行测试。

我会做这样的事情:

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



这是我为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();
    }
}

然后在我的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();
    }
}

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

<appender name="TEST" class="com.intuit.icn.TestAppender">
    <encoder>
        <pattern>%m%n</pattern>
    </encoder>
</appender>

并将此appender添加到根appender:

<root>
    <level value="error" />
    <appender-ref ref="STDOUT" />
    <appender-ref ref="TEST" />
</root>

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

ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");



有两件事你可能试图测试。

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

这两件事实际上是不同的事情,所以可以单独测试。 然而,测试第二个(短信文本)是有问题的,我建议不要这样做。 对消息文本的测试最终将包括检查一个文本字符串(预期的消息文本)是否与日志代码中使用的文本字符串相同,或者可以平分派生。

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

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

因此,我建议业务逻辑不会直接生成日志消息文本。 而是让它委托给一个日志记录对象。

  • 日志记录对象的类应提供合适的内部API,您的业务对象可以使用该API来表示使用域模型的对象发生的事件,而不是文本字符串。
  • 日志记录类的实现负责生成这些域对象的文本表示,并为事件提供合适的文本描述,然后将该文本消息转发给低级日志记录框架(如JUL,log4j或slf4j)。
  • 您的业​​务逻辑仅负责调用记录器类的内部API的正确方法,传递正确的域对象以描述发生的实际事件。
  • 您的具体日志记录类implements了一个interface ,该interface描述了业务逻辑可能使用的内部API。
  • 实现业务逻辑并且必须执行日志记录的类具有对要委派的日志记录对象的引用。 引用的类是抽象interface
  • 使用依赖注入来设置对记录器的引用。

然后,您可以测试您的业务逻辑类是否正确地告诉记录界面关于事件的信息,方法是创建一个模拟记录器,该记录器实现内部记录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);
    }
 }



哇。 我不确定为什么这么难。 我发现我无法使用上面的任何代码示例,因为我通过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));
  }
}



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



嘲笑是一种选择,虽然这很难,因为记录器通常是私有的静态最终 - 所以设置模拟记录器不是小菜一碟,或者需要修改被测试的类。

你可以创建一个自定义的Appender(或者其他所谓的),并且通过一个仅测试的配置文件或者运行时(依赖于日志框架)来注册它。 然后你可以得到该appender(静态的,如果在配置文件中声明的话,或者通过它的当前引用,如果你插入运行时),并验证它的内容。