Comment faire une affirmation JUnit sur un message dans un logger
j'ai un code-sous-test qui demande à un Java logger de signaler son statut. Dans le code de test de JUnit, j'aimerais vérifier que l'entrée correcte du journal a été faite dans cet enregistreur. Quelque chose du genre:
methodUnderTest(bool x){
if(x)
logger.info("x happened")
}
@Test tester(){
// perhaps setup a logger first.
methodUnderTest(true);
assertXXXXXX(loggedLevel(),Level.INFO);
}
je suppose que cela pourrait être fait avec un logger (ou handler, ou formater) spécialement adapté, mais je préférerais réutiliser une solution qui existe déjà. (Et, pour être honnête, il n'est pas clair pour moi comment faire pour obtenir l'logRecord d'un bûcheron, mais supposons que ce soit possible.)
20 réponses
j'en ai eu besoin plusieurs fois aussi. J'ai rassemblé un petit échantillon ci-dessous, que vous voudriez ajuster à vos besoins. Fondamentalement, vous créez votre propre Appender
et l'ajoutez à l'enregistreur que vous voulez. Si vous voulez tout rassembler, de la racine logger est un bon endroit pour commencer, mais vous pouvez utiliser un plus spécifiques si vous le souhaitez. N'oubliez pas de retirer L'Appender lorsque vous avez terminé, sinon vous pourriez créer une fuite de mémoire. Ci-dessous, je l'ai fait dans le test, mais setUp
ou @Before
et tearDown
ou @After
pourraient être de meilleurs endroits, selon vos besoins.
aussi, l'implémentation ci-dessous recueille tout dans un List
en mémoire. Si vous vous connectez beaucoup, vous pourriez envisager d'ajouter un filtre pour supprimer les entrées ennuyeuses , ou pour écrire le journal dans un fichier temporaire sur le disque (indice: LoggingEvent
est Serializable
, donc vous devriez être en mesure de sérialiser les objets d'événement, si votre message de journal est.)
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class MyTest {
@Test
public void test() {
final TestAppender appender = new TestAppender();
final Logger logger = Logger.getRootLogger();
logger.addAppender(appender);
try {
Logger.getLogger(MyTest.class).info("Test");
}
finally {
logger.removeAppender(appender);
}
final List<LoggingEvent> log = appender.getLog();
final LoggingEvent firstLogEntry = log.get(0);
assertThat(firstLogEntry.getLevel(), is(Level.INFO));
assertThat((String) firstLogEntry.getMessage(), is("Test"));
assertThat(firstLogEntry.getLoggerName(), is("MyTest"));
}
}
class TestAppender extends AppenderSkeleton {
private final List<LoggingEvent> log = new ArrayList<LoggingEvent>();
@Override
public boolean requiresLayout() {
return false;
}
@Override
protected void append(final LoggingEvent loggingEvent) {
log.add(loggingEvent);
}
@Override
public void close() {
}
public List<LoggingEvent> getLog() {
return new ArrayList<LoggingEvent>(log);
}
}
Merci beaucoup pour ces réponses (étonnamment) rapides et utiles; ils m'ont mis sur la bonne voie pour ma solution.
le codebase où je veux utiliser ceci, utilise java.util.logging comme son mécanisme logger, et je ne me sens pas assez à la maison dans ces codes pour changer complètement que pour log4j ou logger interfaces/façades. Mais sur la base de ces suggestions, je 'piraté' a j.u.l'.gestionnaire d'extension et qui fonctionne comme une friandise.
un bref résumé suivre. Étendre java.util.logging.Handler
:
class LogHandler extends Handler
{
Level lastLevel = Level.FINEST;
public Level checkLevel() {
return lastLevel;
}
public void publish(LogRecord record) {
lastLevel = record.getLevel();
}
public void close(){}
public void flush(){}
}
évidemment, vous pouvez stocker autant que vous voulez/besoin de la LogRecord
, ou les pousser tous dans une pile jusqu'à ce que vous obtenez un débordement.
dans la préparation pour le test de junit, vous créez un java.util.logging.Logger
et y ajoutez un nouveau LogHandler
:
@Test tester() {
Logger logger = Logger.getLogger("my junit-test logger");
LogHandler handler = new LogHandler();
handler.setLevel(Level.ALL);
logger.setUseParentHandlers(false);
logger.addHandler(handler);
logger.setLevel(Level.ALL);
l'appel à setUseParentHandlers()
est de réduire au silence les manipulateurs normaux, de sorte que (pour ce test de junit) pas inutile la journalisation se passe. Faites tout ce que votre code-under-test a besoin pour utiliser cet enregistreur, lancez le test et assertEquality:
libraryUnderTest.setLogger(logger);
methodUnderTest(true); // see original question.
assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() );
}
(bien sûr, vous déplaceriez une grande partie de ce travail dans une méthode @Before
et faire diverses autres améliorations, mais qui encombrerait cette présentation.)
effectivement, vous testez un effet secondaire d'une classe dépendante. Pour les tests unitaires, vous avez seulement besoin de vérifier que
logger.info()
a été appelé avec le bon paramètre. Par conséquent, utilisez un cadre de moquerie pour émuler logger et qui vous permettra de tester le comportement de votre propre classe.
moquerie est une option ici, bien que ce serait difficile, parce que les loggers sont généralement privée finale statique - donc la mise en place d'un logger simulé ne serait pas une partie de gâteau, ou exigerait la modification de la classe à l'essai.
vous pouvez créer un Appender personnalisé (ou quel que soit son nom), et l'enregistrer - soit via un fichier de configuration de test seulement, ou l'exécution (d'une manière, dépendant du cadre de journalisation). Et puis vous pouvez obtenir cette appender (soit statiquement, si déclaré dans le fichier de configuration, ou par sa référence courante, si vous le branchez l'exécution), et vérifier son contenu.
une autre option est de simuler Appender et de vérifier si le message a été enregistré dans cet appender. Exemple pour Log4j 1.2.x et mockito:
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class MyTest {
private final Appender appender = mock(Appender.class);
private final Logger logger = Logger.getRootLogger();
@Before
public void setup() {
logger.addAppender(appender);
}
@Test
public void test() {
// when
Logger.getLogger(MyTest.class).info("Test");
// then
ArgumentCaptor<LoggingEvent> argument = ArgumentCaptor.forClass(LoggingEvent.class);
verify(appender).doAppend(argument.capture());
assertEquals(Level.INFO, argument.getValue().getLevel());
assertEquals("Test", argument.getValue().getMessage());
assertEquals("MyTest", argument.getValue().getLoggerName());
}
@After
public void cleanup() {
logger.removeAppender(appender);
}
}
Voici ce que j'ai fait pour logback.
j'ai créé une classe testamentaire:
public class TestAppender extends AppenderBase<ILoggingEvent> {
private Stack<ILoggingEvent> events = new Stack<ILoggingEvent>();
@Override
protected void append(ILoggingEvent event) {
events.add(event);
}
public void clear() {
events.clear();
}
public ILoggingEvent getLastEvent() {
return events.pop();
}
}
puis dans le parent de ma classe de test unité testng j'ai créé une méthode:
protected TestAppender testAppender;
@BeforeClass
public void setupLogsForTesting() {
Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
testAppender = (TestAppender)root.getAppender("TEST");
if (testAppender != null) {
testAppender.clear();
}
}
j'ai un test de retour.fichier xml défini dans src / test / resources et j'ai ajouté un éditeur de test:
<appender name="TEST" class="com.intuit.icn.TestAppender">
<encoder>
<pattern>%m%n</pattern>
</encoder>
</appender>
et a ajouté cet accessoire à l'accessoire de racine:
<root>
<level value="error" />
<appender-ref ref="STDOUT" />
<appender-ref ref="TEST" />
</root>
maintenant dans mon test classes qui s'étendent de ma classe de test parent je peux obtenir l'appender et obtenir le dernier message enregistré et vérifier le message, le niveau, le lancable.
ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
inspiré par la solution de @RonaldBlaschke, j'ai trouvé ceci:
public class Log4JTester extends ExternalResource {
TestAppender appender;
@Override
protected void before() {
appender = new TestAppender();
final Logger rootLogger = Logger.getRootLogger();
rootLogger.addAppender(appender);
}
@Override
protected void after() {
final Logger rootLogger = Logger.getRootLogger();
rootLogger.removeAppender(appender);
}
public void assertLogged(Matcher<String> matcher) {
for(LoggingEvent event : appender.events) {
if(matcher.matches(event.getMessage())) {
return;
}
}
fail("No event matches " + matcher);
}
private static class TestAppender extends AppenderSkeleton {
List<LoggingEvent> events = new ArrayList<LoggingEvent>();
@Override
protected void append(LoggingEvent event) {
events.add(event);
}
@Override
public void close() {
}
@Override
public boolean requiresLayout() {
return false;
}
}
}
... ce qui vous permet de faire:
@Rule public Log4JTester logTest = new Log4JTester();
@Test
public void testFoo() {
user.setStatus(Status.PREMIUM);
logTest.assertLogged(
stringContains("Note added to account: premium customer"));
}
vous pourriez probablement lui faire utiliser hamcrest d'une manière plus intelligente, mais je l'ai laissé à ceci.
comme mentionné par les autres, vous pourriez utiliser un cadre de moquerie. Pour que cela fonctionne, vous devez exposer le logger dans votre classe (bien que je préfèrerais de façon appropriée le faire package privé au lieu de créer un setter public).
L'autre solution est de créer un faux enregistreur à la main. Vous devez écrire le faux logger (plus de code de fixture) mais dans ce cas, je préférerais la lisibilité améliorée des tests par rapport au code sauvegardé à partir de la moquerie Framework.
je ferais quelque chose comme ça:
class FakeLogger implements ILogger {
public List<String> infos = new ArrayList<String>();
public List<String> errors = new ArrayList<String>();
public void info(String message) {
infos.add(message);
}
public void error(String message) {
errors.add(message);
}
}
class TestMyClass {
private MyClass myClass;
private FakeLogger logger;
@Before
public void setUp() throws Exception {
myClass = new MyClass();
logger = new FakeLogger();
myClass.logger = logger;
}
@Test
public void testMyMethod() {
myClass.myMethod(true);
assertEquals(1, logger.infos.size());
}
}
quant à moi, vous pouvez simplifier votre test en utilisant JUnit
avec Mockito
.
Je propose pour cela la solution suivante:
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.times;
@RunWith(MockitoJUnitRunner.class)
public class MyLogTest {
private static final String FIRST_MESSAGE = "First message";
private static final String SECOND_MESSAGE = "Second message";
@Mock private Appender appender;
@Captor private ArgumentCaptor<LoggingEvent> captor;
@InjectMocks private MyLog;
@Before
public void setUp() {
LogManager.getRootLogger().addAppender(appender);
}
@After
public void tearDown() {
LogManager.getRootLogger().removeAppender(appender);
}
@Test
public void shouldLogExactlyTwoMessages() {
testedClass.foo();
then(appender).should(times(2)).doAppend(captor.capture());
List<LoggingEvent> loggingEvents = captor.getAllValues();
assertThat(loggingEvents).extracting("level", "renderedMessage").containsExactly(
tuple(Level.INFO, FIRST_MESSAGE)
tuple(Level.INFO, SECOND_MESSAGE)
);
}
}
C'est pourquoi nous avons nice flexibilité pour les tests avec quantité de message différente
Wow. Je ne suis pas sûr pourquoi c'était si dur. J'ai trouvé que je n'étais pas capable d'utiliser les échantillons de code ci-dessus parce que j'utilisais log4j2 au lieu de slf4j. C'est ma solution:
public class SpecialLogServiceTest {
@Mock
private Appender appender;
@Captor
private ArgumentCaptor<LogEvent> captor;
@InjectMocks
private SpecialLogService specialLogService;
private LoggerConfig loggerConfig;
@Before
public void setUp() {
// prepare the appender so Log4j likes it
when(appender.getName()).thenReturn("MockAppender");
when(appender.isStarted()).thenReturn(true);
when(appender.isStopped()).thenReturn(false);
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
final Configuration config = ctx.getConfiguration();
loggerConfig = config.getLoggerConfig("org.example.SpecialLogService");
loggerConfig.addAppender(appender, AuditLogCRUDService.LEVEL_AUDIT, null);
}
@After
public void tearDown() {
loggerConfig.removeAppender("MockAppender");
}
@Test
public void writeLog_shouldCreateCorrectLogMessage() throws Exception {
SpecialLog specialLog = new SpecialLogBuilder().build();
String expectedLog = "this is my log message";
specialLogService.writeLog(specialLog);
verify(appender).append(captor.capture());
assertThat(captor.getAllValues().size(), is(1));
assertThat(captor.getAllValues().get(0).getMessage().toString(), is(expectedLog));
}
}
pour log4j2 la solution est légèrement différente car AppenderSkeleton n'est plus disponible. De plus, utiliser Mockito, ou une bibliothèque similaire pour créer un Appender avec un ArgumentCaptor ne fonctionnera pas si vous attendez plusieurs messages de journalisation parce que le MutableLogEvent est réutilisé sur plusieurs messages de journalisation. La meilleure solution que j'ai trouvée pour log4j2 est:
private static MockedAppender mockedAppender;
private static Logger logger;
@Before
public void setup() {
mockedAppender.message.clear();
}
/**
* For some reason mvn test will not work if this is @Before, but in eclipse it works! As a
* result, we use @BeforeClass.
*/
@BeforeClass
public static void setupClass() {
mockedAppender = new MockedAppender();
logger = (Logger)LogManager.getLogger(MatchingMetricsLogger.class);
logger.addAppender(mockedAppender);
logger.setLevel(Level.INFO);
}
@AfterClass
public static void teardown() {
logger.removeAppender(mockedAppender);
}
@Test
public void test() {
// do something that causes logs
for (String e : mockedAppender.message) {
// add asserts for the log messages
}
}
private static class MockedAppender extends AbstractAppender {
List<String> message = new ArrayList<>();
protected MockedAppender() {
super("MockedAppender", null, null);
}
@Override
public void append(LogEvent event) {
message.add(event.getMessage().getFormattedMessage());
}
}
une autre idée qui mérite d'être mentionnée, bien que ce soit un sujet plus ancien, est de créer un producteur CDI pour injecter votre logger afin que la moquerie devienne facile. (Et cela donne aussi l'avantage de ne plus avoir à déclarer la "déclaration entière de l'enregistreur", mais c'est hors sujet)
exemple:
création de l'enregistreur pour injecter:
public class CdiResources {
@Produces @LoggerType
public Logger createLogger(final InjectionPoint ip) {
return Logger.getLogger(ip.getMember().getDeclaringClass());
}
}
le qualificatif:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface LoggerType {
}
en utilisant l'enregistreur dans votre code de production:
public class ProductionCode {
@Inject
@LoggerType
private Logger logger;
public void logSomething() {
logger.info("something");
}
}
tester l'enregistreur dans votre code de test (en donnant un exemple de code easyMock):
@TestSubject
private ProductionCode productionCode = new ProductionCode();
@Mock
private Logger logger;
@Test
public void testTheLogger() {
logger.info("something");
replayAll();
productionCode.logSomething();
}
en utilisant Jmockit (1.21) j'ai pu écrire ce test simple. Le test permet de s'assurer qu'un message d'erreur spécifique n'est appelé qu'une seule fois.
@Test
public void testErrorMessage() {
final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger( MyConfig.class );
new Expectations(logger) {{
//make sure this error is happens just once.
logger.error( "Something went wrong..." );
times = 1;
}};
new MyTestObject().runSomethingWrong( "aaa" ); //SUT that eventually cause the error in the log.
}
moquerie de L'Appender peut aider à capturer les lignes de log. Trouver l'échantillon sur: http://clearqa.blogspot.co.uk/2016/12/test-log-lines.html
// Fully working test at: https://github.com/njaiswal/logLineTester/blob/master/src/test/java/com/nj/Utils/UtilsTest.java
@Test
public void testUtilsLog() throws InterruptedException {
Logger utilsLogger = (Logger) LoggerFactory.getLogger("com.nj.utils");
final Appender mockAppender = mock(Appender.class);
when(mockAppender.getName()).thenReturn("MOCK");
utilsLogger.addAppender(mockAppender);
final List<String> capturedLogs = Collections.synchronizedList(new ArrayList<>());
final CountDownLatch latch = new CountDownLatch(3);
//Capture logs
doAnswer((invocation) -> {
LoggingEvent loggingEvent = invocation.getArgumentAt(0, LoggingEvent.class);
capturedLogs.add(loggingEvent.getFormattedMessage());
latch.countDown();
return null;
}).when(mockAppender).doAppend(any());
//Call method which will do logging to be tested
Application.main(null);
//Wait 5 seconds for latch to be true. That means 3 log lines were logged
assertThat(latch.await(5L, TimeUnit.SECONDS), is(true));
//Now assert the captured logs
assertThat(capturedLogs, hasItem(containsString("One")));
assertThat(capturedLogs, hasItem(containsString("Two")));
assertThat(capturedLogs, hasItem(containsString("Three")));
}
utilisez le code ci-dessous. J'utilise le même code pour mon test d'intégration de printemps où j'utilise log back pour la journalisation. Utilisez la méthode assertJobIsScheduled pour affirmer le texte imprimé dans le journal.
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
private Logger rootLogger;
final Appender mockAppender = mock(Appender.class);
@Before
public void setUp() throws Exception {
initMocks(this);
when(mockAppender.getName()).thenReturn("MOCK");
rootLogger = (Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(mockAppender);
}
private void assertJobIsScheduled(final String matcherText) {
verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
@Override
public boolean matches(final Object argument) {
return ((LoggingEvent)argument).getFormattedMessage().contains(matcherText);
}
}));
}
si vous utilisez java.util.logging.Logger
cet article peut être très utile, il crée un nouveau handler et faire des affirmations sur la sortie log:
http://octodecillion.com/blog/jmockit-test-logging /
il y a deux choses que vous pourriez essayer de tester.
- Quand il s'agit d'un événement d'intérêt pour l'opérateur de mon programme, mon programme d'effectuer un enregistrement approprié opération, qui peut informer l'exploitant de cet événement.
- lorsque mon programme effectue une opération de journalisation, le message de journalisation qu'il produit a-t-il le texte correct?
Ces deux choses sont en fait des choses différentes, et donc peut être testé séparément. Cependant, tester le second (le texte des messages) est tellement problématique, je recommande de ne pas le faire du tout. Un test du texte d'un message consistera en fin de compte à vérifier qu'une chaîne de texte (le texte du message attendu) est la même que la chaîne de texte utilisée dans votre code de journalisation, ou peut être dérivée trivialement de celle-ci.
- ces tests ne testent pas du tout la logique du programme, ils testent seulement qu'une ressource (une chaîne) est équivalente à une autre ressources.
- les tests sont fragiles; même une modification mineure au formatage d'un message de log casse vos tests.
- les tests sont incompatibles avec l'internationalisation (traduction) de votre interface de journalisation.Les tests supposent qu'il n'y a qu'un seul texte de message possible, et donc qu'un seul langage humain possible.
notez qu'avoir votre code de programme (mettant en œuvre une certaine logique opérationnelle, peut-être) appelant directement le texte journalisation de l'interface est mauvaise conception (mais malheureusement très commum). Le Code qui est responsable de la logique d'affaires est aussi en train de décider de certaines politiques d'enregistrement et du texte des messages d'enregistrement. Il mélange la logique d'affaires avec le code d'interface utilisateur (Oui, les messages de journal font partie de l'interface utilisateur de votre programme). Ces choses devraient être séparés.
je recommande donc que business logic ne génère pas directement le texte des messages log. Au contraire, il déléguer à un enregistrement de l'objet.
- la classe de l'objet de journalisation doit fournir une API interne appropriée, que votre objet d'affaires peut utiliser pour exprimer l'événement qui s'est produit en utilisant des objets de votre modèle de domaine, pas des chaînes de texte.
- la mise en œuvre de votre classe de journalisation est responsable de produire des représentations textuelles de ces objets de domaine, et de rendre une description textuelle appropriée de l'événement, puis de transmettre ce message texte à la journalisation de bas niveau cadre (comme JUL, log4j ou slf4j).
- votre logique d'entreprise est responsable uniquement de l'appel des méthodes correctes de L'API interne de votre classe logger, en passant les objets de domaine corrects, pour décrire les événements réels qui se sont produits.
- Votre béton classe de log
implements
uninterface
, qui décrit l'API interne de votre logique métier peut utiliser. - Votre classe(s) qui implémente la logique d'entreprise et doit effectuer logging a une référence à l'objet logging à déléguer. La classe de la référence est l'abrégé
interface
. - utilisez l'injection de dépendances pour configurer la référence à l'enregistreur.
vous pouvez ensuite tester que vos classes de logique d'entreprise informent correctement l'interface de journalisation des événements, en créant un Logger simulé, qui implémente L'API de journalisation interne, et en utilisant l'injection de dépendances dans la phase de configuration de votre test.
comme ceci:
public class MyService {// The class we want to test
private final MyLogger logger;
public MyService(MyLogger logger) {
this.logger = Objects.requireNonNull(logger);
}
public void performTwiddleOperation(Foo foo) {// The method we want to test
...// The business logic
logger.performedTwiddleOperation(foo);
}
};
public interface MyLogger {
public void performedTwiddleOperation(Foo foo);
...
};
public final class MySl4jLogger: implements MyLogger {
...
@Override
public void performedTwiddleOperation(Foo foo) {
logger.info("twiddled foo " + foo.getId());
}
}
public final void MyProgram {
public static void main(String[] argv) {
...
MyLogger logger = new MySl4jLogger(...);
MyService service = new MyService(logger);
startService(service);// or whatever you must do
...
}
}
public class MyServiceTest {
...
static final class MyMockLogger: implements MyLogger {
private Food.id id;
private int nCallsPerformedTwiddleOperation;
...
@Override
public void performedTwiddleOperation(Foo foo) {
id = foo.id;
++nCallsPerformedTwiddleOperation;
}
void assertCalledPerformedTwiddleOperation(Foo.id id) {
assertEquals("Called performedTwiddleOperation", 1, nCallsPerformedTwiddleOperation);
assertEquals("Called performedTwiddleOperation with correct ID", id, this.id);
}
};
@Test
public void testPerformTwiddleOperation_1() {
// Setup
MyMockLogger logger = new MyMockLogger();
MyService service = new MyService(logger);
Foo.Id id = new Foo.Id(...);
Foo foo = new Foo(id, 1);
// Execute
service.performedTwiddleOperation(foo);
// Verify
...
logger.assertCalledPerformedTwiddleOperation(id);
}
}
ce que j'ai fait si tout ce que je veux faire est de voir qu'une chaîne de caractères a été journalisée (par opposition à la vérification de déclarations de journalisation exactes qui est tout simplement trop fragile) est de rediriger StdOut vers un buffer, faire un contains, puis réinitialiser StdOut:
PrintStream original = System.out;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer));
// Do something that logs
assertTrue(buffer.toString().contains(myMessage));
System.setOut(original);
L'API de Log4J2 est légèrement différente. Aussi, vous pourriez utiliser son async appender. J'ai créé un appender verrouillé pour cela:
public static class LatchedAppender extends AbstractAppender implements AutoCloseable {
private final List<LogEvent> messages = new ArrayList<>();
private final CountDownLatch latch;
private final LoggerConfig loggerConfig;
public LatchedAppender(Class<?> classThatLogs, int expectedMessages) {
this(classThatLogs, null, null, expectedMessages);
}
public LatchedAppender(Class<?> classThatLogs, Filter filter, Layout<? extends Serializable> layout, int expectedMessages) {
super(classThatLogs.getName()+"."+"LatchedAppender", filter, layout);
latch = new CountDownLatch(expectedMessages);
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
final Configuration config = ctx.getConfiguration();
loggerConfig = config.getLoggerConfig(LogManager.getLogger(classThatLogs).getName());
loggerConfig.addAppender(this, Level.ALL, ThresholdFilter.createFilter(Level.ALL, null, null));
start();
}
@Override
public void append(LogEvent event) {
messages.add(event);
latch.countDown();
}
public List<LogEvent> awaitMessages() throws InterruptedException {
assertTrue(latch.await(10, TimeUnit.SECONDS));
return messages;
}
@Override
public void close() {
stop();
loggerConfig.removeAppender(this.getName());
}
}
utilisez - le comme ceci:
try (LatchedAppender appender = new LatchedAppender(ClassUnderTest.class, 1)) {
ClassUnderTest.methodThatLogs();
List<LogEvent> events = appender.awaitMessages();
assertEquals(1, events.size());
//more assertions here
}//appender removed
Voici une solution de Logback simple et efficace.
Il ne nécessite pas d'ajouter/créer une nouvelle classe.
Elle s'appuie sur ListAppender
: un appender de logback whitebox où les entrées de log sont ajoutées dans un champ public List
que nous pourrions donc utiliser pour faire 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");
}
}
FooTest classe:
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());
}
}
JUnit affirmations ne sonnent pas très adapté pour faire valoir certaines propriétés spécifiques des éléments de la liste.
Matcher/affirmation bibliothèques AssertJ ou Hamcrest semble mieux pour que :
avec AssertJ il 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));