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

Lesezeit: 9 Minuten

Benutzer-Avatar
Carlspring

Ist es möglich das Logging (SLF4J + Logback) irgendwie abzufangen und an eine InputStream (oder etwas anderes, das lesbar ist) über einen JUnit-Testfall …?

Benutzer-Avatar
davidxxx

Die Slf4j-API bietet einen solchen Weg nicht, aber Logback bietet eine einfache Lösung.

Sie können verwenden ListAppender : ein Whitebox-Logback-Appender, in dem Protokolleinträge in a hinzugefügt werden public List Feld, das wir verwenden könnten, um unsere Behauptungen aufzustellen.

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");
    }
}

FootTest-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 auch Matcher-/Assertion-Bibliotheken wie 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));

  • Ich danke dir sehr! Das ist genau das, wonach ich gesucht habe!

    – Ol

    16. November 2018 um 10:12 Uhr

  • Ich erhalte ClassCastException für Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);. ich benutze LoggerFactory von org.slf4j.LoggerFactory und Logger von ch.qos.logback.classic.Logger

    – anstellen

    27. Mai 2019 um 9:48 Uhr


  • @Hiren Wenn Sie JUnit 5 verwenden, stellen Sie sicher, dass Sie nicht versehentlich importieren org.junit.platform.commons.logging.LoggerFactorywas mir passiert ist.

    – Denis Washington

    5. Juli 2019 um 8:33 Uhr

  • Beachten Sie, dass Sie anstelle von ILoggingEvent::getMessage ILoggingEvent::getFormattedMessage verwenden sollten, wenn Ihr Protokoll einen Parameterwert enthält. Andernfalls schlägt Ihr Assertion fehl, da der Wert fehlt.

    – Robert Maurer

    30. Juli 2019 um 15:10 Uhr

  • wenn Sie verwenden SLF4J Diese Lösung wird am Ende erhöhen SLF4J: Class path contains multiple SLF4J bindings. Warnung, da Sie sowohl SLF4J als auch logback.classic haben

    – Ghilteras

    24. Oktober 2019 um 18:52 Uhr

Benutzer-Avatar
Evgeniy Dorofeev

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 Wenn Sie keine Ausgabe erhalten, lesen Sie die Begründung im Kommentarbereich.

  • Beachten Sie, dass Sie logback classic + slf4j verwenden müssen ILoggingEvent Anstatt von LoggingEvent. Das hat bei mir funktioniert.

    – etech

    27. Mai 2016 um 20:52 Uhr


  • @Evgeniy Dorofeev Könnten Sie bitte zeigen, wie man logback-test.xml konfiguriert?

    – hipokito

    16. November 2016 um 17:16 Uhr

  • Ich nehme an, Sie müssen löschen events nach jeder Testausführung.

    – Andrii Karaiwanskyi

    17. Februar 2017 um 10:28 Uhr


  • @hipokito Sie können das erwähnte verwenden [here] (logback.qos.ch/manual/configuration.html) in sample0.xml. Vergessen Sie nicht, den Appender an Ihre Implementierung anzupassen

    – Codierungs_Idiot

    6. Juli 2017 um 22:00 Uhr


  • @EvgeniyDorofeev kannst du mir dabei helfen? stackoverflow.com/questions/48551083/…

    – Bhavya Arora

    31. Januar 2018 um 21:57 Uhr

Benutzer-Avatar
Oleg Majewski

Sie können slf4j-test von verwenden http://projects.lidalia.org.uk/slf4j-test/. Es ersetzt die gesamte logback slf4j-Implementierung durch seine eigene slf4j-API-Implementierung für Tests und stellt eine API bereit, die sich gegen Protokollierungsereignisse behaupten 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();
    }
}

  • 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 bereits die andere Antwort akzeptiert, die auch richtig ist.

    – Karlsfeder

    2. März 2016 um 10:51 Uhr

  • Vollständiges Beispiel mit Lidalia slf4j-test Paket finden Sie hier: github.com/jaegertracing/jaeger-client-java/pull/378/files

    – Debosmit Ray

    6. April 2018 um 1:58 Uhr

  • Diese Lösung funktioniert gut, wenn Sie Spring nicht verwenden. Wenn Sie Spring verwenden, wird eine Ausnahme “Klasse nicht gefunden” ausgelöst (JoranConfigurator).

    – Jesu H

    2. Januar 2020 um 18:54 Uhr

Benutzer-Avatar
Epoxid

Mit JUnit5

private ListAppender<ILoggingEvent> logWatcher;

@BeforeEach
void setup() {
  logWatcher = new ListAppender<>();
  logWatcher.start();
  ((Logger) LoggerFactory.getLogger(MyClass.class)).addAppender(logWatcher);
}

Notiz: MeineKlasse.Klasse – sollte Ihre Prod-Klasse sein, von der Sie die Protokollausgabe erwarten

use: (AssertJ-Beispiel)

