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 ...?
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());
...
}
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();
}
}
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));
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.
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.
Une solution simple pourrait être de se moquer de l'appender avec Mockito (par exemple)
@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;
}));
}
}
REMARQUE: j'utilise l'assertion plutôt que de renvoyer false
comme code d'erreur et erreur (possible) plus facile à lire.