web-dev-qa-db-fra.com

Mocking Logger et LoggerFactory avec PowerMock et Mockito

J'ai l'enregistreur suivant que je veux simuler, mais pour valider les entrées du journal sont appelées, pas pour le contenu.

private static Logger logger = 
        LoggerFactory.getLogger(GoodbyeController.class);

Je veux me moquer de n'importe quelle classe qui est utilisée pour LoggerFactory.getLogger () mais je n'ai pas pu trouver comment faire cela .

@Before
public void performBeforeEachTest() {
    PowerMockito.mockStatic(LoggerFactory.class);
    when(LoggerFactory.getLogger(GoodbyeController.class)).
        thenReturn(loggerMock);

    when(loggerMock.isDebugEnabled()).thenReturn(true);
    doNothing().when(loggerMock).error(any(String.class));

    ...
}

J'aimerais savoir:

  1. Puis-je imiter le LoggerFactory.getLogger() statique pour qu'il fonctionne pour n'importe quelle classe?
  2. Il me semble que je ne peux exécuter que when(loggerMock.isDebugEnabled()).thenReturn(true); dans le @Before et qu’il me semble donc impossible de modifier les caractéristiques par méthode. Y a-t-il un moyen de contourner ceci?

Modifier les résultats:

Je pensais avoir déjà essayé et cela ne fonctionnait pas:

 when(LoggerFactory.getLogger(any(Class.class))).thenReturn(loggerMock);

Mais merci, cela a fonctionné.

Cependant, j'ai essayé d'innombrables variantes pour:

when(loggerMock.isDebugEnabled()).thenReturn(true);

Je ne peux pas demander à loggerMock de modifier son comportement en dehors de @Before, mais cela ne se produit qu'avec Coburtura. Avec Clover, la couverture est de 100%, mais il y a toujours un problème dans les deux cas.

J'ai ce cours simple:

public ExampleService{
    private static final Logger logger =
            LoggerFactory.getLogger(ExampleService.class);

    public String getMessage() {        
    if(logger.isDebugEnabled()){
        logger.debug("isDebugEnabled");
        logger.debug("isDebugEnabled");
    }
    return "Hello world!";
    }
    ...
}

Ensuite, j'ai ce test:

@RunWith(PowerMockRunner.class)
@PrepareForTest({LoggerFactory.class})
public class ExampleServiceTests {

    @Mock
    private Logger loggerMock;
    private ExampleServiceservice = new ExampleService();

    @Before
    public void performBeforeEachTest() {
        PowerMockito.mockStatic(LoggerFactory.class);
        when(LoggerFactory.getLogger(any(Class.class))).
            thenReturn(loggerMock);

        //PowerMockito.verifyStatic(); // fails
    }

    @Test
    public void testIsDebugEnabled_True() throws Exception {
        when(loggerMock.isDebugEnabled()).thenReturn(true);
        doNothing().when(loggerMock).debug(any(String.class));

        assertThat(service.getMessage(), is("Hello null: 0"));
        //verify(loggerMock, atLeast(1)).isDebugEnabled(); // fails
    }

    @Test
    public void testIsDebugEnabled_False() throws Exception {
        when(loggerMock.isDebugEnabled()).thenReturn(false);
        doNothing().when(loggerMock).debug(any(String.class));

        assertThat(service.getMessage(), is("Hello null: 0"));
        //verify(loggerMock, atLeast(1)).isDebugEnabled(); // fails
    }
}

Dans le trèfle, je montre une couverture à 100% du bloc if(logger.isDebugEnabled()){ . Mais si je tente de vérifier la loggerMock:

verify(loggerMock, atLeast(1)).isDebugEnabled();

Je n'ai aucune interaction ..__ J'ai également essayé PowerMockito.verifyStatic(); dans @Before mais cela a aussi zéro interactions.

Cela semble étrange que Cobertura montre que if(logger.isDebugEnabled()){ n'est pas complet à 100%, et Clover le fait, mais les deux conviennent que la vérification échoue.

47
Mick Knutson

@Mick, essayez aussi de préparer le propriétaire du champ statique, par exemple:

@PrepareForTest({GoodbyeController.class, LoggerFactory.class})

EDIT1: Je viens de créer un petit exemple. D'abord le contrôleur:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Controller {
    Logger logger = LoggerFactory.getLogger(Controller.class);

    public void log() { logger.warn("yup"); }
}

Puis le test:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Controller.class, LoggerFactory.class})
public class ControllerTest {

