web-dev-qa-db-fra.com

Comment intercepter la journalisation SLF4J via un test JUnit?

Est-il possible d'intercepter en quelque sorte la journalisation (SLF4J + logback) et d'obtenir une InputStream (ou autre chose lisible) via un scénario de test JUnit ...?

32
carlspring

Vous pouvez créer un appender personnalisé 

public class TestAppender extends AppenderBase<LoggingEvent> {
    static List<LoggingEvent> events = new ArrayList<>();

    @Override
    protected void append(LoggingEvent e) {
        events.add(e);
    }
}

et configurez logback-test.xml pour l'utiliser. Nous pouvons maintenant vérifier les événements de journalisation à partir de notre test:

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

Vous pouvez utiliser slf4j-test à partir de http://projects.lidalia.org.uk/slf4j-test/ . Il remplace l'intégralité de l'implémentation de journalisation slf4j par sa propre implémentation d'api slf4j pour les tests et fournit une faire valoir contre les événements de journalisation.

exemple:

<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

L'API Slf4j ne fournit pas un tel moyen mais Logback fournit une solution simple.

Vous pouvez utiliser ListAppender : un appender de consignation de boîte blanche dans lequel des entrées de journal sont ajoutées dans un champ public List que nous pourrions utiliser pour nos assertions. 

Voici un exemple simple. 

Classe Foo:

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

Classe FooTest:

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

Vous pouvez également utiliser les bibliothèques Matcher/assertion comme AssertJ ou Hamcrest.

Avec AssertJ ce serait: 

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

Bien que créer un appender personnalisé de logback soit une bonne solution, ce n’est que la première étape, vous finirez par développer/réinventer slf4j-test , et si vous allez un peu plus loin: spf4j-slf4j-test ou d'autres cadres que je ne connais pas encore.

Vous devrez éventuellement vous inquiéter du nombre d'événements que vous gardez en mémoire, échouer les tests unitaires lorsqu'une erreur est consignée (et non confirmée), rendre les journaux de débogage disponibles en cas d'échec du test, etc.

Disclaimer: Je suis l'auteur de spf4j-slf4j-test, j'ai écrit ce backend pour pouvoir mieux tester spf4j , qui est un bon endroit pour consulter des exemples d'utilisation de spf4j-slf4j-test. L'un des principaux avantages que j'ai obtenus est la réduction de ma production (limitée avec Travis), tout en conservant tous les détails dont j'ai besoin en cas d'échec.

3
user2179737

J'ai eu des problèmes lors du test des journaux comme: LOGGER.error (message, exception).

La solution décrite dans http://projects.lidalia.org.uk/slf4j-test/ essaie de s'affirmer également à l'exception et il n'est pas facile (et à mon avis inutile) de recréer la pile.

J'ai résolu de cette façon:

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

}

Cela a aussi l’avantage de ne pas dépendre de la bibliothèque Hamcrest matchers.

2
daemon_nio

Une solution simple pourrait être de se moquer de l'appender avec Mockito (par exemple)

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

    }

}

REMARQUE: j'utilise l'assertion plutôt que de renvoyer false comme code d'erreur et erreur (possible) plus facile à lire.

0
snovelli