如何在记录器中对消息执行JUnit断言


205

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

Answers:


142

我也需要几次。我在下面整理了一个小样本,您可以根据需要进行调整。基本上,您可以创建自己的Appender并将其添加到所需的记录器中。如果您想收集所有内容,那么根记录器是一个很好的起点,但是如果您愿意,可以使用更具体的信息。完成后,请不要忘记删除Appender,否则可能会导致内存泄漏。下面,我在测试中完成了此操作,但根据您的需要,setUp或者@Before和/ tearDown@After可能是更好的地方。

同样,下面的实现将所有内容收集到一个List内存中。如果记录很多,您可能会考虑添加一个过滤器以删除无聊的条目,或将日志写入磁盘上的临时文件(提示:LoggingEventis Serializable,因此,如果您的日志消息是,则应该能够序列化事件对象是。)

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

public class MyTest {
    @Test
    public void test() {
        final TestAppender appender = new TestAppender();
        final Logger logger = Logger.getRootLogger();
        logger.addAppender(appender);
        try {
            Logger.getLogger(MyTest.class).info("Test");
        }
        finally {
            logger.removeAppender(appender);
        }

        final List<LoggingEvent> log = appender.getLog();
        final LoggingEvent firstLogEntry = log.get(0);
        assertThat(firstLogEntry.getLevel(), is(Level.INFO));
        assertThat((String) firstLogEntry.getMessage(), is("Test"));
        assertThat(firstLogEntry.getLoggerName(), is("MyTest"));
    }
}

class TestAppender extends AppenderSkeleton {
    private final List<LoggingEvent> log = new ArrayList<LoggingEvent>();

    @Override
    public boolean requiresLayout() {
        return false;
    }

    @Override
    protected void append(final LoggingEvent loggingEvent) {
        log.add(loggingEvent);
    }

    @Override
    public void close() {
    }

    public List<LoggingEvent> getLog() {
        return new ArrayList<LoggingEvent>(log);
    }
}

4
这很好。我唯一要做的改进是调用logger.getAllAppenders(),然后逐步进行调用appender.setThreshold(Level.OFF)(并在完成后将其重置!)。这样可以确保您尝试生成的“不良”消息不会出现在测试日志中,并且会吓到下一个开发人员。
编码员

1
在Log4j 2.x中,由于您需要创建插件而有些复杂,请看以下内容:stackoverflow.com/questions/24205093/…–
paranza

1
谢谢你 但是,如果您使用的是LogBack,则可以使用ListAppender<ILoggingEvent>而不是创建自己的自定义附加程序。
sinujohn

2
但这对slf4j不起作用!您知道我该如何将其更改为与此同时使用吗?
士兰

3
@sd如果将强制转换Loggerorg.apache.logging.log4j.core.Logger(接口的实现类),则可以setAppender()/removeAppender()再次访问。
David Moles

59

这是一个简单高效的Logback解决方案。
不需要添加/创建任何新类。
它依赖于ListAppender:白盒logback追加器,在其中可以将日志条目添加public List到我们可以用来进行断言的字段中。

这是一个简单的例子。

Foo类:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Foo {

    static final Logger LOGGER = LoggerFactory.getLogger(Foo .class);

    public void doThat() {
        LOGGER.info("start");
        //...
        LOGGER.info("finish");
    }
}

FooTest类:

import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;

public class FooTest {

    @Test
    void doThat() throws Exception {
        // get Logback Logger 
        Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);

        // create and start a ListAppender
        ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
        listAppender.start();

        // add the appender to the logger
        // addAppender is outdated now
        fooLogger.addAppender(listAppender);

        // call method under test
        Foo foo = new Foo();
        foo.doThat();

        // JUnit assertions
        List<ILoggingEvent> logsList = listAppender.list;
        assertEquals("start", logsList.get(0)
                                      .getMessage());
        assertEquals(Level.INFO, logsList.get(0)
                                         .getLevel());

