Verspotten von Logger und LoggerFactory mit PowerMock und Mockito

Lesezeit: 12 Minuten

Benutzer-Avatar
Mich Knutson

Ich habe den folgenden Logger, den ich verspotten möchte, aber um zu validieren, werden Log-Einträge aufgerufen, nicht für den Inhalt.

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

Ich möchte JEDE Klasse verspotten, die für LoggerFactory.getLogger() verwendet wird, aber ich konnte nicht herausfinden, wie das geht. Damit bin ich bisher gelandet:

@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));

    ...
}

Ich würde gerne wissen:

  1. Kann ich die Statik verspotten? LoggerFactory.getLogger() für jede Klasse arbeiten?
  2. Ich kann nur scheinbar rennen when(loggerMock.isDebugEnabled()).thenReturn(true); in dem @Before und daher kann ich die Eigenschaften anscheinend nicht pro Methode ändern. Gibt es eine Möglichkeit, dies zu umgehen?

Ergebnisse bearbeiten:

Ich dachte, ich hätte das schon versucht und es hat nicht funktioniert:

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

Aber danke, denn es hat funktioniert.

Ich habe jedoch unzählige Variationen ausprobiert, um:

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

Ich kann den LoggerMock nicht dazu bringen, sein Verhalten außerhalb zu ändern @Before aber das passiert nur mit Coburtura. Bei Clover zeigt die Abdeckung 100 % an, aber es gibt immer noch ein Problem.

Ich habe diese einfache Klasse:

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!";
    }
    ...
}

Dann habe ich diesen 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
    }
}

