web-dev-qa-db-fra.com

Comment faire une assertion JUnit sur un message dans un enregistreur

J'ai un code sous test qui appelle un enregistreur Java pour signaler son état . Dans le code de test JUnit, j'aimerais vérifier que l'entrée de journal correcte a été effectuée dans cet enregistreur. Quelque chose dans les lignes suivantes:

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 enregistreur spécialement adapté (ou un gestionnaire, ou un formateur), mais je préférerais réutiliser une solution déjà existante. (Et, pour être honnête, il n’est pas clair pour moi comment obtenir l’enregistrement logRecord à partir d’un enregistreur, mais supposons que cela soit possible.)

134
Jon

Merci beaucoup pour ces réponses (étonnamment) rapides et utiles; ils m'ont mis sur la bonne voie pour ma solution.

La base de code où je veux utiliser ceci utilise Java.util.logging comme mécanisme de journalisation, et je ne me sens pas assez à l'aise dans ces codes pour changer complètement cela en log4j ou en interfaces/façades de journalisation. Mais sur la base de ces suggestions, je "piraté" une extension j.u.l.handler et cela fonctionne comme un régal.

Un bref résumé suit. É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(){}
}

De toute évidence, vous pouvez stocker autant que vous le souhaitez/vouloir/avoir besoin de la LogRecord, ou les mettre tous dans une pile jusqu'à ce que vous obteniez un débordement.

Lors de la préparation du test Junit, vous créez un Java.util.logging.Logger auquel vous ajoutez une nouvelle 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() consiste à faire taire les gestionnaires normaux, afin que (pour cette exécution du test Junit) aucune journalisation inutile ne se produise. Faites tout ce dont votre code à tester 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 entendu, vous déplaceriez une grande partie de ce travail dans une méthode @Before et apporteriez diverses améliorations, mais cela encombrerait cette présentation.)

31
Jon

J'ai eu besoin de cela plusieurs fois aussi. J'ai rassemblé ci-dessous un petit échantillon que vous voudriez adapter à vos besoins. En gros, vous créez votre propre Appender et vous l'ajoutez à l'enregistreur souhaité. Si vous souhaitez tout collecter, le logger racine est un bon point de départ, mais vous pouvez en utiliser un plus spécifique si vous le souhaitez. N'oubliez pas de supprimer 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, en fonction de vos besoins.

En outre, l’implémentation ci-dessous collecte tout ce qui se trouve dans un List en mémoire. Si vous vous connectez beaucoup, vous pouvez envisager d’ajouter un filtre pour supprimer les entrées ennuyeuses ou pour écrire le journal dans un fichier temporaire sur le disque (astuce: LoggingEvent est Serializable, vous devriez donc pouvoir sérialiser les objets le message du 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);
    }
}
124
Ronald Blaschke

Effectivement, vous testez un effet secondaire d'une classe dépendante. Pour les tests unitaires, il vous suffit de vérifier que

logger.info()

a été appelé avec le paramètre correct. Utilisez donc un framework moqueur pour émuler le logger et cela vous permettra de tester le comportement de votre propre classe.

16
djna

Voici une solution de consignation simple et efficace.
Il n’est pas nécessaire d’ajouter/créer une nouvelle classe.
Il s’appuie sur ListAppender : un ajouteur de journal de boîte blanche dans lequel les entrées de journal sont ajoutées dans un champ public List que nous pourrions ainsi 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");
    }
}

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

Les assertions JUnit ne paraissent pas très adaptées pour affirmer certaines propriétés spécifiques des éléments de la liste.
Les bibliothèques Matcher/assertion comme AssertJ ou Hamcrest semblent mieux pour cela:

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));
15
davidxxx

Une autre option consiste à simuler Appender et à vérifier si un message a été connecté à 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);
    }
}
12
Marcin

Le moquage est une option ici, bien que cela soit difficile, car les enregistreurs sont généralement des statiques finales privées. Par conséquent, la configuration d'un enregistreur factice ne serait pas un jeu d'enfant ou exigerait une modification de la classe à tester.