        assertEquals("finish", logsList.get(1)
                                       .getMessage());
        assertEquals(Level.INFO, logsList.get(1)
                                         .getLevel());
    }
}

JUnit断言听起来不太适合于断言列表元素的某些特定属性。
匹配器/断言库,例如AssertJ或Hamcrest似乎更好:

使用AssertJ,它将是:

import org.assertj.core.api.Assertions;

Assertions.assertThat(listAppender.list)
          .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
          .containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));

如果记录错误,如何停止测试失败?
Ghilteras

我不确定@Ghilteras。记录错误不应使测试失败。你解释什么?
davidxxx '18

另外,请记住不要mock测试中的类。您需要使用new运算符来实例化它
Dmytro Chasovskyi

35

非常感谢您提供这些快速而有用的答案;他们使我找到了解决问题的正确方法。

我想使用此代码库,将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()是使常规处理程序静音,以便(对于此junit-test运行)不会发生不必要的日志记录。做任何您要测试的代码才能使用此记录器,运行测试并使用assertEquality:

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

(当然,您会将这项工作的大部分内容转移到一种@Before方法中,并进行其他各种改进,但这会使本次演示变得混乱。)


16

另一个选项是模拟Appender并验证是否已将消息记录到此附加程序。Log4j 1.2.x和mockito的示例:

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

public class MyTest {

    private final Appender appender = mock(Appender.class);
    private final Logger logger = Logger.getRootLogger();

    @Before
    public void setup() {
        logger.addAppender(appender);
    }

    @Test
    public void test() {
        // when
        Logger.getLogger(MyTest.class).info("Test");

        // then
        ArgumentCaptor<LoggingEvent> argument = ArgumentCaptor.forClass(LoggingEvent.class);
        verify(appender).doAppend(argument.capture());
        assertEquals(Level.INFO, argument.getValue().getLevel());
        assertEquals("Test", argument.getValue().getMessage());
        assertEquals("MyTest", argument.getValue().getLoggerName());
    }

    @After
    public void cleanup() {
        logger.removeAppender(appender);
    }
}

16

有效地,您正在测试依赖类的副作用。对于单元测试,您只需要验证

logger.info()

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


3
您如何模拟大多数记录器定义的私有静态final字段?Powermockito?玩得开心..
Stefano L

Stefano:最后一个领域已经以某种方式初始化,我已经看到了各种注入Mocks的方法,而不是真实的东西。首先可能需要对可测试性进行某种级别的设计。blog.codecentric.de/en/2011/11/...
DJNA

正如Mehdi所说,可能使用合适的处理程序就足够了,
djna

11

在这里可以选择模拟,尽管很难,因为记录器通常是私有的static final-因此设置模拟记录器并不是小菜一碟,或者需要修改被测类。

您可以创建一个自定义的Appender(或任何它所谓的),并通过仅测试的配置文件或运行时进行注册(在某种程度上取决于日志框架)。然后,您可以获取该附加程序(如果是静态的(如果在配置文件中声明的话),或者是通过其当前引用(如果要插入运行时的话)),并验证其内容。


10

受到@RonaldBlaschke解决方案的启发,我想到了这一点:

public class Log4JTester extends ExternalResource {
    TestAppender appender;

    @Override
    protected void before() {
        appender = new TestAppender();
        final Logger rootLogger = Logger.getRootLogger();
        rootLogger.addAppender(appender);
    }

    @Override
    protected void after() {
        final Logger rootLogger = Logger.getRootLogger();
        rootLogger.removeAppender(appender);
    }

    public void assertLogged(Matcher<String> matcher) {
        for(LoggingEvent event : appender.events) {
            if(matcher.matches(event.getMessage())) {
                return;
            }
        }
        fail("No event matches " + matcher);
    }

    private static class TestAppender extends AppenderSkeleton {

        List<LoggingEvent> events = new ArrayList<LoggingEvent>();

        @Override
        protected void append(LoggingEvent event) {
            events.add(event);
        }

        @Override
        public void close() {

        }

        @Override
        public boolean requiresLayout() {
            return false;
        }
    }

}

...可让您执行以下操作:

@Rule public Log4JTester logTest = new Log4JTester();

@Test
public void testFoo() {
     user.setStatus(Status.PREMIUM);
     logTest.assertLogged(
        stringContains("Note added to account: premium customer"));
}

您可能可以使它以更聪明的方式使用hamcrest,但是我已经把它留在了这里。


6

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

5

正如其他人提到的,您可以使用模拟框架。为此,您必须在班上公开记录器(尽管我很可能更喜欢将记录器打包为私有的,而不是创建公共的设置器)。

另一个解决方案是手动创建一个伪造的记录器。您必须编写伪造的记录器(更多的夹具代码),但是在这种情况下,我希望相对于从模拟框架中保存的代码来增强测试的可读性。

我会做这样的事情:

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

5

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

4

这是我所做的注销操作。

我创建了一个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 name="TEST" class="com.intuit.icn.TestAppender">
    <encoder>
        <pattern>%m%n</pattern>
    </encoder>
</appender>

并将此附加器添加到根附加器中:

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

现在,在从父级测试类扩展的测试类中,我可以获取附加程序并获取记录的最后一条消息,并验证该消息,级别和可抛出对象。

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

我看不到在哪里定义了getAppender方法?
bioinfornatics

getAppender是ch.qos.logback.classic.Logger上的方法
kfox

4

对于Junit 5(Jupiter),Spring的OutputCaptureExtension非常有用。从Spring Boot 2.2开始可用,并且在spring-boot-test构件中可用。

示例(取自javadoc):

@ExtendWith(OutputCaptureExtension.class)
class MyTest {
    @Test
    void test(CapturedOutput output) {
        System.out.println("ok");
        assertThat(output).contains("ok");
        System.err.println("error");
    }

    @AfterEach
    void after(CapturedOutput output) {
        assertThat(output.getOut()).contains("ok");
        assertThat(output.getErr()).contains("error");
    }
}

我认为日志语句与getOut()或不同getErr()
拉姆

这就是我一直在寻找的答案(尽管这个问题与spring boot无关)!
helleye

3

至于我,你可以使用简化您的测试JUnitMockito。我为此提出以下解决方案:

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

这就是为什么我们对于具有不同消息量的测试具有很好的灵活性


1
为了不重复几乎相同的代码块,我想补充一点,对于Log4j2,几乎1to1对我有用。只需将导入更改为“ org.apache.logging.log4j.core”,将记录器转换为“ org.apache.logging.log4j.core.Logger”,添加 when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); 并更改
LoggingEvent-

3
Here is the sample code to mock log, irrespective of the version used for junit or sping, springboot.

import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
import org.mockito.ArgumentMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.Test;

import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class MyTest {
  private static Logger logger = LoggerFactory.getLogger(MyTest.class);

    @Test
    public void testSomething() {
    ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
    final Appender mockAppender = mock(Appender.class);
    when(mockAppender.getName()).thenReturn("MOCK");
    root.addAppender(mockAppender);

    //... do whatever you need to trigger the log

    verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
      @Override
      public boolean matches(final Object argument) {
        return ((LoggingEvent)argument).getFormattedMessage().contains("Hey this is the message I want to see");
      }
    }));
  }
}

1
这对我有用。我不需要行'when(mockAppender.getName())。thenReturn(“ MOCK”)'。
Mayank Raghav

1

