How can I configure slf4j
to redirect all logged information to a Java string?
This is sometimes useful in unit tests, e.g. to test no war开发者_如何学运维nings are printed when loading a servlet, or to make sure a forbidden SQL table is never used.
A bit late, but still...
As logging configurations should be easy to replace when unit testing, you could just configure to log over stdout and then capture that prior to executing the logging subject. Then set the logger to be silent for all but the subject under test.
@Test
public void test()
{
String log = captureStdOut(() -> {
// ... invoke method that shouldn't log
});
assertThat(log, is(emptyString()));
}
public static String captureStdOut(Runnable r)
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream out = System.out;
try {
System.setOut(new PrintStream(baos, true, StandardCharsets.UTF_8.name()));
r.run();
return new String(baos.toByteArray(), StandardCharsets.UTF_8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("End of the world, Java doesn't recognise UTF-8");
} finally {
System.setOut(out);
}
}
And if using slf4j over log4j in tests, a simple log4j.properties:
log4j.rootLogger=OFF, out
log4j.category.com.acme.YourServlet=INFO, out
log4j.appender.out=org.apache.log4j.ConsoleAppender
log4j.appender.out.layout=org.apache.log4j.PatternLayout
log4j.appender.out.layout.ConversionPattern=%-5p %c{1}:%L - %m%n
Or if you loath configuration as an external dependencies in unit tests, then programmatically configure log4j:
//...
static final String CONSOLE_APPENDER_NAME = "console.appender";
private String pattern = "%d [%p|%c|%C{1}] %m%n";
private Level threshold = Level.ALL;
private Level defaultLevel = Level.OFF;
//...
public void configure()
{
configureRootLogger();
configureConsoleAppender();
configureCustomLevels();
}
private void configureConsoleAppender()
{
ConsoleAppender console = new ConsoleAppender();
console.setName(CONSOLE_APPENDER_NAME);
console.setLayout(new PatternLayout(pattern));
console.setThreshold(threshold);
console.activateOptions();
Logger.getRootLogger().addAppender(console);
}
private void configureRootLogger()
{
Logger.getRootLogger().getLoggerRepository().resetConfiguration();
Logger.getRootLogger().setLevel(defaultLevel);
}
As I see it you have two options.
First you could implement a custom Appender (depending on which slf4j implementation you're using) which simply appends each logged statement to a StringBuffer. In this case you probably have to hold a static reference to your StringBuffer so your test classes can access it.
Second you could write your own implementation of ILoggerFactory and Logger. Again your Logger would just append all the messages to internal StringBuffers, although in this case you'd probably have multiple buffers, one for each log level. If you did it this way you'd have an easy way of retrieving the Logger instances since you'd own the factory that was distributing them.
Shouldn't make sense to redirect all the logs to watch to a separate log file? That way you have the control you want (you can delete the log file before running the test and checking if the file has been create at any moment) without losing the benefits of logging (redirecting your output to a String can cause memory leaks and is less performant)
This is a simple way to log to the console:
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.BasicConfigurator;
import ch.qos.logback.classic.LoggerContext;
private void LogToConsole() {
BasicConfigurator bc = new BasicConfigurator();
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
lc.reset();
bc.configure(lc);
}
Not quite exactly what you're doing, but I've written a LogInterceptingTestHarness
which enables assertion of specific log statements. You could similarly use it (or something like it) to assert nothing has been logged at a certain level.
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.List;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.junit.After;
import org.junit.Before;
import org.mockito.ArgumentCaptor;
import lombok.Getter;
/**
* Use this class to intercept logs for the purposes of unit testing log output.
* <p>
* On {@link Before} of the unit test, call {@link #initHarness(Class, Level)} or {@link #initHarness(Class, Level, String)} to get a new harness and hold onto reference to it in a class-level
* variable of your unit test
* <p>
* On {@link After} of the unit test, you MUST call {@link #teardown()} in order to remove the mocked {@link #appender}
*
* @author jeff.nelson
*
*/
@Getter
public class LogInterceptingTestHarness {
private final Appender appender;
private final ArgumentCaptor<LogEvent> logEventCaptor;
private final Logger itsLogger;
private LogInterceptingTestHarness(Class<?> classInterceptLogsFor, Level logLevel, String appenderName) {
logEventCaptor = ArgumentCaptor.forClass(LogEvent.class);
appender = mock(Appender.class);
doReturn("testAppender").when(appender).getName();
doReturn(true).when(appender).isStarted();
itsLogger = (Logger) LogManager.getLogger(classInterceptLogsFor);
itsLogger.addAppender(appender);
itsLogger.setLevel(logLevel);
}
public void teardown() {
itsLogger.removeAppender(appender);
}
public List<LogEvent> verifyNumLogEvents(int numEvents) {
verify(appender, times(numEvents)).append(logEventCaptor.capture());
return logEventCaptor.getAllValues();
}
public LogEvent verifyOneLogEvent() {
return verifyNumLogEvents(1).get(0);
}
public void assertLoggedMessage(String message) {
assertLogMessage(message, logEventCaptor.getValue());
}
public void assertLoggedMessage(String message, int messageIndex) {
assertLogMessage(message, logEventCaptor.getAllValues().get(messageIndex));
}
public static void assertLogMessage(String message, LogEvent event) {
assertEquals(message, event.getMessage().getFormattedMessage());
}
public static LogInterceptingTestHarness initHarness(Class<?> classInterceptLogsFor, Level logLevel) {
return initHarness(classInterceptLogsFor, logLevel, "testAppender");
}
public static LogInterceptingTestHarness initHarness(Class<?> classInterceptLogsFor, Level logLevel, String appenderName) {
return new LogInterceptingTestHarness(classInterceptLogsFor, logLevel, appenderName);
}
}
精彩评论