如何对日志中的消息进行JUnit断言

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

331977 次浏览

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

logger.info()

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

在这里,模拟是一个选项,尽管这很困难,因为记录器通常是私有的静态final -所以设置模拟记录器不是小意思,或者需要修改被测试的类。

你可以创建一个自定义的Appender(或者不管它叫什么),并注册它——要么通过一个只测试的配置文件,要么通过运行时(在某种程度上,依赖于日志框架)。 然后,您可以获得该appender(如果在配置文件中声明,可以是静态的,如果在运行时插入它,则可以通过它的当前引用),并验证其内容

正如前面提到的,您可以使用mock框架。为此,您必须在类中公开记录器(尽管我可能更倾向于使其包私有而不是创建公共setter)。

另一种解决方案是手工创建一个假记录器。您必须编写伪记录器(更多的fixture代码),但在这种情况下,我更喜欢使用模拟框架中保存的代码来增强测试的可读性。

我会这样做:

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

我也用过好几次。我在下面整理了一个小样本,您可以根据自己的需要进行调整。基本上,你创建自己的Appender并将其添加到你想要的记录器。如果您想收集所有内容,根日志记录器是一个很好的开始,但如果您愿意,您可以使用更具体的。完成后不要忘记删除Appender,否则可能会造成内存泄漏。下面我已经在测试中完成了,但setUp@BeforetearDown@After可能是更好的地方,这取决于你的需要。

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

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

非常感谢这些(令人惊讶的)快速而有用的回答;他们让我找到了正确的解决方法。

我想要使用的代码库使用java.util.logging作为其记录器机制,我对这些代码感到不够熟悉,无法完全将其更改为log4j或记录器接口/facade。但基于这些建议,我“破解”了一个j.u.l handler扩展,这是一种享受。

下面是一个简短的总结。延长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方法中,并进行各种各样的改进,但这会使这个演示变得混乱。)

这是我为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(), "...");

另一个选项是模拟Appender并验证消息是否已记录到此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);
}
}

另一个值得提及的想法是创建一个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();
}

受到@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,但我就讲到这里。

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

mock Appender可以帮助捕获日志行。 Find sample on: 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")));
}
对我来说,你可以通过使用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)
);
}
}

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

使用下面的代码。我在spring集成测试中使用相同的代码,其中我使用日志回日志。使用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);
}
}));
}

对于log4j2,解决方案略有不同,因为AppenderSkeleton不再可用。此外,使用Mockito或类似的库来创建带有ArgumentCaptor的Appender将无法工作,因为MutableLogEvent在多个日志消息上被重用。我为log4j2找到的最佳解决方案是:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;


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());
}
}
这里有一个很好的和优雅的方法来解决这个问题: https://www.baeldung.com/junit-asserting-logs < / p >

您可能要测试两件事。

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

这两件事实际上是不同的,所以可以分别测试。然而,测试第二个(消息文本)问题很大,我建议不要这样做。消息文本的测试最终将包括检查一个文本字符串(预期的消息文本)是否与日志记录代码中使用的文本字符串相同,或者可以简单地从文本字符串派生出来。

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

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

因此,我建议业务逻辑不要直接生成日志消息的文本。相反,让它委托给一个日志对象。

  • 日志对象的类应该提供合适的内部API,业务对象可以使用该API来表示使用域模型的对象(而不是文本字符串)发生的事件。
  • 日志类的实现负责生成这些域对象的文本表示,并呈现事件的适当文本描述,然后将该文本消息转发到低级日志框架(如JUL、log4j或slf4j)。
  • 您的业务逻辑只负责调用记录器类的内部API的正确方法,传递正确的域对象,以描述实际发生的事件。
  • 你的具体日志类implementsinterface,它描述了你的业务逻辑可能使用的内部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);
}
}

如果我想做的只是看到一些字符串被记录(而不是验证确切的日志语句,这太脆弱了),我所做的是将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);

哇。我不知道为什么这么难。我发现我无法使用上面的任何代码示例,因为我使用的是log4j2而不是slf4j。这是我的解决方案:

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的API略有不同。你也可以使用它的async appender。我为此创建了一个锁定的appender:

    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

这里有一个简单有效的Logback解决方案 它不需要添加/创建任何新类 它依赖于ListAppender:一个白盒回log appender,其中日志条目被添加到public List字段中,我们可以使用它来创建断言。< / p >

