我正在使用slf4j,我想对我的代码进行单元测试,以确保在某些条件下生成警告/错误日志消息。我宁愿这些是严格的单元测试,所以我不想从文件中提取日志配置以测试日志消息的生成。我正在使用的模拟框架是Mockito。
答案 0 :(得分:17)
为了测试slf4j而不依赖于特定的实现(例如log4j),您可以按照this SLF4J FAQ中的描述提供自己的slf4j日志记录实现。您的实现可以记录已记录的消息,然后由单元测试进行查询以进行验证。
slf4j-test包正是这样做的。它是一个内存中的slf4j日志记录实现,它提供了检索已记录消息的方法。
答案 1 :(得分:10)
我认为您可以使用自定义appender解决您的问题。创建一个实现org.apache.log4j.Appender
的测试appender,并在log4j.properties
中设置你的appender并在执行测试用例时加载它。
如果您从appender
回拨测试工具,则可以查看已记录的消息
答案 2 :(得分:8)
SLF4J的更好的测试实现在并发测试执行的环境中非常有效https://github.com/portingle/slf4jtesting
关于slf4j日志测试以及现有测试方法在并发测试执行方面的局限性,我已经进行了一些讨论。
我决定将我的话放入代码中,结果就是git repo。
答案 3 :(得分:5)
对于 JUnit 5,创建一个扩展来实现上面 andrew-feng 中的 Create a test rule 提供的解决方案:
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);
}
答案 4 :(得分:2)
您可以将自己需要的重要日志记录调用放在他们自己的方法中,而不是模拟SLF4J,您可以更轻松地进行模拟。
如果你真的想模仿SLF4J,我敢打赌你可以为它创建一个自己的提供者,它允许你从SLF4J端提供一个模拟记录器,而不是在你的服务对象中注入一个。
答案 5 :(得分:1)
创建测试规则:
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);
}
答案 6 :(得分:1)
使用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");
}
}
TestClass
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"))));
}
}
请参阅http://projects.lidalia.org.uk/slf4j-test/以获取更多详细信息
答案 7 :(得分:0)
与@Zsolt类似,您可以模拟log4j Appender
并将其设置在Logger
上,然后验证对Appender.doAppend()
的调用。这使您无需修改实际代码即可进行测试。
答案 8 :(得分:0)
这是我的方法。
首先,我允许注入记录器。但是我也提供了默认值:
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 */
}
}
然后我在内存记录器中写了一个非常基本的内容
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));
}
带着一粒盐拿代码,想法就在那里。我没有编译代码。
答案 9 :(得分:0)
我知道这个问题发布已经有一段时间了,但是我遇到了类似的问题,我的解决方案可能会有所帮助。按照@Zsolt提出的解决方案,我们使用附加器,更具体地说是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项目中都行不通。
答案 10 :(得分:0)
只需使用普通的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方法beeing
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));
请注意,并非在所有情况下都无法通过反射设置最终字段。例如,如果多个测试用例试图对其进行修改,我将无法使其正常运行。
答案 11 :(得分:0)
您可以尝试使用另一个库来支持简单的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);
}
}
如您所见,测试代码中不需要任何特殊步骤。您只需在项目中为库添加依赖项。
更多示例和说明,位于: