it-swarm.com.de

Wie kann die SLF4J-Protokollierung über einen JUnit-Test abgefangen werden?

Ist es möglich, die Protokollierung (SLF4J + logback) irgendwie abzufangen und eine InputStream (oder etwas anderes, das lesbar ist) über einen JUnit-Testfall zu erhalten ...?

32
carlspring

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 für die Verwendung. Jetzt können wir Protokollierungsereignisse aus unserem Test prüfen:

@Test
public void test() {
    ...
    Assert.assertEquals(1, TestAppender.events.size());
    ...
}
22

Sie können slf4j-test von http://projects.lidalia.org.uk/slf4j-test/ ..__ verwenden, es ersetzt die gesamte Implementierung von logback slf4j durch die eigene Implementierung von slf4j für Tests und stellt eine API bereit gegen Protokollierungsereignisse geltend machen.

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();
    }
}
11
Oleg Majewski

Die Slf4j-API bietet dies nicht, aber Logback bietet eine einfache Lösung.

Sie können ListAppender : einen Whitebox-Logback-Appender verwenden, in dem Protokolleinträge in einem public List-Feld hinzugefügt werden, das wir für unsere Assertions verwenden könnten. 

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 auch Matcher/Assertion-Bibliotheken als AssertJ oder Hamcrest verwenden.

Mit AssertJ wäre es: 

import org.assertj.core.api.Assertions;

Assertions.assertThat(listAppender.list)
          .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
          .containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));
8
davidxxx

Das Erstellen eines benutzerdefinierten Logback-Appenders ist zwar eine gute Lösung, aber es ist nur der erste Schritt. Am Ende werden Sie schließlich slf4j-test entwickeln/neu erfinden, und wenn Sie ein wenig weiter gehen: spf4j-slf4j-test oder andere Frameworks, die ich noch nicht kenne.

Sie müssen sich eventuell Gedanken darüber machen, wie viele Ereignisse Sie im Speicher behalten, Komponententests fehlschlagen, wenn ein Fehler protokolliert (und nicht aktiviert), Debug-Protokolle bei einem Testfehler 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. Hier können Sie Beispiele für die Verwendung von spf4j-slf4j-test finden. Einer der Hauptvorteile, die ich erzielt habe, war die Reduzierung meiner Build-Leistung (die bei Travis begrenzt ist), während ich trotzdem alle Details habe, die ich für den Fall eines Fehlers benötige.

3
user2179737

Ich hatte Probleme beim Testen von Protokollzeilen wie: LOGGER.error (Nachricht, Ausnahme).

Die in http://projects.lidalia.org.uk/slf4j-test/ beschriebene Lösung versucht ebenso die Ausnahme zu behaupten und es ist nicht leicht (und meiner Meinung nach wertlos), den Stacktrace neu zu erstellen.

Ich habe auf folgende 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 auf die Hamcrest matchers Bibliothek angewiesen zu sein.

2
daemon_nio

Ich würde eine einfache, wiederverwendbare Spion-Implementierung empfehlen, die in einem Test als JUnit-Regel enthalten sein 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.

1
oberlies

Eine einfache Lösung könnte sein, 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 false als Make-Code und (mögliche) Fehler leichter lesbar zurückzugeben.

0
snovelli