In Klee zeige ich 100% Abdeckung der if(logger.isDebugEnabled()){ Block. Aber wenn ich versuche, das zu überprüfen loggerMock:

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

Ich bekomme null Interaktionen. Ich habe es auch versucht PowerMockito.verifyStatic(); in @Before aber das hat auch null Wechselwirkungen.

Das scheint nur seltsam, dass Cobertura das zeigt if(logger.isDebugEnabled()){ als nicht 100 % vollständig und Clover auch, aber beide stimmen darin überein, dass die Verifizierung fehlschlägt.

  • Haben Sie die @MockPolicy ausprobiert? Beispiele hier sind für Mocks im EasyMock-Stil, können aber für Mockito angepasst werden.

    – Matt Lachmann

    27. Juni 2013 um 16:39 Uhr


Benutzer-Avatar
Brice

EDIT 2020-09-21: Seit 3.4.0 unterstützt Mockito mocking statische Methoden, API befindet sich noch in der Inkubation und wird sich wahrscheinlich ändern, insbesondere in Bezug auf Stubbing und Verifizierung. Es erfordert die mockito-inline Artefakt. Und Sie müssen den Test nicht vorbereiten oder einen bestimmten Läufer verwenden. Alles, was Sie tun müssen, ist:

@Test
public void name() {
    try (MockedStatic<LoggerFactory> integerMock = mockStatic(LoggerFactory.class)) {
        final Logger logger = mock(Logger.class);
        integerMock.when(() -> LoggerFactory.getLogger(any(Class.class))).thenReturn(logger);
        new Controller().log();
        verify(logger).warn(any());
    }
}

Der zwei wichtige Aspekt in diesem Code ist, dass Sie den Gültigkeitsbereich festlegen müssen, wenn der statische Mock gilt, dh innerhalb dieses Try-Blocks. Und Sie müssen die Stubbing- und Verifizierungs-API von aufrufen MockedStatic Objekt.


@Mick, versuche auch den Besitzer des statischen Feldes vorzubereiten, zB:

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

EDIT1: Ich habe gerade ein kleines Beispiel erstellt. Zuerst die Steuerung:

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

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

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

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

Beachten Sie die Importe! Bemerkenswerte Bibliotheken im Klassenpfad: Mockito, PowerMock, JUnit, logback-core, logback-classic, slf4j


EDIT2: Da es eine beliebte Frage zu sein scheint, möchte ich darauf hinweisen ob diese Protokollmeldungen so wichtig sind und müssen getestet werden, dh sie sind Feature / Business Part des Systems Dann wäre die Einführung einer echten Abhängigkeit, die deutlich macht, dass diese Protokolle Funktionen sind, im gesamten Systemdesign so viel besseranstatt sich auf statischen Code eines Standards und technische Klassen eines Loggers zu verlassen.

Für diese Angelegenheit würde ich empfehlen, etwas wie = a zu basteln Reporter Klasse mit Methoden wie z reportIncorrectUseOfYAndZForActionX oder reportProgressStartedForActionX. Dies hätte den Vorteil, dass die Funktion für jeden sichtbar wäre, der den Code liest. Aber es wird auch helfen, Tests durchzuführen und die Implementierungsdetails dieser bestimmten Funktion zu ändern.

Daher benötigen Sie keine statischen Mocking-Tools wie PowerMock. Meiner Meinung nach kann statischer Code in Ordnung sein, aber sobald der Test verlangt, statisches Verhalten zu verifizieren oder zu simulieren, ist es notwendig, umzugestalten und klare Abhängigkeiten einzuführen.

  • HINWEIS: Wenn Sie dort einen zweiten @-Test versucht haben, haben Sie ein Problem. verify() funktioniert nicht, wenn es in einem zusätzlichen Test erneut aufgerufen wird. Nicht, wenn Sie @ Before oder neue Variablennamen verwenden. Der classLoader wird nur eine davon erstellen, also können Sie nicht zwei verschiedene statische Klassen haben. Teilen Sie Ihre Tests in einzelne Klassen auf.

    – Josef Lust

    22. Februar 2013 um 20:45 Uhr

  • Leider bekomme ich es nicht zum Laufen… Mockito sagt… Eigentlich gab es null Interaktionen mit diesem Mock.

    – Cengiz

    18. März 2013 um 12:53 Uhr

  • Funktioniert gut für mich (einschließlich Überprüfung). Stellen Sie sicher, dass Sie @PrepareForTest verwenden, und stellen Sie außerdem sicher, dass Sie die richtige Protokollsuchmethode der Protokollfabrik simuliert haben.

    – Lo Tan

    22. Mai 2013 um 17:35 Uhr


  • @Brice Ich weiß, dass dies eine ziemlich alte Frage ist, aber ich möchte Sie etwas zu Ihrem Rat fragen, “eine echte Abhängigkeit einzuführen” und die Notwendigkeit statischer Spottwerkzeuge zu vermeiden. Ich stimme Ihrem Punkt, das Feature für jedermann sichtbar zu machen, voll und ganz zu, aber ich denke, wenn Sie diese Berichtsklasse auch auf Komponenten testen müssen, müssen Sie immer noch diese statischen Mocking-Tools verwenden. Willst du nicht?

    – beni0888

    22. Mai 2018 um 10:43 Uhr

  • @beni0888 Es wird zwar empfohlen, diese Klasse zu testen, aber die konkrete Abhängigkeit kann mit a gestaltet werden Logger -Feld in den Konstruktoren festgelegt, und als solches wäre es möglich, ein reines Mockito zu übergeben Logger mock, in einem Konstruktor, der nur zu Testzwecken sichtbar ist.

    – Brice

    23. Mai 2018 um 19:26 Uhr

Etwas spät zur Party – ich habe etwas Ähnliches gemacht und brauchte ein paar Hinweise und bin hier gelandet. Keine Anerkennung – ich habe den gesamten Code von Brice übernommen, aber die “Null-Interaktionen” als Cengiz erhalten.

Anhand von Jheriks und Joseph Lusts Anleitung weiß ich, warum – ich hatte mein Objekt als Feld getestet und es im Gegensatz zu Brice in einem @Before neu erstellt. Dann war der eigentliche Logger nicht der Schein, sondern eine echte Klasse, wie Jhriks vorgeschlagen hatte …

Normalerweise würde ich dies für mein zu testendes Objekt tun, um für jeden Test ein neues Objekt zu erhalten. Als ich das Feld in ein lokales verschoben und im Test neu angelegt habe, lief es ok. Als ich jedoch einen zweiten Test versuchte, war es nicht der Mock in meinem Test, sondern der Mock aus dem ersten Test, und ich bekam wieder null Interaktionen.

Wenn ich die Erstellung des Mocks in @BeforeClass einfüge, ist der Logger im zu testenden Objekt immer der Mock, aber siehe den Hinweis unten für die Probleme damit …

Klasse im 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");
        }

    }
}

Prüfen

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

Notiz

Wenn Sie zwei Tests mit der gleichen Erwartung haben, musste ich die Überprüfung in der @AfterClass durchführen, da die Aufrufe auf der Statik gestapelt sind – verify(mockLOG, times(2)).info("true"); – Anstelle von Times(1) in jedem Test, da der zweite Test fehlschlagen würde und sagen würde, wo 2 Aufrufe davon sind. Das ist eine hübsche Hose, aber ich konnte keinen Weg finden, die Aufrufe zu löschen. Ich würde gerne wissen, ob jemand eine Lösung dafür findet….

  • Der Vorschlag von Markus Wendl, Reset zu verwenden, hat bei mir funktioniert.

    – Ryan Pfister

    16. November 2015 um 19:06 Uhr

  • +500. @BeforeClass hat für mich den Unterschied gemacht.

    – Jameson

    9. Juni 2018 um 4:25 Uhr

Benutzer-Avatar
jhericks

Um Ihre erste Frage zu beantworten, sollte es so einfach sein wie das Ersetzen:

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

mit

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

In Bezug auf Ihre zweite Frage (und möglicherweise das rätselhafte Verhalten bei der ersten) denke ich, dass das Problem darin besteht, dass der Logger statisch ist. So,

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

ausgeführt wird, wenn die Klasse initialisiert wird, nicht das wann die Objekt wird instanziiert. Manchmal kann dies ungefähr zur gleichen Zeit sein, also wird es Ihnen gut gehen, aber das ist schwer zu garantieren. Sie richten also LoggerFactory.getLogger ein, um Ihren Mock zurückzugeben, aber die Logger-Variable wurde möglicherweise bereits mit einem echten Logger-Objekt festgelegt, wenn Ihre Mocks eingerichtet sind.