@Test
void myMethod_logs2Messages() {

  ...
  int logSize = logWatcher.list.size();
  assertThat(logWatcher.list.get(logSize - 2).getFormattedMessage()).contains("EXPECTED MSG 1");
  assertThat(logWatcher.list.get(logSize - 1).getFormattedMessage()).contains("EXPECTED MSG 2");
}

zerstören:

Detach wird empfohlen, für eine bessere Leistung:

@AfterEach
void teardown() {
  ((Logger) LoggerFactory.getLogger(MyClass.class)).detachAndStopAllAppenders();
}

Importe:

import org.slf4j.LoggerFactory;
import ch.qos.logback.core.read.ListAppender;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.Logger;

Credits an: Antwort von @ davidxxx. Sehen Sie es für import ch.qos.logback... Details: https://stackoverflow.com/a/52229629/601844

Benutzer-Avatar
snovelli

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

MeineKlasse.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 eine Behauptung, anstatt zurückzukehren false da es den Code und (mögliche) Fehler leichter lesbar macht, aber es funktioniert nicht, wenn Sie mehrere Überprüfungen haben. In diesem Fall müssen Sie zurückkehren boolean zeigt an, ob der Wert wie erwartet ist.

  • funktioniert das, wenn ich die lombok.extern.slf4j-Anmerkungen 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 in meiner Klasse bereitgestellt wird …

    – enth

    1. April 2020 um 22:24 Uhr

  • @ennth Es sollte funktionieren, weil Sie den Schein mit der statischen Methode LoggerFactory.getLogger().addAppender(mockAppender) injizieren. Das funktioniert genauso, wenn Sie den Logger mit Lombok erstellen

    – Snovelli

    3. April 2020 um 12:22 Uhr


  • Habe das gleiche nicht funktionierende Problem. Was sind die ‘Importe’ für die Klassen Logger und LoggerFactory? Warum werden die statischen Importe aufgelistet und die anderen nicht?

    – Dirk Schumacher

    28. Mai 2020 um 7:40 Uhr

Benutzer-Avatar
Benutzer2179737

Obwohl das Erstellen eines benutzerdefinierten Logback-Appenders eine gute Lösung ist, ist dies nur der erste Schritt, Sie werden schließlich entwickeln/neu erfinden slf4j-testund wenn du etwas weiter gehst: spf4j-slf4j-test oder andere Frameworks, die ich noch nicht kenne.

Sie müssen sich schließlich Gedanken darüber machen, wie viele Ereignisse Sie im Speicher behalten, Unit-Tests fehlschlagen, wenn ein Fehler protokolliert (und nicht bestätigt) wird, Debug-Protokolle bei fehlgeschlagenen Tests verfügbar machen usw.

Haftungsausschluss: Ich bin der Autor von spf4j-slf4j-test, ich habe dieses Backend geschrieben, um besser testen zu können spf4j, das ein guter Ort ist, um nach Beispielen für die Verwendung von spf4j-slf4j-test zu suchen. 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.

  • funktioniert das, wenn ich die lombok.extern.slf4j-Anmerkungen 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 in meiner Klasse bereitgestellt wird …

    – enth

    1. April 2020 um 22:24 Uhr

  • @ennth Es sollte funktionieren, weil Sie den Schein mit der statischen Methode LoggerFactory.getLogger().addAppender(mockAppender) injizieren. Das funktioniert genauso, wenn Sie den Logger mit Lombok erstellen

    – Snovelli

    3. April 2020 um 12:22 Uhr


  • Habe das gleiche nicht funktionierende Problem. Was sind die ‘Importe’ für die Klassen Logger und LoggerFactory? Warum werden die statischen Importe aufgelistet und die anderen nicht?

    – Dirk Schumacher

    28. Mai 2020 um 7:40 Uhr

Benutzer-Avatar
oberlies

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();

Anruf log.getEvents() (oder andere benutzerdefinierte Methoden), um die protokollierten Ereignisse zu überprüfen.

  • Damit dies funktioniert, müssen Sie import ch.qos.logback.classic.Logger; Anstatt von import org.slf4j.LoggerFactory; Andernfalls addAppender() ist nicht verfügbar. Ich habe eine Weile gebraucht, um das herauszufinden.

    – Urs Beeli

    1. Oktober 2019 um 14:13 Uhr


  • Funktioniert nicht für mich. Es sieht so aus, als ob die Regel nicht richtig angewendet wird: Beim Debuggen habe ich festgestellt 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 einem bestimmten Paket abgelegt werden? Bitte fügen Sie Ihrer Antwort auch den Importabschnitt hinzu, da einige der Objekte/Schnittstellen mehrdeutige Namen haben.

    – Philzen

    23. Februar 2020 um 2:26 Uhr


1317730cookie-checkWie kann man die SLF4J-Protokollierung (mit Logback) über einen JUnit-Test abfangen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy