243 votes

Comment faire une assertion JUnit sur un message dans un journalisateur

J'ai du code en cours de test qui fait appel à un journal Java pour rapporter son état. Dans le code de test JUnit, je voudrais vérifier que l'entrée de journal correcte a été faite dans ce journal. Quelque chose le long des lignes suivantes :

methodUnderTest(bool x){
    if(x)
        logger.info("x est arrivé")
}

@Test tester(){
    // peut-être configurer d'abord un journal.
    methodUnderTest(true);
    assertXXXXXX(niveauJournalisé(),Level.INFO);
}

Je suppose que cela pourrait être fait avec un journal spécialement adapté (ou un gestionnaire, ou un formateur), 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 accéder à l'enregistrement de journal à partir d'un journal, mais je suppose que c'est possible.)

151voto

Ronald Blaschke Points 1822

J'ai eu besoin de cela plusieurs fois également. J'ai mis en place un petit exemple ci-dessous, que vous voudriez ajuster à vos besoins. Fondamentalement, vous créez votre propre Appender et vous l'ajoutez au journal que vous souhaitez. Si vous souhaitez collecter tout, le journal racine est un bon endroit pour commencer, mais vous pouvez utiliser un endroit plus spécifique si vous le souhaitez. N'oubliez pas de supprimer l'Appender lorsque vous avez fini, 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 collecte tout dans une List en mémoire. Si vous enregistrez 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 (Astuce: LoggingEvent est Serializable, donc vous devriez pouvoir simplement sérialiser les objets d'événements, si votre message de journal l'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 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 log = new ArrayList();

    @Override
    public boolean requiresLayout() {
        return false;
    }

    @Override
    protected void append(final LoggingEvent loggingEvent) {
        log.add(loggingEvent);
    }

    @Override
    public void close() {
    }

    public List getLog() {
        return new ArrayList(log);
    }
}

4 votes

Cela fonctionne très bien. La seule amélioration que je ferais serait de appeler logger.getAllAppenders(), puis de parcourir et d'appeler appender.setThreshold(Level.OFF) sur chacun (et de les réinitialiser quand vous avez fini !). Cela garantit que les messages "mauvais" que vous essayez de générer n'apparaissent pas dans les journaux de test et ne perturbent pas le développeur suivant.

1 votes

Dans Log4j 2.x, c'est légèrement plus complexe car vous devez créer un plugin, regardez ceci: stackoverflow.com/questions/24205093/…

1 votes

Merci pour cela. Mais si vous utilisez LogBack, vous pouvez utiliser ListAppender au lieu de créer votre propre appender personnalisé.

41voto

Jon Points 241

Merci beaucoup pour ces réponses (surprenamment) rapides et utiles; elles 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/facades de journalisation. Mais en me basant sur ces suggestions, j'ai "bidouillé" une extension de j.u.l.handler et ça marche à merveille.

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

Évidemment, vous pouvez stocker autant que vous le souhaitez de LogRecord ou les pousser tous dans une pile jusqu'à ce qu'il y ait un dépassement.

Dans la préparation du test junit, vous créez un java.util.logging.Logger et ajoutez un tel nouveau LogHandler à celui-ci:

@Test tester() {
    Logger logger = Logger.getLogger("mon journal de test junit");
    LogHandler handler = new LogHandler();
    handler.setLevel(Level.ALL);
    logger.setUseParentHandlers(false);
    logger.addHandler(handler);
    logger.setLevel(Level.ALL);

L'appel à setUseParentHandlers() est pour silencer les gestionnaires normaux, de sorte que (pour cette exécution de test junit) aucune journalisation inutile ne se produise. Faites ce que votre code sous test doit faire avec ce journal, exécutez le test et assertEquality:

    libraryUnderTest.setLogger(logger);
    methodUnderTest(true);  // voir question originale.
    assertEquals("Niveau de journalisation comme prévu?", Level.INFO, handler.checkLevel() );
}

(Bien sûr, vous déplaceriez une grande partie de ce travail dans une méthode @Before et apporteriez diverses autres améliorations, mais cela encombrerait cette présentation.)

21voto

djna Points 34761

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

logger.info()

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

C'est un compromis intéressant : combien de travail représente la simulation de cette classe dépendante ? Combien cela coûte-t-il de mettre en place un harnais de test incluant la classe dépendante et de vérifier les résultats de la dépendance ? En général, si mon "Unité" est hautement algorithmique, je préfère investir du temps pour l'isoler en utilisant des simulations.

8 votes

Comment se moquer d'un champ privé statique final, comme la plupart des enregistreurs sont définis ? Powermockito ? Amusez-vous..

0 votes

Stefano : Ce champ final a été initialisé d'une manière ou d'une autre, j'ai vu diverses approches pour injecter des Mocks plutôt que la vraie chose. Cela nécessite probablement un certain niveau de conception pour la testabilité en premier lieu. blog.codecentric.de/en/2011/11/…

0 votes

Comme Mehdi l'a dit, il est possible qu'en utilisant un gestionnaire approprié cela puisse suffire.

11voto

Bozho Points 273663

Se moquer est une option ici, bien que cela serait difficile, car les enregistreurs sont généralement privés statiques finaux - donc définir un enregistreur fictif ne serait pas un jeu d'enfant, ou nécessiterait une modification de la classe testée.

Vous pouvez créer un Appender personnalisé (ou quel que soit son nom), et l'enregistrer - soit via un fichier de configuration uniquement pour les tests, soit en temps d'exécution (d'une manière dépendante du framework de journalisation). Ensuite, vous pouvez obtenir cet appender (soit statiquement, s'il est déclaré dans le fichier de configuration, soit par sa référence actuelle, si vous le branchez en temps d'exécution), et vérifier son contenu.

5voto

Arne Points 6797

Comme mentionné par les autres, vous pourriez utiliser un cadre de simulation. Pour que cela fonctionne, vous devez exposer le journaliseur dans votre classe (bien que je préférerais probablement le rendre privé au niveau du package au lieu de créer un setter public).

L'autre solution consiste à créer manuellement un faux journaliseur. Vous devez écrire le faux journaliseur (plus de code de fixture) mais dans ce cas, je préférerais la lisibilité améliorée des tests par rapport au code enregistré du cadre de simulation.

Je ferais quelque chose comme ceci:

class FakeLogger implements ILogger {
    public List infos = new ArrayList();
    public List errors = new ArrayList();

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X