这里有一个简单的例子。

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

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

解决方案是这样的:

  • 将log4j appender定义为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();
    }
    }
    
  • 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"));
    }
    }
    

Don't forget to have log4j2.xml as part of src/test/resources

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

我回答了log4j类似的问题,见how-can-i-test-with-junit-that-a-warning-was-logged-with-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>

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

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

下面是一个包含5AssertJ的例子:

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

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

我也遇到了同样的挑战,最后来到了这一页。虽然我已经晚了11年才回答这个问题,但我想也许它对其他人仍然有用。我发现davidxxx的答案与Logback和ListAppander非常有用。我在多个项目中使用了相同的配置,但是当我需要更改某些内容时,复制/粘贴它并维护所有版本并不是那么有趣。我觉得把它建成一个图书馆,回馈社区会更好。它与SLFJ4, Log4j, Log4j2, Java Util日志,JBoss日志和Lombok注释一起工作。请看看这里:LogCaptor详细的例子,以及如何将它添加到您的项目。

示例情况:

public class FooService {


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


public void sayHello() {
LOGGER.warn("Congratulations, you are pregnant!");
}


}

使用LogCaptor的单元测试示例:

import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;


import static org.assertj.core.api.Assertions.assertThat;


public class FooServiceTest {


@Test
public void sayHelloShouldLogWarnMessage() {
LogCaptor logCaptor = LogCaptor.forClass(FooService.class);


FooService fooService = new FooService();
fooService.sayHello();


assertThat(logCaptor.getWarnLogs())
.contains("Congratulations, you are pregnant!");
}
}

我不太确定是否应该把这个贴在这里,因为这也可以被看作是推广“我的图书馆”的一种方式。但我认为这对面临同样挑战的开发人员有帮助。

在类实现中不需要依赖硬编码的静态全局记录器,可以在默认构造函数中提供默认记录器,然后使用特定的构造函数设置对所提供的记录器的引用。

class MyClassToTest {
private final Logger logger;
    

public MyClassToTest() {
this(SomeStatic.logger);
};
    

MyClassToTest(Logger logger) {
this.logger = logger;
};
    

public void someOperation() {
logger.warn("warning message");
// ...
};
};


class MyClassToTestTest {
    

@Test
public warnCalled() {
Logger loggerMock = mock(Logger.class);
MyClassTest myClassToTest = new MyClassToTest(logger);
myClassToTest.someOperation();
verify(loggerMock).warn(anyString());
};
}

在我的案例中,我解决了如下的问题:

Logger root = (Logger) LoggerFactory.getLogger(CSVTasklet.class); //CSVTasklet is my target class
final Appender mockAppender = mock(Appender.class);
root.addAppender(mockAppender);


verify(mockAppender).doAppend(argThat((ArgumentMatcher) argument -> ((LoggingEvent) argument).getMessage().contains("No projects."))); // I checked "No projects." in the log

检查这个库https://github.com/Hakky54/log-captor

在maven文件中包含库的引用:

<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>logcaptor</artifactId>
<version>2.5.0</version>
<scope>test</scope>
</dependency>

在java代码测试方法中,你应该包括以下内容:

LogCaptor logCaptor = LogCaptor.forClass(MyClass.class);


// do the test logic....


assertThat(logCaptor.getLogs()).contains("Some log to assert");


通过添加Appender进行单元测试并不能真正测试Logger的配置。所以,我认为这是一个独特的情况下,单元测试没有带来那么多的价值,但集成测试带来了很多价值(特别是当您的日志记录具有某些审计目的时)

为了为它创建一个集成测试,让我们假设你正在运行一个简单的ConsoleAppender,并想测试它的输出。然后,你应该测试消息是如何从System.out写到它自己的ByteArrayOutputStream的。

从这个意义上说,我会做以下事情(我使用JUnit 5):

public class Slf4jAuditLoggerTest {


private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();


@BeforeEach
public void beforeEach() {
System.setOut(new PrintStream(outContent));
}

通过这种方式,你可以简单地测试它的输出:

    @Test
public void myTest() {
// Given...
// When...
// Then
assertTrue(outContent.toString().contains("[INFO] My formatted string from Logger"));
}

如果你这样做了,你将为你的项目带来更多的价值,而不需要使用内存中的实现,创建一个新的Appender,或者其他什么。

简单的方法

  @ExtendWith(OutputCaptureExtension.class)
class MyTestClass {
    

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