Vous pouvez créer un Appender personnalisé (ou le nom de son choix) et l'enregistrer, soit via un fichier de configuration réservé au test, soit à l'exécution (d'une certaine manière, en fonction du cadre de journalisation) . Vous pouvez ensuite obtenir cet appender. (soit statiquement, si déclaré dans le fichier de configuration, soit par sa référence actuelle, si vous le branchez au moment de l'exécution), et vérifiez son contenu.

10
Bozho

Inspiré par la solution de @ RonaldBlaschke, j'ai proposé 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 le faire utiliser de façon plus intelligente hamcrest, mais je m'en suis tenu à cela.

5
slim

Voici ce que j'ai fait pour la journalisation.

J'ai créé une classe TestAppender:

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 unitaire 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 fichier logback-test.xml défini dans src/test/resources et j'ai ajouté un testeur de test:

<appender name="TEST" class="com.intuit.icn.TestAppender">
    <encoder>
        <pattern>%m%n</pattern>
    </encoder>
</appender>

et ajouté cet appender à l'appender root:

<root>
    <level value="error" />
    <appender-ref ref="STDOUT" />
    <appender-ref ref="TEST" />
</root>

Maintenant, dans mes classes de test qui s'étendent de ma classe de test parent, je peux obtenir l'appender, obtenir le dernier message enregistré et vérifier le message, le niveau, le jetable.

ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
4
kfox

Comme mentionné par les autres, vous pouvez utiliser un cadre moqueur. Pour que cela fonctionne, vous devez exposer l'enregistreur de votre classe (bien que je préférerais que le paquet soit privé plutôt que de créer un séparateur public).

L'autre solution consiste à créer un faux enregistreur à la main. Vous devez écrire le faux enregistreur (plus de code d'installation) mais dans ce cas, je préférerais une lisibilité améliorée des tests par rapport au code enregistré dans le cadre moqueur.

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

Sensationnel. Je ne sais pas pourquoi c'était si difficile. J'ai constaté que je ne pouvais utiliser aucun des exemples de code ci-dessus parce que j'utilisais log4j2 sur slf4j. Ceci 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));
  }
}
3
Dagmar

Pour log4j2, la solution est légèrement différente car AppenderSkeleton n'est plus disponible. En outre, l'utilisation de Mockito ou d'une bibliothèque similaire pour créer un Appender avec un ArgumentCaptor ne fonctionnera pas si vous attendez plusieurs messages de journalisation car MutableLogEvent est réutilisé avec plusieurs messages de journal. 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());
    }
}
2
joseph

Pour ma part, vous pouvez simplifier votre test en utilisant JUnit avec Mockito. Je vous propose 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 messages différente

2
Dmytro Melnychuk

Mocking the Appender peut vous aider à capturer les lignes de journal . Vous trouverez un exemple 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")));
}
0
nishant

En utilisant Jmockit (1.21), j'ai pu écrire ce test simple. Le test s'assure 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.    
}
0
Yarix

Ce que j'ai fait si tout ce que je veux faire, c'est voir qu'une chaîne a été enregistrée (par opposition à la vérification d'instructions de journal exactes et trop fragiles) consiste à rediriger StdOut vers un tampon, à effectuer un conteneur, 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);
0
Cheefachi

si vous utilisez Java.util.logging.Logger, cet article peut s'avérer très utile. Il crée un nouveau gestionnaire et formule des assertions dans le journal. Sortie: http://octodecillion.com/blog/jmockit-test-logging/

0
Mehdi Karamosly

Utilisez le code ci-dessous. J'utilise le même code pour mon test d'intégration Spring, 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);
        }
    }));
}
0
SUMIT

