Ist es möglich, die Protokollierung (SLF4J + Logback) irgendwie abzufangen und InputStream
über einen JUnit-Testfall eine (oder etwas anderes, das lesbar ist) zu erhalten ...?
Ist es möglich, die Protokollierung (SLF4J + Logback) irgendwie abzufangen und InputStream
über einen JUnit-Testfall eine (oder etwas anderes, das lesbar ist) zu erhalten ...?
Antworten:
Sie können einen benutzerdefinierten Appender erstellen
public class TestAppender extends AppenderBase<LoggingEvent> {
static List<LoggingEvent> events = new ArrayList<>();
@Override
protected void append(LoggingEvent e) {
events.add(e);
}
}
und konfigurieren Sie logback-test.xml, um es zu verwenden. Jetzt können wir die Protokollierungsereignisse aus unserem Test überprüfen:
@Test
public void test() {
...
Assert.assertEquals(1, TestAppender.events.size());
...
}
HINWEIS: Verwenden ILoggingEvent
Sie diese Option, wenn Sie keine Ausgabe erhalten. Die Begründung finden Sie im Kommentarbereich.
events
nach jeder Testausführung löschen müssen.
sample0.xml
. Vergessen Sie nicht, den Appender auf Ihre Implementierung zu ändern
Die Slf4j-API bietet keine solche Möglichkeit, aber Logback bietet eine einfache Lösung.
Sie können ListAppender
Folgendes verwenden : einen Whitebox-Logback-Appender, in dem Protokolleinträge in ein public List
Feld eingefügt werden, mit dem wir unsere Aussagen treffen können.
Hier ist ein einfaches Beispiel.
Foo Klasse:
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-Klasse:
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
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());
}
}
Sie können Matcher / Assertion-Bibliotheken auch als AssertJ oder Hamcrest verwenden.
Mit AssertJ wäre es:
import org.assertj.core.api.Assertions;
Assertions.assertThat(listAppender.list)
.extracting(ILoggingEvent::getFormattedMessage, ILoggingEvent::getLevel)
.containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));
Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);
. Ich benutze LoggerFactory
von org.slf4j.LoggerFactory
und Logger
vonch.qos.logback.classic.Logger
SLF4J
diese Lösung verwenden, wird am Ende eine SLF4J: Class path contains multiple SLF4J bindings.
Warnung ausgegeben, da Sie sowohl SLF4J als auch logback.classic
Sie können slf4j-test von http://projects.lidalia.org.uk/slf4j-test/ verwenden . Es ersetzt die gesamte logback slf4j-Implementierung durch eine eigene slf4j-API-Implementierung für Tests und bietet eine API, die gegen Protokollierungsereignisse aktiviert werden kann.
Beispiel:
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<classpathDependencyExcludes>
<classpathDependencyExcludes>ch.qos.logback:logback-classic</classpathDependencyExcludes>
</classpathDependencyExcludes>
</configuration>
</plugin>
</plugins>
</build>
public class Slf4jUser {
private static final Logger logger = LoggerFactory.getLogger(Slf4jUser.class);
public void aMethodThatLogs() {
logger.info("Hello World!");
}
}
public class Slf4jUserTest {
Slf4jUser slf4jUser = new Slf4jUser();
TestLogger logger = TestLoggerFactory.getTestLogger(Slf4jUser.class);
@Test
public void aMethodThatLogsLogsAsExpected() {
slf4jUser.aMethodThatLogs();
assertThat(logger.getLoggingEvents(), is(asList(info("Hello World!"))));
}
@After
public void clearLoggers() {
TestLoggerFactory.clear();
}
}
slf4j-test
Paket von Lidalia finden Sie hier: github.com/jaegertracing/jaeger-client-java/pull/378/files
Eine einfache Lösung könnte darin bestehen, den Appender mit Mockito zu verspotten (zum Beispiel)
@Slf4j
class MyClass {
public void doSomething() {
log.info("I'm on it!");
}
}
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class MyClassTest {
@Mock private Appender<ILoggingEvent> mockAppender;
private MyClass sut = new MyClass();
@Before
public void setUp() {
Logger logger = (Logger) LoggerFactory.getLogger(MyClass.class.getName());
logger.addAppender(mockAppender);
}
@Test
public void shouldLogInCaseOfError() {
sut.doSomething();
verify(mockAppender).doAppend(ArgumentMatchers.argThat(argument -> {
assertThat(argument.getMessage(), containsString("I'm on it!"));
assertThat(argument.getLevel(), is(Level.INFO));
return true;
}));
}
}
HINWEIS: Ich verwende Assertion, anstatt zurückzukehren, false
da dadurch Code und (mögliche) Fehler leichter zu lesen sind, aber es funktioniert nicht, wenn Sie mehrere Überprüfungen haben. In diesem Fall müssen Sie zurückgeben und boolean
angeben, ob der Wert wie erwartet ist.
Obwohl das Erstellen eines benutzerdefinierten Logback-Appenders eine gute Lösung ist, ist dies nur der erste Schritt. Am Ende werden Sie slf4j-test entwickeln / neu erfinden. Wenn Sie noch ein bisschen weiter gehen: spf4j-slf4j-test oder andere Frameworks, die ich nicht verwende. Ich weiß es noch nicht.
Sie müssen sich eventuell Gedanken darüber machen, wie viele Ereignisse Sie im Speicher behalten, Unit-Tests nicht bestehen, wenn ein Fehler protokolliert (und nicht bestätigt) wird, Debug-Protokolle bei Testfehlern verfügbar machen usw.
Haftungsausschluss: Ich bin der Autor von spf4j-slf4j-test. Ich habe dieses Backend geschrieben, um spf4j besser testen zu können. Dies ist ein guter Ort, um Beispiele für die Verwendung von spf4j-slf4j-test zu finden. Einer der Hauptvorteile, die ich erzielt habe, war die Reduzierung meiner Build-Ausgabe (die bei Travis begrenzt ist), während ich immer noch alle Details habe, die ich brauche, wenn ein Fehler auftritt.
Ich würde eine einfache, wiederverwendbare Spionageimplementierung empfehlen, die als JUnit-Regel in einen Test aufgenommen werden kann:
public final class LogSpy extends ExternalResource {
private Logger logger;
private ListAppender<ILoggingEvent> appender;
@Override
protected void before() {
appender = new ListAppender<>();
logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); // cast from facade (SLF4J) to implementation class (logback)
logger.addAppender(appender);
appender.start();
}
@Override
protected void after() {
logger.detachAppender(appender);
}
public List<ILoggingEvent> getEvents() {
if (appender == null) {
throw new UnexpectedTestError("LogSpy needs to be annotated with @Rule");
}
return appender.list;
}
}
In Ihrem Test würden Sie den Spion folgendermaßen aktivieren:
@Rule
public LogSpy log = new LogSpy();
Rufen Sie log.getEvents()
(oder andere benutzerdefinierte Methoden) auf, um die protokollierten Ereignisse zu überprüfen.
import ch.qos.logback.classic.Logger;
statt import org.slf4j.LoggerFactory;
sonst addAppender()
nicht verfügbar sein. Ich habe eine Weile gebraucht, um das herauszufinden.
before()
und after()
nie erreicht, daher wird der Appender nie erstellt / angehängt und der UnexpectedTestError wird ausgelöst. Irgendwelche Ideen, was ich falsch mache? Muss die Regel in ein bestimmtes Paket eingefügt werden? Außerdem fügen Sie Ihrer Antwort bitte den Importabschnitt hinzu, da einige der Objekte / Schnittstellen mehrdeutige Namen haben.
Ich hatte Probleme beim Testen der Protokollzeile wie: LOGGER.error (Nachricht, Ausnahme) .
Die in http://projects.lidalia.org.uk/slf4j-test/ beschriebene Lösung versucht, die Ausnahme ebenfalls geltend zu machen, und es ist nicht einfach (und meiner Meinung nach wertlos), den Stacktrace neu zu erstellen.
Ich habe auf diese Weise beschlossen:
import org.junit.Test;
import org.slf4j.Logger;
import uk.org.lidalia.slf4jext.LoggerFactory;
import uk.org.lidalia.slf4jtest.TestLogger;
import uk.org.lidalia.slf4jtest.TestLoggerFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
import static uk.org.lidalia.slf4jext.Level.ERROR;
import static uk.org.lidalia.slf4jext.Level.INFO;
public class Slf4jLoggerTest {
private static final Logger LOGGER = LoggerFactory.getLogger(Slf4jLoggerTest.class);
private void methodUnderTestInSomeClassInProductionCode() {
LOGGER.info("info message");
LOGGER.error("error message");
LOGGER.error("error message with exception", new RuntimeException("this part is not tested"));
}
private static final TestLogger TEST_LOGGER = TestLoggerFactory.getTestLogger(Slf4jLoggerTest.class);
@Test
public void testForMethod() throws Exception {
// when
methodUnderTestInSomeClassInProductionCode();
// then
assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").contains(
tuple(INFO, "info message"),
tuple(ERROR, "error message"),
tuple(ERROR, "error message with exception")
);
}
}
Dies hat auch den Vorteil, nicht von der Hamcrest Matchers Library abhängig zu sein .
ILoggingEvent
stattdessen verwendenLoggingEvent
. Das hat bei mir funktioniert.