[Java] 如何做一个记录器中的消息断言JUnit


Answers

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

代码是我想要使用这个,使用java.util.logging作为其记录机制,并且我不觉得在这些代码中足够的家,以完全改变为log4j或记录器接口/外墙。 但是根据这些建议,我“篡改了”一个扩展程序,这就是一种享受。

一个简短的总结如下。 扩展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,但假设这是可能的。)




至于我,你可以通过在Mockito使用JUnit来简化你的测试。 我提出以下解决方案:

import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.times;

@RunWith(MockitoJUnitRunner.class)
public class MyLogTest {
    private static final String FIRST_MESSAGE = "First message";
    private static final String SECOND_MESSAGE = "Second message";
    @Mock private Appender appender;
    @Captor private ArgumentCaptor<LoggingEvent> captor;
    @InjectMocks private MyLog;

    @Before
    public void setUp() {
        LogManager.getRootLogger().addAppender(appender);
    }

    @After
    public void tearDown() {
        LogManager.getRootLogger().removeAppender(appender);
    }

    @Test
    public void shouldLogExactlyTwoMessages() {
        testedClass.foo();

        then(appender).should(times(2)).doAppend(captor.capture());
        List<LoggingEvent> loggingEvents = captor.getAllValues();
        assertThat(loggingEvents).extracting("level", "renderedMessage").containsExactly(
                tuple(Level.INFO, FIRST_MESSAGE)
                tuple(Level.INFO, SECOND_MESSAGE)
        );
    }
}

这就是为什么我们有不同的消息数量测试的灵活性




使用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.    
}



对于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(静态的,如果在配置文件中声明,或者通过它的当前引用,如果你插入运行时),并验证它的内容。




这是我做的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并获取最后一条消息,并验证消息,级别和可丢弃性。

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



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

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

我会做这样的事情:

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