Vous pouvez essayer de tester deux choses.

  • Lorsqu'il y a un événement intéressant pour l'opérateur de mon programme, mon programme effectue-t-il une opération de journalisation appropriée, qui peut informer l'opérateur de cet événement?.
  • Lorsque mon programme effectue une opération de journalisation, le message de journal qu’il produit contient-il le texte correct?.

Ces deux choses sont en réalité différentes, et pourraient donc être testées séparément. Cependant, tester le second (le texte des messages) est tellement problématique que je recommande de ne pas le faire du tout. En fin de compte, le test du texte d'un message consiste à vérifier qu'une chaîne de texte (le texte du message attendu) est identique à la chaîne de texte utilisée dans votre code de journalisation ou qu'elle peut en être dérivée de manière triviale.

  • Ces tests ne testent pas du tout la logique du programme, ils testent uniquement qu'une ressource (une chaîne) est équivalente à une autre ressource.
  • Les tests sont fragiles. même un ajustement mineur à la mise en forme d'un message de journal interrompt vos tests.
  • Les tests sont incompatibles avec l’internationalisation (traduction) de votre interface de journalisation. Ils supposent qu’un seul texte de message est possible, et donc qu’une seule langue humaine est possible.

Notez qu'avoir votre code de programme (implémentant peut-être une logique métier) appelant directement l'interface de journalisation de texte est de mauvaise conception (mais malheureusement très commun). Le code responsable de la logique applicative décide également de la stratégie de journalisation et du texte des messages de journalisation. Il associe la logique métier au code de l'interface utilisateur (oui, les messages de journalisation font partie de l'interface utilisateur de votre programme). Ces choses devraient être séparées.

Je recommande donc que la logique applicative ne génère pas directement le texte des messages de journal. Au lieu de cela, déléguez-le à un objet de journalisation.

  • La classe de l'objet de journalisation doit fournir une API interne appropriée, que votre objet métier peut utiliser pour exprimer l'événement qui s'est produit à l'aide d'objets de votre modèle de domaine, et non de chaînes de texte.
  • L'implémentation de votre classe de journalisation est chargée de générer des représentations textuelles de ces objets de domaine et de restituer une description textuelle appropriée de l'événement, puis de transférer ce message texte au cadre de journalisation de bas niveau (tel que JUL, log4j ou slf4j).
  • Votre logique d’entreprise n’est responsable que d’appeler les méthodes correctes de l’API interne de votre classe de consignateur, en transmettant les objets de domaine corrects, afin de décrire les événements réels survenus.
  • Votre classe de consignation concrète implements et interface, qui décrit l'API interne que votre logique métier peut utiliser.
  • Votre classe qui implémente la logique applicative et doit effectuer la journalisation a une référence à l'objet de journalisation à déléguer. La classe de la référence est le résumé interface.
  • Utilisez l'injection de dépendance pour configurer la référence à l'enregistreur.

Vous pouvez ensuite vérifier que vos classes de logique métier communiquent correctement les événements à l'interface de journalisation en créant un journal factice implémentant l'API de journalisation interne et en utilisant l'injection de dépendances lors de la phase de configuration de votre test.

Comme ça:

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

Une autre idée qui mérite d'être mentionnée, même s'il s'agit d'un sujet plus ancien, consiste à créer un producteur de CDI pour injecter votre enregistreur afin de faciliter les moqueries. (Et cela donne aussi l'avantage de ne plus avoir à déclarer la "déclaration complète du logger", mais c'est hors sujet)

Exemple:

Création du logger à 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 {
}

Utilisation de l'enregistreur dans votre code de production:

public class ProductionCode {
    @Inject
    @LoggerType
    private Logger logger;

    public void logSomething() {
        logger.info("something");
    }
}

Test de l’enregistreur dans votre code de test (en donnant un exemple easyMock):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

@Test
public void testTheLogger() {
   logger.info("something");
   replayAll();
   productionCode.logSomething();
}
0
GregD

L'API pour Log4J2 est légèrement différente. Vous pouvez aussi utiliser son appender async. 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
0
robbo