问题
I'm using slf4j and I want to unit test my code to make sure that warn/error log messages are generated under certain conditions. I'd rather these be strict unit tests, so I'd prefer not to have to pull up logging configuration from a file in order to test that the log messages are generated. The mocking framework I'm using is Mockito.
回答1:
I think you could solve your problem with a custom appender. Create a test appender which implements the org.apache.log4j.Appender
, and set your appender in the log4j.properties
and load it when you execute test cases.
If you call back to the test harness from that appender
you can check the logged messages
回答2:
For testing slf4j without relying on a specific implementation (such as log4j), you can provide your own slf4j logging implementation as described in this SLF4J FAQ. Your implementation can record the messages that were logged and then be interrogated by your unit tests for validation.
The slf4j-test package does exactly this. It's an in-memory slf4j logging implementation that provides methods for retrieving logged messages.
回答3:
A better test implementation of SLF4J that works really well in an environment with concurrent test execution is https://github.com/portingle/slf4jtesting
I've chimed in on a few discussion on slf4j log testing and the limitations of existing test approaches when it comes to concurrent test execution.
I decided to put my words into code and that git repo is the result.
回答4:
Instead of mocking SLF4J you could place the important logging calls you need to test inside their own methods which you can mock more easily.
If you really want to mock SLF4J, I would bet you could create your own provider for it that would allow you to supply a mock logger from the SLF4J side instead of injecting one in your service objects.
回答5:
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.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());
}
}
Then use it:
@Rule
public final LoggerRule loggerRule = new LoggerRule();
@Test
public void yourTest() {
// ...
assertThat(loggerRule.getFormattedMessages().size()).isEqualTo(2);
}
回答6:
So here is my method.
First, I allow the logger to be injected. But I provide a default as well:
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
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);
}
Then in my unit tests, I can do one of two things:
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));
}
or similar to the above, but do a cast on the custom 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));
}
Take the code with a grain of salt, the ideas are there. I didn't compile the code.
回答7:
Similar to @Zsolt, you can mock log4j Appender
and set it on the Logger
, then verify calls to Appender.doAppend()
. This allows you to test without having to modify the real code.
来源:https://stackoverflow.com/questions/4650222/what-is-the-best-way-to-unit-test-slf4j-log-messages