Log4J2的API略有不同。另外,您可能正在使用其异步附加程序。我为此创建了一个锁存的附加程序:

    public static class LatchedAppender extends AbstractAppender implements AutoCloseable {

    private final List<LogEvent> messages = new ArrayList<>();
    private final CountDownLatch latch;
    private final LoggerConfig loggerConfig;

    public LatchedAppender(Class<?> classThatLogs, int expectedMessages) {
        this(classThatLogs, null, null, expectedMessages);
    }
    public LatchedAppender(Class<?> classThatLogs, Filter filter, Layout<? extends Serializable> layout, int expectedMessages) {
        super(classThatLogs.getName()+"."+"LatchedAppender", filter, layout);
        latch = new CountDownLatch(expectedMessages);
        final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
        final Configuration config = ctx.getConfiguration();
        loggerConfig = config.getLoggerConfig(LogManager.getLogger(classThatLogs).getName());
        loggerConfig.addAppender(this, Level.ALL, ThresholdFilter.createFilter(Level.ALL, null, null));
        start();
    }

    @Override
    public void append(LogEvent event) {
        messages.add(event);
        latch.countDown();
    }

    public List<LogEvent> awaitMessages() throws InterruptedException {
        assertTrue(latch.await(10, TimeUnit.SECONDS));
        return messages;
    }

    @Override
    public void close() {
        stop();
        loggerConfig.removeAppender(this.getName());
    }
}

像这样使用它:

        try (LatchedAppender appender = new LatchedAppender(ClassUnderTest.class, 1)) {

        ClassUnderTest.methodThatLogs();
        List<LogEvent> events = appender.awaitMessages();
        assertEquals(1, events.size());
        //more assertions here

    }//appender removed

1

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

但是,如果您没有做任何花哨的事情,则应该能够将其强制转换为实现类org.apache.logging.log4j.core.Logger,该实现类确实公开了这些方法。

这是MockitoAssertJ的示例:

// 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);

0

另一个值得一提的想法,尽管它是一个较旧的话题,但它是创建一个CDI生产者来注入您的记录器,从而使模拟变得容易。(这还具有不必再声明“整个记录器语句”的优点,但这是不合时宜的)

例:

创建要注入的记录器:

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

0

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

0

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

0

使用下面的代码。我在春季集成测试中使用了相同的代码,在该示例中,我使用了log back进行记录。使用方法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);
        }
    }));
}


0

您可能要尝试测试两件事。

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

这两件事实际上是不同的事情,因此可以分别进行测试。但是,测试第二个消息(消息文本)非常麻烦,我建议完全不要这样做。消息文本的测试最终将包括检查一个文本字符串(预期的消息文本)是否与日志记录代码中使用的文本字符串相同,或者可以从中简单地得出。

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

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

因此,我建议业务逻辑不要直接生成日志消息的文本。而是将其委托给日志记录对象。

  • 日志记录对象的类应提供合适的内部API,您的业务对象可使用该API来表达使用域模型的对象而不是文本字符串发生的事件。
  • 日志记录类的实现负责生成这些域对象的文本表示形式,并呈现事件的适当文本描述,然后将该文本消息转发到低级日志记录框架(例如JUL,log4j或slf4j)。
  • 您的业​​务逻辑仅负责调用记录器类的内部API的正确方法,传递正确的域对象,以描述发生的实际事件。
  • 您的具体日志记录类implementsan interface,描述了您的业务逻辑可能使用的内部API。
  • 您的实现业务逻辑并且必须执行日志记录的类具有对要委托给的日志记录对象的引用。参考的类是abstract 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);
    }
 }

0

如果我想做的是看到已记录一些字符串(而不是验证确切的日志语句,而这太脆了),我要做的就是将StdOut重定向到缓冲区,执行一个contains,然后重置StdOut:

PrintStream original = System.out;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer));

// Do something that logs

assertTrue(buffer.toString().contains(myMessage));
System.setOut(original);

1
我尝试过这样做java.util.logging(尽管我使用过System.setErr(new PrintStream(buffer));,因为它记录到stderr),但是它不起作用(缓冲区保持为空)。如果我System.err.println("foo")直接使用它,它将起作用,因此,我假定日志记录系统保留了它对from的输出流的引用System.err,因此我的调用对 System.setErr(..)日志输出没有影响,因为它在日志系统初始化之后发生。
hoijui

0