Möglicherweise können Sie den Logger explizit mit etwas wie festlegen ReflectionTestUtils (Ich weiß nicht, ob das mit statischen Feldern funktioniert) oder es von einem statischen Feld in ein Instanzfeld ändern. In beiden Fällen müssen Sie LoggerFactory.getLogger nicht nachahmen, da Sie die nachgebildete Logger-Instanz direkt injizieren.

Ich denke, Sie können die Aufrufe mit Mockito.reset (mockLog) zurücksetzen. Sie sollten dies vor jedem Test aufrufen, also wäre innerhalb von @Before ein guter Ort.

class SomeService{
  private static final Logger logger = LogManager.getLogger(SomeService.class);

  public void callSomeMethod(){
   ...
   ...
   if (logger.isDebugEnabled()) {
        logger.debug(String.format("some message ....."));
    }
  }

}

JUNIT Testabdeckungen –

import java.io.IOException;
 
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
 
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
 

@RunWith(MockitoJUnitRunner.class)
public class SomeServiceTest {
  @Mock
  private Appender mockAppender;
 
  private Logger logger;
 
  @Before
  public void setup() {
    when(mockAppender.getName()).thenReturn("MockAppender");
    when(mockAppender.isStarted()).thenReturn(true);
    when(mockAppender.isStopped()).thenReturn(false);
 
    logger = (Logger)LogManager.getLogger(SomeService.class);
    logger.addAppender(mockAppender);
    logger.setLevel(Level.DEBUG);
  }
 
  
 
  @Test
  public void callSomeMethodTest() {
    //write test cases, logger.isDebugEnabled() will call.
  }
 
}

Benutzer-Avatar
Johnson

Verwenden Sie die explizite Injektion. Kein anderer Ansatz ermöglicht es Ihnen beispielsweise, Tests parallel in derselben JVM auszuführen.

Muster, die Classloader-weit alles verwenden, wie statischer Protokollbinder, oder mit Umgebungsdenken wie logback.XML herumspielen, sind beim Testen kaputt.

Betrachten Sie die von mir erwähnten parallelisierten Tests oder den Fall, in dem Sie die Protokollierung von Komponente A abfangen möchten, deren Konstruktion hinter API B verborgen ist. Dieser letztere Fall ist einfach zu handhaben, wenn Sie eine Loggerfactory mit Abhängigkeitsinjektion von oben verwenden, aber nicht wenn Sie Logger injizieren, da es in dieser Assembly bei ILoggerFactory.getLogger keine Naht gibt.

Und es geht auch nicht nur um Unit-Tests. Manchmal möchten wir, dass Integrationstests Protokolle ausgeben. Manchmal tun wir das nicht. Jemand möchte, dass ein Teil der Integrationstestprotokollierung selektiv unterdrückt wird, z. B. für erwartete Fehler, die sonst die CI-Konsole überladen und verwirren würden. Alles ganz einfach, wenn Sie ILoggerFactory von oben in Ihre Hauptleitung einfügen (oder was auch immer Sie für ein Framework verwenden).

So…

Injizieren Sie entweder einen Reporter wie vorgeschlagen oder übernehmen Sie ein Muster zum Injizieren der ILoggerFactory. Durch explizite ILoggerFactory-Injektion anstelle von Logger können Sie viele Zugriffs-/Abfangmuster und Parallelisierung unterstützen.

Benutzer-Avatar
Gayan Weerakutti

Das Folgende ist eine Testklasse, die den privaten statischen endgültigen Logger namens vortäuscht log im Unterricht LogUtil.

Neben dem Spott über die getLogger Factory-Aufruf, ist es notwendig, das Feld explizit per Reflektion, in zu setzen @BeforeClass

public class LogUtilTest {

    private static Logger logger;

    private static MockedStatic<LoggerFactory> loggerFactoryMockedStatic;

    /**
     * Since {@link LogUtil#log} being a static final variable it is only initialized once at the class load time
     * So assertions are also performed against the same mock {@link LogUtilTest#logger}
     */
    @BeforeClass
    public static void beforeClass() {
        logger = mock(Logger.class);
        loggerFactoryMockedStatic = mockStatic(LoggerFactory.class);
        loggerFactoryMockedStatic.when(() -> LoggerFactory.getLogger(anyString())).thenReturn(logger);
        Whitebox.setInternalState(LogUtil.class, "log", logger);
    }

    @AfterClass
    public static void after() {
        loggerFactoryMockedStatic.close();
    }
} 

  • Das macht keinen Sinn. Warum müssten Sie die statische Methode verspotten, wenn Sie deren Rückgabewert überschreiben?

    – T3rm1

    8. Oktober 2021 um 13:15 Uhr

1143490cookie-checkVerspotten von Logger und LoggerFactory mit PowerMock und Mockito

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy