我正在使用 slf4j,我想对我的代码进行单元测试,以确保在某些条件下生成警告/错误日志消息。我宁愿这些是严格的单元测试,所以我宁愿不必从文件中提取日志配置来测试是否生成了日志消息。我使用的模拟框架是 Mockito。
13 回答
为了在不依赖特定实现(例如 log4j)的情况下测试 slf4j,您可以提供自己的 slf4j 日志实现,如本 SLF4J FAQ中所述。您的实现可以记录已记录的消息,然后由您的单元测试询问以进行验证。
slf4j -test包正是这样做的。它是一个内存中的 slf4j 日志实现,提供了检索日志消息的方法。
创建测试规则:
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.stream.Collectors;
public class LoggerRule implements TestRule {
private final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
private final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
setup();
base.evaluate();
teardown();
}
};
}
private void setup() {
logger.addAppender(listAppender);
listAppender.start();
}
private void teardown() {
listAppender.stop();
listAppender.list.clear();
logger.detachAppender(listAppender);
}
public List<String> getMessages() {
return listAppender.list.stream().map(e -> e.getMessage()).collect(Collectors.toList());
}
public List<String> getFormattedMessages() {
return listAppender.list.stream().map(e -> e.getFormattedMessage()).collect(Collectors.toList());
}
}
然后使用它:
@Rule
public final LoggerRule loggerRule = new LoggerRule();
@Test
public void yourTest() {
// ...
assertThat(loggerRule.getFormattedMessages().size()).isEqualTo(2);
}
----- 2021 年 10 月扩展的 JUnit 5 -----
日志捕获:
public class LogCapture {
private ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
LogCapture() {
}
public String getFirstFormattedMessage() {
return getFormattedMessageAt(0);
}
public String getLastFormattedMessage() {
return getFormattedMessageAt(listAppender.list.size() - 1);
}
public String getFormattedMessageAt(int index) {
return getLoggingEventAt(index).getFormattedMessage();
}
public LoggingEvent getLoggingEvent() {
return getLoggingEventAt(0);
}
public LoggingEvent getLoggingEventAt(int index) {
return (LoggingEvent) listAppender.list.get(index);
}
public List<LoggingEvent> getLoggingEvents() {
return listAppender.list.stream().map(e -> (LoggingEvent) e).collect(Collectors.toList());
}
public void setLogFilter(Level logLevel) {
listAppender.clearAllFilters();
listAppender.addFilter(buildLevelFilter(logLevel));
}
public void clear() {
listAppender.list.clear();
}
void start() {
setLogFilter(Level.INFO);
listAppender.start();
}
void stop() {
if (listAppender == null) {
return;
}
listAppender.stop();
listAppender.list.clear();
listAppender = null;
}
ListAppender<ILoggingEvent> getListAppender() {
return listAppender;
}
private Filter<ILoggingEvent> buildLevelFilter(Level logLevel) {
LevelFilter levelFilter = new LevelFilter();
levelFilter.setLevel(logLevel);
levelFilter.setOnMismatch(FilterReply.DENY);
levelFilter.start();
return levelFilter;
}
}
日志捕获扩展:
public class LogCaptureExtension implements ParameterResolver, AfterTestExecutionCallback {
private Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
private LogCapture logCapture;
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == LogCapture.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
logCapture = new LogCapture();
setup();
return logCapture;
}
@Override
public void afterTestExecution(ExtensionContext context) {
teardown();
}
private void setup() {
logger.addAppender(logCapture.getListAppender());
logCapture.start();
}
private void teardown() {
if (logCapture == null || logger == null) {
return;
}
logger.detachAndStopAllAppenders();
logCapture.stop();
}
}
然后使用它:
@ExtendWith(LogCaptureExtension.class)
public class SomeTest {
@Test
public void sometest(LogCapture logCapture) {
// do test here
assertThat(logCapture.getLoggingEvents()).isEmpty();
}
// ...
}
我认为您可以使用自定义附加程序解决您的问题。创建一个实现 , 的测试附加程序,并在执行测试用例时org.apache.log4j.Appender
将附加程序设置在并加载它。log4j.properties
如果您从中调用测试工具,appender
则可以检查记录的消息
在具有并发测试执行的环境中运行良好的 SLF4J 更好的测试实现是https://github.com/portingle/slf4jtesting
当涉及到并发测试执行时,我已经加入了一些关于 slf4j 日志测试和现有测试方法的局限性的讨论。
我决定把我的话写成代码,结果就是 git repo。
对于 JUnit 5,在创建测试规则中创建一个实现上述andrew-feng提供的解决方案的扩展:
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.stream.Collectors;
public class LoggerExtension implements BeforeEachCallback, AfterEachCallback {
private final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
private final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
@Override
public void afterEach(ExtensionContext extensionContext) throws Exception {
listAppender.stop();
listAppender.list.clear();
logger.detachAppender(listAppender);
}
@Override
public void beforeEach(ExtensionContext extensionContext) throws Exception {
logger.addAppender(listAppender);
listAppender.start();
}
public List<String> getMessages() {
return listAppender.list.stream().map(e -> e.getMessage()).collect(Collectors.toList());
}
public List<String> getFormattedMessages() {
return listAppender.list.stream().map(e -> e.getFormattedMessage()).collect(Collectors.toList());
}
}
然后使用它:
@RegisterExtension
public LoggerExtension loggerExtension = new LoggerExtension();
@Test
public void yourTest() {
// ...
assertThat(loggerExtension.getFormattedMessages().size()).isEqualTo(2);
}
您可以将需要测试的重要日志调用放在他们自己的方法中,而不是模拟SLF4J ,您可以更轻松地模拟这些调用。
如果你真的想模拟 SLF4J,我敢打赌你可以为它创建自己的提供程序,这将允许你从 SLF4J 端提供一个模拟记录器,而不是在你的服务对象中注入一个模拟记录器。
使用 slf4j-test 可以消除上面讨论的许多变通方法
pom.xml
<dependency>
<groupId>uk.org.lidalia</groupId>
<artifactId>slf4j-test</artifactId>
<version>1.2.0</version>
</dependency>
示例类
@Slf4j
public class SampleClass {
public void logDetails(){
log.info("Logging");
}
}
测试类
import org.junit.Test;
import uk.org.lidalia.slf4jtest.TestLogger;
import uk.org.lidalia.slf4jtest.TestLoggerFactory;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static uk.org.lidalia.slf4jtest.LoggingEvent.info;
public class SampleClassTest {
TestLogger logger = TestLoggerFactory.getTestLogger(SampleClass.class);
@Test
public void testLogging(){
SampleClass sampleClass = new SampleClass();
//Invoke slf4j logger
sampleClass.logDetails();
assertThat(logger.getLoggingEvents(), is(asList(info("Logging"))));
}
}
与@Zsolt 类似,您可以模拟 log4jAppender
并将其设置在 上Logger
,然后验证对Appender.doAppend()
. 这使您无需修改真实代码即可进行测试。
您可以尝试另一个库来支持轻松模拟 slf4j 记录器 - slf4j-mock,您的代码看起来如下:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.Logger;
@RunWith(MockitoJUnitRunner.class)
public class JUnit4ExampleTest {
private static final String INFO_TEST_MESSAGE = "info log test message from JUnit4";
@Mock
Logger logger;
@InjectMocks
Example sut;
@Test
public void logInfoShouldBeLogged() {
// when
sut.methodWithLogInfo(INFO_TEST_MESSAGE);
// then
Mockito.verify(logger).info(INFO_TEST_MESSAGE);
Mockito.verifyNoMoreInteractions(logger);
}
}
如您所见,在测试代码中不需要任何特殊步骤。您只需在项目中添加对库的依赖项。
更多示例和说明:
我有一个新答案,我将在这篇文章的顶部发布(我的“旧”答案仍然在这篇文章的底部)(在撰写本文时,我的“旧”答案是“0”,所以没有害处,不犯规!)
较新的答案:
这是 Gradle 包:
testImplementation 'com.portingle:slf4jtesting:1.1.3'
马文链接:
https://mvnrepository.com/artifact/com.portingle/slf4jtesting
德语代码:
(下面的导入和私有方法将进入 MyTestClass(.java))
import static org.junit.Assert.assertNotNull;
import slf4jtest.LogLevel;
import slf4jtest.Settings;
import slf4jtest.TestLogger;
import slf4jtest.TestLoggerFactory;
@Test
public void myFirstTest() {
org.slf4j.Logger unitTestLogger = this.getUnitTestLogger();
ISomethingToTestObject testItem = new SomethingToTestObject (unitTestLogger);
SomeReturnObject obj = testItem.myMethod("arg1");
assertNotNull(wrapper);
/* now here you would find items in the unitTestLogger */
assertContains(unitTestLogger, LogLevel.DebugLevel, "myMethod was started");
}
// render nicer errors
private void assertContains(TestLogger unitTestLogger, LogLevel logLev, String expected) throws Error {
if (!unitTestLogger.contains(logLev, expected)) {
throw new AssertionError("expected '" + expected + "' but got '" + unitTestLogger.lines() + "'");
}
}
// render nicer errors
private void assertNotContains(TestLogger unitTestLogger, LogLevel logLev, String expected) throws Error {
if (unitTestLogger.contains(logLev, expected)) {
throw new AssertionError("expected absence of '" + expected + "' but got '" + unitTestLogger.lines() + "'");
}
}
private TestLogger getUnitTestLogger() {
TestLoggerFactory loggerFactory = Settings.instance()
.enableAll() // necessary as by default only ErrorLevel is enabled
.buildLogging();
TestLogger returnItem = loggerFactory.getLogger(MyTestClasss.class.getName());
assertNotNull(returnItem);
return returnItem;
}
==============================下面的旧答案..不要使用============= ===
下面是我之前的回答。在我发现它(上面的包)之后,我改变了我的下面的代码......以使用上面的包。
So here is my method.
First, I allow the logger to be injected. But I provide a default as well:
```java
package com.mycompany.myproject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyCoolClass { //implements IMyCoolClass {
private static final String PROCESS_STARTED = "Process started. (key='%1$s')";
private final Logger logger;
public MyCoolClass() {
this(LoggerFactory.getLogger(MyCoolClass.class));
}
public MyCoolClass(Logger lgr) {
this.logger = lgr;
}
public doSomething(int key)
{
logger.info(String.format(PROCESS_STARTED, key));
/*now go do something */
}
}
Then I wrote a very basic in memory logger
```java
import org.slf4j.Marker;
import java.util.ArrayList;
import java.util.Collection;
public class InMemoryUnitTestLogger implements org.slf4j.Logger {
public Collection<String> informations = new ArrayList<String>();
public Collection<String> errors = new ArrayList<String>();
public Collection<String> traces = new ArrayList<String>();
public Collection<String> debugs = new ArrayList<>();
public Collection<String> warns = new ArrayList<>();
public Collection<String> getInformations() {
return informations;
}
public Collection<String> getErrors() {
return errors;
}
public Collection<String> getTraces() {
return traces;
}
public Collection<String> getDebugs() {
return debugs;
}
public Collection<String> getWarns() {
return warns;
}
@Override
public String getName() {
return "FakeLoggerName";
}
@Override
public boolean isTraceEnabled() {
return false;
}
@Override
public boolean isTraceEnabled(Marker marker) {
return false;
}
@Override
public boolean isDebugEnabled() {
return false;
}
@Override
public boolean isDebugEnabled(Marker marker) {
return false;
}
@Override
public boolean isWarnEnabled(Marker marker) {
return false;
}
@Override
public boolean isInfoEnabled(Marker marker) {
return false;
}
@Override
public boolean isWarnEnabled() {
return false;
}
@Override
public boolean isErrorEnabled(Marker marker) {
return false;
}
@Override
public boolean isInfoEnabled() {
return false;
}
@Override
public boolean isErrorEnabled() {
return false;
}
@Override
public void trace(String s) {
this.internalTrace(s);
}
@Override
public void trace(String s, Object o) {
this.internalTrace(s);
}
@Override
public void trace(String s, Object o, Object o1) {
this.internalTrace(s);
}
@Override
public void trace(String s, Object... objects) {
this.internalTrace(s);
}
@Override
public void trace(String s, Throwable throwable) {
this.internalTrace(s);
}
@Override
public void trace(Marker marker, String s) {
this.internalTrace(s);
}
@Override
public void trace(Marker marker, String s, Object o) {
this.internalTrace(s);
}
@Override
public void trace(Marker marker, String s, Object o, Object o1) {
this.internalTrace(s);
}
@Override
public void trace(Marker marker, String s, Object... objects) {
this.internalTrace(s);
}
@Override
public void trace(Marker marker, String s, Throwable throwable) {
this.internalTrace(s);
}
@Override
public void debug(String s) {
this.internalDebug(s);
}
@Override
public void debug(String s, Object o) {
this.internalDebug(s);
}
@Override
public void debug(String s, Object o, Object o1) {
this.internalDebug(s);
}
@Override
public void debug(String s, Object... objects) {
this.internalDebug(s);
}
@Override
public void debug(String s, Throwable throwable) {
this.internalDebug(s);
}
@Override
public void debug(Marker marker, String s) {
this.internalDebug(s);
}
@Override
public void debug(Marker marker, String s, Object o) {
this.internalDebug(s);
}
@Override
public void debug(Marker marker, String s, Object o, Object o1) {
this.internalDebug(s);
}
@Override
public void debug(Marker marker, String s, Object... objects) {
this.internalDebug(s);
}
@Override
public void debug(Marker marker, String s, Throwable throwable) {
this.internalDebug(s);
}
public void info(String s) {
this.internalInfo(s);
}
@Override
public void info(String s, Object o) {
this.internalInfo(s);
}
@Override
public void info(String s, Object o, Object o1) {
this.internalInfo(s);
}
@Override
public void info(String s, Object... objects) {
this.internalInfo(s);
}
@Override
public void info(String s, Throwable throwable) {
this.internalInfo(s);
}
@Override
public void info(Marker marker, String s) {
this.internalInfo(s);
}
@Override
public void info(Marker marker, String s, Object o) {
this.internalInfo(s);
}
@Override
public void info(Marker marker, String s, Object o, Object o1) {
this.internalInfo(s);
}
@Override
public void info(Marker marker, String s, Object... objects) {
this.internalInfo(s);
}
@Override
public void info(Marker marker, String s, Throwable throwable) {
this.internalInfo(s);
}
public void error(String s) {
this.internalError(s);
}
@Override
public void error(String s, Object o) {
this.internalError(s);
}
@Override
public void error(String s, Object o, Object o1) {
this.internalError(s);
}
@Override
public void error(String s, Object... objects) {
this.internalError(s);
}
@Override
public void error(String s, Throwable throwable) {
this.internalError(s);
}
@Override
public void error(Marker marker, String s) {
this.internalError(s);
}
@Override
public void error(Marker marker, String s, Object o) {
this.internalError(s);
}
@Override
public void error(Marker marker, String s, Object o, Object o1) {
this.internalError(s);
}
@Override
public void error(Marker marker, String s, Object... objects) {
this.internalError(s);
}
@Override
public void error(Marker marker, String s, Throwable throwable) {
this.internalError(s);
}
public void warn(String s) {
this.internalWarn(s);
}
@Override
public void warn(String s, Object o) {
this.internalWarn(s);
}
@Override
public void warn(String s, Object... objects) {
this.internalWarn(s);
}
@Override
public void warn(String s, Object o, Object o1) {
this.internalWarn(s);
}
@Override
public void warn(String s, Throwable throwable) {
this.internalWarn(s);
}
@Override
public void warn(Marker marker, String s) {
this.internalWarn(s);
}
@Override
public void warn(Marker marker, String s, Object o) {
this.internalWarn(s);
}
@Override
public void warn(Marker marker, String s, Object o, Object o1) {
this.internalWarn(s);
}
@Override
public void warn(Marker marker, String s, Object... objects) {
this.internalWarn(s);
}
@Override
public void warn(Marker marker, String s, Throwable throwable) {
this.internalWarn(s);
}
private void internalDebug(String s) {
System.out.println(s);
this.debugs.add(s);
}
private void internalInfo(String msg) {
System.out.println(msg);
this.informations.add(msg);
}
private void internalTrace(String msg) {
//??System.out.println(msg);
this.traces.add(msg);
}
private void internalWarn(String msg) {
System.err.println(msg);
this.warns.add(msg);
}
private void internalError(String msg) {
System.err.println(msg);
this.errors.add(msg);
}
然后在我的单元测试中,我可以做以下两件事之一:
private ByteArrayOutputStream setupSimpleLog(Logger lgr) {
ByteArrayOutputStream pipeOut = new ByteArrayOutputStream();
PrintStream pipeIn = new PrintStream(pipeOut);
System.setErr(pipeIn);
return pipeOut;
}
private Logger getSimpleLog() {
Logger lgr = new InMemoryUnitTestLogger();
return lgr;
}
private void myTest() {
Logger lgr = getSimpleLog();
ByteArrayOutputStream pipeOut = this.setupSimpleLog(lgr);
MyCoolClass testClass = new MyCoolClass(lgr);
int myValue = 333;
testClass.doSomething(myValue);
String findMessage = String.format(MyCoolClass.PROCESS_STARTED, myValue);
String output = new String(pipeOut.toByteArray());
assertTrue(output.contains(findMessage));
}
或与上述类似,但对自定义 Logger 进行强制转换
private void myTest() {
Logger lgr = getSimpleLog();
MyCoolClass testClass = new MyCoolClass(lgr);
int myValue = 333;
testClass.doSomething(myValue);
String findMessage = String.format(MyCoolClass.PROCESS_STARTED, myValue);
InMemoryUnitTestLogger castLogger = (InMemoryUnitTestLogger)lgr;
/* now check the exact subcollection for the message) */
assertTrue(castLogger.getInfos().contains(findMessage));
}
对代码持保留态度,想法就在那里。我没有编译代码。
我知道这个问题发布已经有一段时间了,但我刚刚遇到了一个类似的问题,我的解决方案可能会有所帮助。按照@Zsolt 提出的解决方案,我们使用了一个appender,更具体地说是Logback 的ListAppender
。在此处显示代码和配置(Groovy 代码,但可以轻松移植到 Java):
用于日志访问的 Groovy 类:
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.spi.LoggingEvent
import ch.qos.logback.core.read.ListAppender
import org.slf4j.LoggerFactory
class LogAccess {
final static String DEFAULT_PACKAGE_DOMAIN = Logger.ROOT_LOGGER_NAME
final static String DEFAULT_APPENDER_NAME = 'LIST'
final List<LoggingEvent> list
LogAccess(String packageDomain = DEFAULT_PACKAGE_DOMAIN, String appenderName = DEFAULT_APPENDER_NAME) {
Logger logger = (Logger) LoggerFactory.getLogger(packageDomain)
ListAppender<LoggingEvent> appender = logger.getAppender(appenderName) as ListAppender<LoggingEvent>
if (appender == null) {
throw new IllegalStateException("'$DEFAULT_APPENDER_NAME' appender not found. Did you forget to add 'logback.xml' to the resources folder?")
}
this.list = appender.list
this.clear()
}
void clear() {
list.clear()
}
boolean contains(String logMessage) {
return list.reverse().any { it.getFormattedMessage() == logMessage }
}
@Override
String toString() {
list.collect { it. getFormattedMessage() }
}
}
示例 logback.xml 配置:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- These 2 'includes' tags ensure regular springboot console logging works as usual -->
<!-- See https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-configure-logback-for-logging -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<appender name="LIST" class="ch.qos.logback.core.read.ListAppender"/>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="LIST" />
</root>
</configuration>
测试:
LogAccess log = new LogAccess()
def expectedLogEntry = 'Expected Log Entry'
assert !log.contains(expectedLogEntry)
methodUnderTest()
assert log.contains(expectedLogEntry)
我在带有 Groovy+Spock 的 SpringBoot 项目中使用它,但我不明白为什么这在任何带有 Logback 的 Java 项目中都不起作用。
只需使用普通的 Mockito 和一些反射逻辑来模拟它:
// Mock the Logger
Logger mock = Mockito.mock(Logger.class);
// Set the Logger to the class you want to test.
// Since this is often a private static field you have to
// hack a little bit: (Solution taken from https://stackoverflow.com/a/3301720/812093)
setFinalStatic(ClassBeeingTested.class.getDeclaredField("log"), mock);
使用 setFinalStatic 方法蜂鸣
public static void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}
然后只需执行要测试的代码并验证 - 例如以下验证 Logger.warn 方法被调用了两次:
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
Mockito.verify(mock,Mockito.atLeastOnce()).warn(argumentCaptor.capture());
List<String> allValues = argumentCaptor.getAllValues();
assertEquals(2, allValues.size());
assertEquals("myFirstExpectedMessage", allValues.get(0));
assertEquals("mySecondExpectedMessage", allValues.get(1));
请注意,通过反射设置最终字段并非在所有情况下都有效。例如,如果多个测试用例试图修改它,我就无法让它工作。
这个解决方案之前已经在这个 groovy 答案和这个评论中提到过,但是因为我不认为它本身就是一个答案,所以在这里添加它作为一个社区 wiki 答案。
所以使用 logback listappender 的 JUnit5 解决方案:
import static org.assertj.core.api.Assertions.assertThat;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
public class LoggingTest {
private final ClassToTest sut = new ClassToTest();
private ListAppender<ILoggingEvent> listAppender;
@BeforeEach
void init() {
final var log = (Logger) LoggerFactory.getLogger(ClassToTest.class);
listAppender = new ListAppender<>();
listAppender.start();
log.addAppender(listAppender);
}
@Test
public void testLogging() {
sut.doSomethingThatLogs()
String message = listAppender.list.get(0).getFormattedMessage();
assertThat(message).contains("this message should be logged");
}
}