Wie kann ich die SLF4J-Protokollierung (mit Logback) über einen JUnit-Test abfangen?

75

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 ...?

Carlspring
quelle

Antworten:

38

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 ILoggingEventSie diese Option, wenn Sie keine Ausgabe erhalten. Die Begründung finden Sie im Kommentarbereich.

Evgeniy Dorofeev
quelle
15
Hinweis: Wenn Sie logback classic + slf4j verwenden, müssen Sie ILoggingEventstattdessen verwenden LoggingEvent. Das hat bei mir funktioniert.
Etech
6
@Evgeniy Dorofeev Könnten Sie bitte zeigen, wie logback-test.xml konfiguriert wird?
Hipokito
1
Ich gehe davon aus, dass Sie eventsnach jeder Testausführung löschen müssen.
Andrii Karaivanskyi
2
@hipokito Sie können das [hier] ( logback.qos.ch/manual/configuration.html ) in verwenden sample0.xml. Vergessen Sie nicht, den Appender auf Ihre Implementierung zu ändern
encoding_idiot
@EvgeniyDorofeev kannst du mir dabei helfen? stackoverflow.com/questions/48551083/…
Bhavya Arora
70

Die Slf4j-API bietet keine solche Möglichkeit, aber Logback bietet eine einfache Lösung.

Sie können ListAppenderFolgendes verwenden : einen Whitebox-Logback-Appender, in dem Protokolleinträge in ein public ListFeld 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));
davidxxx
quelle
2
Ich danke dir sehr! Genau das habe ich gesucht!
Oli
5
Ich bekomme ClassCastException für Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);. Ich benutze LoggerFactoryvon org.slf4j.LoggerFactoryund Loggervonch.qos.logback.classic.Logger
Hiren
@ Hiren Womit ist genau die Fehlermeldung verbunden?
Davidxxx
6
Es ist wichtig zu beachten, dass Sie anstelle von ILoggingEvent :: getMessage ILoggingEvent :: getFormattedMessage verwenden sollten, wenn Ihr Protokoll einen Parameterwert enthält. Andernfalls schlägt Ihre Zusicherung fehl, da der Wert fehlt.
Robert Mason
4
Wenn Sie SLF4Jdiese Lösung verwenden, wird am Ende eine SLF4J: Class path contains multiple SLF4J bindings.Warnung ausgegeben, da Sie sowohl SLF4J als auch logback.classic
Ghilteras
16

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();
    }
}
Oleg Majewski
quelle
Danke für diese alternative Antwort! Es sieht sehr nützlich aus und ich werde diesen Ansatz höchstwahrscheinlich auch in Zukunft ausprobieren! Leider habe ich die andere Antwort, die ebenfalls richtig ist, bereits akzeptiert.
Carlspring
Ein vollständiges Beispiel mit dem slf4j-testPaket von Lidalia finden Sie hier: github.com/jaegertracing/jaeger-client-java/pull/378/files
Debosmit Ray
1
Diese Lösung funktioniert einwandfrei, wenn Sie Spring nicht verwenden. Wenn Sie Spring verwenden, wird eine Ausnahme für nicht gefundene Klassen (JoranConfigurator) ausgelöst.
Jesus H
7

Eine einfache Lösung könnte darin bestehen, den Appender mit Mockito zu verspotten (zum Beispiel)

MyClass.java

@Slf4j
class MyClass {
    public void doSomething() {
        log.info("I'm on it!");
    }
}

MyClassTest.java

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, falseda 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 booleanangeben, ob der Wert wie erwartet ist.

Snovelli
quelle
funktioniert das, wenn ich die lombok.extern.slf4j-Antennen wie Slf4j verwende? Wie verspottet oder spioniert man den Logger aus, wenn er nicht einmal ein Objekt in meinen Klassen ist? dh log.error wird nur verwendet, indem die Anmerkung Slf4j für meine Klasse
bereitgestellt wird
@ennth Es sollte funktionieren, weil Sie den Mock mit der statischen Methode LoggerFactory.getLogger (). AddAppender (mockAppender) injizieren. Das funktioniert genauso, wenn Sie den Logger mit Lombok
snovelli erstellen.
2
Dasselbe Problem nicht funktionieren. Was sind die 'Importe' für die Klassen Logger und LoggerFactory? Warum werden die statischen Importe aufgelistet und die anderen nicht?
Dirk Schumacher
5

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.

user2179737
quelle
4

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.

oberlies
quelle
2
Damit dies funktioniert, müssen Sie 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.
Urs Beeli
Funktioniert bei mir nicht Es sieht so aus, als ob die Regel nicht korrekt angewendet wird: Beim Debuggen habe ich gefunden 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.
Philzen
2

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 .

daemon_nio
quelle