我为log4j回答了类似的问题,请参阅如何用Junit测试警告,并用log4记录警告

这是较新的示例,使用Log4j2(已通过2.11.2测试)和junit 5进行了示例;

    package com.whatever.log;

    import org.apache.logging.log4j.Level;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.core.Logger;
    import org.apache.logging.log4j.core.*;
    import org.apache.logging.log4j.core.appender.AbstractAppender;
    import org.apache.logging.log4j.core.config.Configuration;
    import org.apache.logging.log4j.core.config.LoggerConfig;
    import org.apache.logging.log4j.core.config.plugins.Plugin;
    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
    import org.apache.logging.log4j.core.config.plugins.PluginElement;
    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;

    import java.util.ArrayList;
    import java.util.List;
    import static org.junit.Assert.*;

class TestLogger {

    private TestAppender testAppender;
    private LoggerConfig loggerConfig;
    private final Logger logger = (Logger)
            LogManager.getLogger(ClassUnderTest.class);

    @Test
    @DisplayName("Test Log Junit5 and log4j2")
    void test() {
        ClassUnderTest.logMessage();
        final LogEvent loggingEvent = testAppender.events.get(0);
        //asset equals 1 because log level is info, change it to debug and
        //the test will fail
        assertTrue(testAppender.events.size()==1,"Unexpected empty log");
        assertEquals(Level.INFO,loggingEvent.getLevel(),"Unexpected log level");
        assertEquals(loggingEvent.getMessage().toString()
                ,"Hello Test","Unexpected log message");
    }

    @BeforeEach
    private void setup() {
        testAppender = new TestAppender("TestAppender", null);

        final LoggerContext context = logger.getContext();
        final Configuration configuration = context.getConfiguration();

        loggerConfig = configuration.getLoggerConfig(logger.getName());
        loggerConfig.setLevel(Level.INFO);
        loggerConfig.addAppender(testAppender,Level.INFO,null);
        testAppender.start();
        context.updateLoggers();
    }

    @AfterEach
    void after(){
        testAppender.stop();
        loggerConfig.removeAppender("TestAppender");
        final LoggerContext context = logger.getContext();
        context.updateLoggers();
    }

    @Plugin( name = "TestAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
    static class TestAppender extends AbstractAppender {

        List<LogEvent> events = new ArrayList();

        protected TestAppender(String name, Filter filter) {
            super(name, filter, null);
        }

        @PluginFactory
        public static TestAppender createAppender(
                @PluginAttribute("name") String name,
                @PluginElement("Filter") Filter filter) {
            return new TestAppender(name, filter);
        }

        @Override
        public void append(LogEvent event) {
            events.add(event);
        }
    }

    static class ClassUnderTest {
        private static final Logger LOGGER =  (Logger) LogManager.getLogger(ClassUnderTest.class);
        public static void logMessage(){
            LOGGER.info("Hello Test");
            LOGGER.debug("Hello Test");
        }
    }
}

使用以下Maven依赖项

 <dependency>
 <artifactId>log4j-core</artifactId>
  <packaging>jar</packaging>
  <version>2.11.2</version>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>

我尝试了这一点,并在loggerConfig = configuration.getLoggerConfig(logger.getName())行上的设置方法内出现错误。错误是无法访问org.apache.logging.log4j.spi.LoggerContextShutdownEnabled的org.apache.logging.log4j.spi.LoggerContextShutdownEnabled类文件未找到
carlos palma

我检查了代码并做了一些小的更改,但对我有用。我建议您检查依赖性并确保所有导入都是正确的
Haim Raman

你好,海姆。我最终实现了logback解决方案...但是我想您是对的,为了实现我必须清除由另一个log4j版本构成的导入文件。
卡洛斯·帕尔马

-1

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

解决方案是这样的:

  • 将log4j附加程序定义为ExternalResource规则

    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();
        }
    }
  • 定义使用您的ExternalResource规则的测试

    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的一部分

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.