    @Test
    public void name() throws Exception {
        mockStatic(LoggerFactory.class);
        Logger logger = mock(Logger.class);
        when(LoggerFactory.getLogger(any(Class.class))).thenReturn(logger);

        new Controller().log();

        verify(logger).warn(anyString());
    }
}

Notez les importations! Des bibliothèques remarquables dans le chemin de classe: Mockito, PowerMock, JUnit, logback-core, logback-clasic, slf4j


EDIT2: Comme cela semble être une question populaire, je voudrais souligner que si ces messages de journalisation sont aussi importants et doivent être testés, c’est-à-dire qu’ils font partie du système avec les fonctionnalités/métier puis en introduisant une dépendance réelle qui fait clairement apparaître ces fonctions comme journal serait bien mieux dans la conception du système entier, au lieu de s’appuyer sur le code statique d’un standard et les classes techniques d’un enregistreur.

Pour cette question, je recommanderais de créer quelque chose comme = une classe Reporter avec des méthodes telles que reportIncorrectUseOfYAndZForActionX ou reportProgressStartedForActionX. Cela aurait l'avantage de rendre la fonctionnalité visible pour toute personne lisant le code. Mais cela aidera aussi à réaliser des tests, à changer les détails d'implémentation de cette fonctionnalité particulière.

Par conséquent, vous n’auriez pas besoin d’outils moqueurs statiques comme PowerMock. À mon avis, le code statique peut convenir, mais dès que le test demande de vérifier ou de simuler un comportement statique, il est nécessaire de refactoriser et d'introduire des dépendances claires.

58
Brice

Un peu tard pour le parti - je faisais quelque chose de similaire et j'avais besoin de conseils et je me suis retrouvé ici. Ne pas prendre de crédit - j'ai pris tout le code de Brice mais j'ai obtenu le "zéro interactions" de Cengiz. 

En me guidant dans les propos de jheriks présentés par AMD Joseph Lust, je pense que je sais pourquoi. J'ai fait tester mon objet en tant que champ et je l’ai rajeuni dans un @Avant contrairement à Brice. Alors l'enregistreur réel n'était pas la maquette, mais une vraie classe initiée comme suggéré par jhriks ... 

Je le ferais normalement pour mon objet testé afin d'obtenir un nouvel objet pour chaque test. Lorsque j'ai déplacé le champ dans une section locale et que je l'ai modifié lors du test, tout s'est bien déroulé. Cependant, si j'ai essayé un deuxième test, ce n'était pas la simulation de mon test, mais celle du premier test et j'ai à nouveau obtenu une interaction zéro. 

Lorsque je mets la création de la maquette dans @BeforeClass, l'enregistreur de l'objet à tester est toujours la maquette, mais reportez-vous à la note ci-dessous pour connaître les problèmes que cela entraîne ...

Classe sous test

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyClassWithSomeLogging  {

    private static final Logger LOG = LoggerFactory.getLogger(MyClassWithSomeLogging.class);

    public void doStuff(boolean b) {
        if(b) {
            LOG.info("true");
        } else {
            LOG.info("false");
        }

    }
}

Test

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.mockito.Mockito.*;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.*;
import static org.powermock.api.mockito.PowerMockito.when;


@RunWith(PowerMockRunner.class)
@PrepareForTest({LoggerFactory.class})
public class MyClassWithSomeLoggingTest {

    private static Logger mockLOG;

    @BeforeClass
    public static void setup() {
        mockStatic(LoggerFactory.class);
        mockLOG = mock(Logger.class);
        when(LoggerFactory.getLogger(any(Class.class))).thenReturn(mockLOG);
    }

    @Test
    public void testIt() {
        MyClassWithSomeLogging myClassWithSomeLogging = new MyClassWithSomeLogging();
        myClassWithSomeLogging.doStuff(true);

        verify(mockLOG, times(1)).info("true");
    }

    @Test
    public void testIt2() {
        MyClassWithSomeLogging myClassWithSomeLogging = new MyClassWithSomeLogging();
        myClassWithSomeLogging.doStuff(false);

        verify(mockLOG, times(1)).info("false");
    }

    @AfterClass
    public static void verifyStatic() {
        verify(mockLOG, times(1)).info("true");
        verify(mockLOG, times(1)).info("false");
        verify(mockLOG, times(2)).info(anyString());
    }
}

Remarque

Si vous avez deux tests avec la même attente, je devais faire la vérification dans @AfterClass car les invocations sur la statique sont empilées - verify(mockLOG, times(2)).info("true"); - plutôt que des temps (1) dans chaque test car le second test échouait en indiquant là où 2 invocation de cela. C'est joli pantalon mais je ne pouvais pas trouver un moyen pour effacer les invocations. J'aimerais savoir si quelqu'un peut penser à un moyen de contourner cela ...

12
user1276925

En réponse à votre première question, cela devrait être aussi simple que de remplacer:

   when(LoggerFactory.getLogger(GoodbyeController.class)).thenReturn(loggerMock);

avec

   when(LoggerFactory.getLogger(any(Class.class))).thenReturn(loggerMock);

En ce qui concerne votre deuxième question (et peut-être le comportement déroutant avec la première), je pense que le problème est que l'enregistreur est statique. Alors, 

private static Logger logger = LoggerFactory.getLogger(GoodbyeController.class);

est exécuté lorsque le class est initialisé, pas quand le object est instancié. Parfois, cela peut se faire à peu près au même moment, alors tout ira bien, mais il est difficile de le garantir. Donc, vous configurez LoggerFactory.getLogger pour qu'il retourne votre modèle, mais la variable de journalisation a peut-être déjà été définie avec un véritable objet Logger au moment de la configuration de vos modèles.

Vous pourrez peut-être définir explicitement le consignateur en utilisant quelque chose comme ReflectionTestUtils (je ne sais pas si cela fonctionne avec des champs statiques) ou le changer d’un champ statique à un champ d’instance. Quoi qu'il en soit, vous n'avez pas besoin de vous moquer de LoggerFactory.getLogger, car vous allez directement injecter l'instance fictive de Logger.

5
jhericks

Je pense que vous pouvez réinitialiser les invocations en utilisant Mockito.reset (mockLog). Vous devriez appeler cela avant chaque test, alors @Avant serait un bon endroit.

2
Markus Wendl

Utilisez une injection explicite . Aucune autre approche ne vous permettra, par exemple, d’exécuter des tests en parallèle dans la même machine virtuelle.

Les modèles qui utilisent tout ce que le chargeur de classe a de large, comme le classeur de journal statique ou qui dérangent l'environnement, pensent comme le journal de bord.XML sont tout ce qu'il y a de plus en matière de test.

Considérez les tests parallélisés que je mentionne ou envisagez le cas où vous souhaitez intercepter la journalisation du composant A dont la construction est cachée derrière l'api B. Ce dernier cas est facile à traiter si vous utilisez un enregistreur de dépendance injecté par le haut, mais pas Si vous injectez Logger car il n'y a pas de couture dans cette assemblée à ILoggerFactory.getLogger.

Et ce n'est pas tout sur les tests unitaires non plus. Parfois, nous souhaitons que les tests d'intégration émettent une journalisation. Parfois nous ne le faisons pas. Quelqu'un souhaite que certaines traces de la journalisation des tests d'intégration soient supprimées de manière sélective, par exemple pour les erreurs attendues qui autrement encombreraient la console CI et semeraient la confusion. Tout est facile si vous injectez ILoggerFactory à partir du haut de votre ligne principale (ou quel que soit le framework que vous pourriez utiliser)

Alors...

Injectez un rapporteur comme suggéré ou adoptez un schéma d’injection d’ILoggerFactory. Par injection ILoggerFactory explicite plutôt que par Logger, vous pouvez prendre en charge de nombreux modèles d’accès/interception et de parallélisation. 

0
johnlon