So führen Sie eine JUnit-Bestätigung für eine Nachricht in einem Logger durch


205

Ich habe einen Code im Test, der einen Java-Logger aufruft, um seinen Status zu melden. Im JUnit-Testcode möchte ich überprüfen, ob in diesem Logger der richtige Protokolleintrag vorgenommen wurde. Etwas in der folgenden Richtung:

methodUnderTest(bool x){
    if(x)
        logger.info("x happened")
}

@Test tester(){
    // perhaps setup a logger first.
    methodUnderTest(true);
    assertXXXXXX(loggedLevel(),Level.INFO);
}

Ich nehme an, dass dies mit einem speziell angepassten Logger (oder Handler oder Formatierer) durchgeführt werden könnte, aber ich würde es vorziehen, eine bereits vorhandene Lösung wiederzuverwenden. (Und um ehrlich zu sein, ist mir nicht klar, wie ich von einem Logger zum logRecord komme, aber nehmen wir an, dass das möglich ist.)

Antworten:


142

Ich habe das auch mehrmals gebraucht. Ich habe unten ein kleines Beispiel zusammengestellt, das Sie an Ihre Bedürfnisse anpassen möchten. Grundsätzlich erstellen Sie Ihre eigenen Appenderund fügen sie dem gewünschten Logger hinzu. Wenn Sie alles sammeln möchten, ist der Root-Logger ein guter Ausgangspunkt, aber Sie können einen spezifischeren verwenden, wenn Sie möchten. Vergessen Sie nicht, den Appender zu entfernen, wenn Sie fertig sind. Andernfalls kann es zu einem Speicherverlust kommen. Unten habe ich es innerhalb des Tests gemacht, aber setUpoder @Beforeund tearDownoder oder @Afterbessere Orte, abhängig von Ihren Bedürfnissen.

Die folgende Implementierung sammelt auch alles in einem ListIn-Speicher. Wenn Sie viel protokollieren, können Sie einen Filter hinzufügen, um langweilige Einträge zu löschen oder das Protokoll in eine temporäre Datei auf der Festplatte zu schreiben (Hinweis: LoggingEventist Serializable, sodass Sie in der Lage sein sollten, die Ereignisobjekte nur zu serialisieren, wenn Ihre Protokollmeldung angezeigt wird ist.)

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

4
Das funktioniert super. Die einzige Verbesserung, die ich machen würde, ist anzurufen logger.getAllAppenders(), dann durchzugehen und appender.setThreshold(Level.OFF)jeden anzurufen (und sie zurückzusetzen, wenn Sie fertig sind!). Dies stellt sicher, dass die "schlechten" Nachrichten, die Sie generieren möchten, nicht in den Testprotokollen angezeigt werden und den nächsten Entwickler ausflippen.
Coderer

1
In Log4j 2.x ist etwas komplizierter, da Sie ein Plugin erstellen müssen. Schauen Sie sich das an: stackoverflow.com/questions/24205093/…
Paranza

1
Danke dafür. Wenn Sie jedoch LogBack verwenden, können Sie es verwenden, ListAppender<ILoggingEvent>anstatt einen eigenen benutzerdefinierten Appender zu erstellen.
Sinujohn

2
aber das funktioniert nicht für slf4j! Weißt du, wie ich es ändern kann, um auch damit zu arbeiten?
Shilan

2
@sd Wenn Sie das Loggerto org.apache.logging.log4j.core.Logger(die Implementierungsklasse für die Schnittstelle) umwandeln, erhalten Sie setAppender()/removeAppender()erneut Zugriff auf .
David Moles

59

Hier ist eine einfache und effiziente Logback-Lösung.
Es ist nicht erforderlich, eine neue Klasse hinzuzufügen / zu erstellen.
Es ListAppenderbasiert auf : einem Whitebox-Logback-Appender, in dem Protokolleinträge in ein public ListFeld eingefügt werden, mit dem wir unsere Aussagen treffen können.

Hier ist ein einfaches Beispiel.

Foo Klasse:

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-Klasse:

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
        // addAppender is outdated now
        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-Zusicherungen klingen nicht sehr angepasst, um bestimmte Eigenschaften der Listenelemente geltend zu machen.
Matcher / Assertion-Bibliotheken wie AssertJ oder Hamcrest erscheinen dafür besser:

Mit AssertJ wäre es:

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

Wie verhindern Sie, dass der Test fehlschlägt, wenn Sie einen Fehler protokollieren?
Ghilteras

@ Philteras Ich bin nicht sicher zu verstehen. Das Protokollieren eines Fehlers sollte Ihren Test nicht zum Scheitern bringen. Was erklärst du?
Davidxxx

Denken Sie auch daran, nicht mockdie Klasse zu testen, die getestet wird. Sie müssen es mit dem newOperator instanziieren
Dmytro Chasovskyi

35

Vielen Dank für diese (überraschend) schnellen und hilfreichen Antworten. Sie haben mich auf den richtigen Weg für meine Lösung gebracht.

Die Codebasis, in der ich dies verwenden möchte, verwendet java.util.logging als Protokollierungsmechanismus, und ich fühle mich in diesen Codes nicht zu Hause genug, um dies vollständig in log4j oder in Protokollierungsschnittstellen / -fassaden zu ändern. Aber basierend auf diesen Vorschlägen habe ich eine Julhandler-Erweiterung "gehackt", und das ist ein Vergnügen.

Es folgt eine kurze Zusammenfassung. Erweitern 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(){}
}

Natürlich können Sie so viel speichern, wie Sie möchten / wollen / brauchen LogRecord, oder sie alle in einen Stapel schieben, bis Sie einen Überlauf erhalten.

In Vorbereitung auf den Junit-Test erstellen Sie einen java.util.logging.Loggerund fügen einen solchen hinzu 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);

Der Aufruf von setUseParentHandlers()besteht darin, die normalen Handler zum Schweigen zu bringen, damit (für diesen Junit-Testlauf) keine unnötige Protokollierung erfolgt. Tun Sie alles, was Ihr zu testender Code benötigt, um diesen Logger zu verwenden, führen Sie den Test und assertEquality aus:

    libraryUnderTest.setLogger(logger);
    methodUnderTest(true);  // see original question.
    assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() );
}

(Natürlich würden Sie einen großen Teil dieser Arbeit in eine @BeforeMethode verschieben und verschiedene andere Verbesserungen vornehmen, aber das würde diese Präsentation überladen.)


16

Eine andere Möglichkeit besteht darin, Appender zu verspotten und zu überprüfen, ob eine Nachricht bei diesem Appender protokolliert wurde. Beispiel für Log4j 1.2.x und 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);
    }
}

16

Tatsächlich testen Sie einen Nebeneffekt einer abhängigen Klasse. Für Unit-Tests müssen Sie dies nur überprüfen

logger.info()

wurde mit dem richtigen Parameter aufgerufen. Verwenden Sie daher ein Mocking-Framework, um den Logger zu emulieren. Auf diese Weise können Sie das Verhalten Ihrer eigenen Klasse testen.


3
Wie haben Sie ein privates statisches Endfeld verspottet, für das die meisten Logger definiert sind? Powermockito? Viel Spaß ..
Stefano L

Stefano: Das letzte Feld wurde irgendwie initialisiert. Ich habe verschiedene Ansätze gesehen, um Mocks zu injizieren, anstatt die Realität. Erfordert wahrscheinlich in erster Linie ein gewisses Maß an Design für die Testbarkeit. blog.codecentric.de/de/2011/11/…
djna

Wie Mehdi sagte, kann die Verwendung eines geeigneten
Handlers

11

Das Verspotten ist hier eine Option, obwohl es schwierig wäre, da Logger im Allgemeinen ein privates statisches Finale sind. Das Einstellen eines Mock-Loggers wäre also kein Kinderspiel oder würde eine Änderung der zu testenden Klasse erfordern.

Sie können einen benutzerdefinierten Appender (oder wie auch immer er heißt) erstellen und registrieren - entweder über eine reine Testkonfigurationsdatei oder über die Laufzeit (in gewisser Weise abhängig vom Protokollierungsframework). Und dann können Sie diesen Appender abrufen (entweder statisch, wenn er in der Konfigurationsdatei deklariert ist, oder anhand seiner aktuellen Referenz, wenn Sie ihn zur Laufzeit einstecken) und seinen Inhalt überprüfen.


10

Inspiriert von der Lösung von @ RonaldBlaschke habe ich mir Folgendes ausgedacht:

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

}

... was Ihnen Folgendes ermöglicht:

@Rule public Log4JTester logTest = new Log4JTester();

@Test
public void testFoo() {
     user.setStatus(Status.PREMIUM);
     logTest.assertLogged(
        stringContains("Note added to account: premium customer"));
}

Sie könnten es wahrscheinlich dazu bringen, Hamcrest auf intelligentere Weise zu verwenden, aber ich habe es dabei belassen.


6

Für log4j2 ist die Lösung etwas anders, da AppenderSkeleton nicht mehr verfügbar ist. Darüber hinaus funktioniert die Verwendung von Mockito oder einer ähnlichen Bibliothek zum Erstellen eines Appenders mit einem ArgumentCaptor nicht, wenn Sie mehrere Protokollierungsnachrichten erwarten, da das MutableLogEvent über mehrere Protokollnachrichten hinweg wiederverwendet wird. Die beste Lösung, die ich für log4j2 gefunden habe, ist:

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

5

Wie von den anderen erwähnt, könnten Sie ein spöttisches Framework verwenden. Damit dies funktioniert, müssen Sie den Logger in Ihrer Klasse verfügbar machen (obwohl ich es vorziehen würde, das Paket privat zu machen, anstatt einen öffentlichen Setter zu erstellen).

Die andere Lösung besteht darin, einen gefälschten Logger von Hand zu erstellen. Sie müssen den gefälschten Logger schreiben (mehr Fixture Code), aber in diesem Fall würde ich die verbesserte Lesbarkeit der Tests gegenüber dem gespeicherten Code aus dem Mocking Framework bevorzugen.

Ich würde so etwas machen:

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

5

Beeindruckend. Ich bin mir nicht sicher, warum das so schwer war. Ich habe festgestellt, dass ich keines der oben genannten Codebeispiele verwenden konnte, da ich log4j2 über slf4j verwendet habe. Das ist meine Lösung:

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

4

Hier ist, was ich für die Rückmeldung getan habe.

Ich habe eine TestAppender-Klasse erstellt:

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

Dann habe ich im übergeordneten Element meiner Testeinheit für Testeinheiten eine Methode erstellt:

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

Ich habe eine logback-test.xml-Datei in src / test / resources definiert und einen Test-Appender hinzugefügt:

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

und fügte diesen Appender dem Stamm-Appender hinzu:

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

Jetzt kann ich in meinen Testklassen, die sich von meiner übergeordneten Testklasse erstrecken, den Appender abrufen und die letzte Nachricht protokollieren sowie die Nachricht, die Ebene und das Wurfobjekt überprüfen.

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

Ich sehe nicht, wo die getAppender-Methode definiert ist?!?
Bioinfornatics

getAppender ist eine Methode auf einem ch.qos.logback.classic.Logger
kfox

4

Für Junit 5 (Jupiter) Spring ist die OutputCaptureExtension von Spring sehr nützlich. Es ist seit Spring Boot 2.2 verfügbar und im Spring-Boot-Test- Artefakt verfügbar .

Beispiel (aus Javadoc):

@ExtendWith(OutputCaptureExtension.class)
class MyTest {
    @Test
    void test(CapturedOutput output) {
        System.out.println("ok");
        assertThat(output).contains("ok");
        System.err.println("error");
    }

    @AfterEach
    void after(CapturedOutput output) {
        assertThat(output.getOut()).contains("ok");
        assertThat(output.getErr()).contains("error");
    }
}

Ich glaube, log Anweisungen sind anders als getOut()oder getErr().
Ram

Dies ist die Antwort, nach der ich gesucht habe (obwohl die Frage nicht mit dem Frühlingsstiefel zusammenhängt)!
Helleye

3

Was mich betrifft, können Sie Ihren Test vereinfachen, indem Sie JUnitmit verwenden Mockito. Ich schlage folgende Lösung vor:

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

Deshalb haben wir eine gute Flexibilität für Tests mit unterschiedlicher Nachrichtenmenge


1
Um nicht fast dieselben Codeblöcke zu wiederholen, möchte ich hinzufügen, dass fast 1to1 für mich für Log4j2 funktioniert. Nur Importe "org.apache.logging.log4j.core", Guss Logger "org.apache.logging.log4j.core.Logger", fügen Veränderung when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); und Veränderung Logging -> LogEvent
Aliaksei Yatsau

3
Here is the sample code to mock log, irrespective of the version used for junit or sping, springboot.

import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
import org.mockito.ArgumentMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.Test;

import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

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

    @Test
    public void testSomething() {
    ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
    final Appender mockAppender = mock(Appender.class);
    when(mockAppender.getName()).thenReturn("MOCK");
    root.addAppender(mockAppender);

    //... do whatever you need to trigger the log

    verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
      @Override
      public boolean matches(final Object argument) {
        return ((LoggingEvent)argument).getFormattedMessage().contains("Hey this is the message I want to see");
      }
    }));
  }
}

1
Das hat bei mir funktioniert. Zeile 'when (mockAppender.getName ()). ThenReturn ("MOCK")' wurde für mich nicht benötigt.
Mayank Raghav

1

Die API für Log4J2 unterscheidet sich geringfügig. Möglicherweise verwenden Sie auch den asynchronen Appender. Ich habe dafür einen gesperrten Appender erstellt:

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

Verwenden Sie es so:

        try (LatchedAppender appender = new LatchedAppender(ClassUnderTest.class, 1)) {

        ClassUnderTest.methodThatLogs();
        List<LogEvent> events = appender.awaitMessages();
        assertEquals(1, events.size());
        //more assertions here

    }//appender removed

1

Beachten Sie, dass in Log4J 2.x die öffentliche Schnittstelle org.apache.logging.log4j.Loggerdie Methoden setAppender()und nicht enthält removeAppender().

Wenn Sie jedoch nichts Besonderes tun, sollten Sie es in die Implementierungsklasse umwandeln können org.apache.logging.log4j.core.Logger, die diese Methoden verfügbar macht.

Hier ist ein Beispiel mit Mockito und AssertJ :

// Import the implementation class rather than the API interface
import org.apache.logging.log4j.core.Logger;
// Cast logger to implementation class to get access to setAppender/removeAppender
Logger log = (Logger) LogManager.getLogger(MyClassUnderTest.class);

// Set up the mock appender, stubbing some methods Log4J needs internally
Appender appender = mock(Appender.class);
when(appender.getName()).thenReturn("Mock Appender");
when(appender.isStarted()).thenReturn(true);

log.addAppender(appender);
try {
    new MyClassUnderTest().doSomethingThatShouldLogAnError();
} finally {
    log.removeAppender(appender);
}

// Verify that we got an error with the expected message
ArgumentCaptor<LogEvent> logEventCaptor = ArgumentCaptor.forClass(LogEvent.class);
verify(appender).append(logEventCaptor.capture());
LogEvent logEvent = logEventCaptor.getValue();
assertThat(logEvent.getLevel()).isEqualTo(Level.ERROR);
assertThat(logEvent.getMessage().getFormattedMessage()).contains(expectedErrorMessage);

0

Eine andere erwähnenswerte Idee, obwohl es sich um ein älteres Thema handelt, ist die Erstellung eines CDI-Produzenten, der Ihren Logger injiziert, damit das Verspotten einfach wird. (Und es bietet auch den Vorteil, dass nicht mehr die "gesamte Logger-Anweisung" deklariert werden muss, aber das ist kein Thema.)

Beispiel:

Erstellen des zu injizierenden Loggers:

public class CdiResources {
  @Produces @LoggerType
  public Logger createLogger(final InjectionPoint ip) {
      return Logger.getLogger(ip.getMember().getDeclaringClass());
  }
}

Das Qualifikationsspiel:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface LoggerType {
}

Verwenden des Loggers in Ihrem Produktionscode:

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

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

Testen des Loggers in Ihrem Testcode (anhand eines easyMock-Beispiels):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

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

0

Mit Jmockit (1.21) konnte ich diesen einfachen Test schreiben. Der Test stellt sicher, dass eine bestimmte ERROR-Nachricht nur einmal aufgerufen wird.

@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

Das Verspotten des Appenders kann dabei helfen, die Protokollzeilen zu erfassen. Ein Beispiel finden Sie unter: 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

Verwenden Sie den folgenden Code. Ich verwende denselben Code für meinen Federintegrationstest, bei dem ich die Protokollierung für die Protokollierung verwende. Verwenden Sie die Methode assertJobIsScheduled, um den im Protokoll gedruckten Text zu bestätigen.

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

Es gibt zwei Dinge, die Sie möglicherweise testen möchten.

  • Wenn für den Bediener meines Programms ein Ereignis von Interesse ist, führt mein Programm einen geeigneten Protokollierungsvorgang durch, der den Bediener über dieses Ereignis informieren kann.
  • Wenn mein Programm einen Protokollierungsvorgang ausführt, hat die von ihm erzeugte Protokollmeldung den richtigen Text.

Diese beiden Dinge sind tatsächlich verschiedene Dinge und könnten daher separat getestet werden. Das Testen des zweiten (des Textes von Nachrichten) ist jedoch so problematisch, dass ich davon abraten würde, es überhaupt zu tun. Ein Test eines Nachrichtentextes besteht letztendlich darin, zu überprüfen, ob eine Textzeichenfolge (der erwartete Nachrichtentext) mit der in Ihrem Protokollierungscode verwendeten Textzeichenfolge identisch ist oder trivial daraus abgeleitet werden kann.

  • Diese Tests testen überhaupt keine Programmlogik, sondern nur, dass eine Ressource (eine Zeichenfolge) einer anderen Ressource entspricht.
  • Die Tests sind fragil; Selbst eine geringfügige Änderung der Formatierung einer Protokollnachricht unterbricht Ihre Tests.
  • Die Tests sind nicht mit der Internationalisierung (Übersetzung) Ihrer Protokollierungsschnittstelle kompatibel. Bei den Tests wird davon ausgegangen, dass nur ein möglicher Nachrichtentext und damit nur eine mögliche menschliche Sprache vorhanden ist.

Beachten Sie, dass Ihr Programmcode (der möglicherweise eine Geschäftslogik implementiert) direkt die Textprotokollierungsschnittstelle aufruft, ein schlechtes Design ist (aber leider sehr häufig). Code, der für die Geschäftslogik verantwortlich ist, entscheidet auch über einige Protokollierungsrichtlinien und den Text von Protokollnachrichten. Es mischt Geschäftslogik mit Benutzeroberflächencode (ja, Protokollmeldungen sind Teil der Benutzeroberfläche Ihres Programms). Diese Dinge sollten getrennt sein.

Ich empfehle daher, dass die Geschäftslogik den Text von Protokollnachrichten nicht direkt generiert. Lassen Sie es stattdessen an ein Protokollierungsobjekt delegieren.

  • Die Klasse des Protokollierungsobjekts sollte eine geeignete interne API bereitstellen, mit der Ihr Geschäftsobjekt das Ereignis ausdrücken kann, das mithilfe von Objekten Ihres Domänenmodells und nicht mithilfe von Textzeichenfolgen aufgetreten ist.
  • Die Implementierung Ihrer Protokollierungsklasse ist dafür verantwortlich, Textdarstellungen dieser Domänenobjekte zu erstellen, eine geeignete Textbeschreibung des Ereignisses zu rendern und diese Textnachricht dann an das Protokollierungsframework auf niedriger Ebene (wie JUL, log4j oder slf4j) weiterzuleiten.
  • Ihre Geschäftslogik ist nur dafür verantwortlich, die richtigen Methoden der internen API Ihrer Logger-Klasse aufzurufen und die richtigen Domänenobjekte zu übergeben, um die tatsächlich aufgetretenen Ereignisse zu beschreiben.
  • Ihre konkrete Protokollierungsklasse implementsan interface, die die interne API beschreibt, die Ihre Geschäftslogik möglicherweise verwendet.
  • Ihre Klasse (n), die Geschäftslogik implementiert und Protokollierung durchführen muss, hat einen Verweis auf das Protokollierungsobjekt, an das delegiert werden soll. Die Klasse der Referenz ist die Zusammenfassung interface.
  • Verwenden Sie die Abhängigkeitsinjektion, um den Verweis auf den Logger einzurichten.

Sie können dann testen, ob Ihre Geschäftslogikklassen die Protokollierungsschnittstelle korrekt über Ereignisse informieren, indem Sie einen Scheinprotokollierer erstellen, der die interne Protokollierungs-API implementiert, und in der Einrichtungsphase Ihres Tests die Abhängigkeitsinjektion verwenden.

So was:

 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

Was ich getan habe, wenn ich nur sehen möchte, dass eine Zeichenfolge protokolliert wurde (im Gegensatz zur Überprüfung der genauen Protokollanweisungen, die einfach zu spröde sind), ist, StdOut in einen Puffer umzuleiten, ein Include auszuführen und dann StdOut zurückzusetzen:

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

1
Ich habe dies mit versucht java.util.logging(obwohl ich es verwendet habe System.setErr(new PrintStream(buffer));, weil es sich bei stderr anmeldet), aber es funktioniert nicht (der Puffer bleibt leer). Wenn ich System.err.println("foo")direkt verwende, funktioniert es, daher gehe ich davon aus, dass das Protokollierungssystem seine eigene Referenz des Ausgabestreams behält, aus dem es stammt System.err, sodass mein Aufruf von System.setErr(..)keine Auswirkung auf die Protokollausgabe hat, wie dies nach der Initialisierung des Protokollsystems geschieht.
Hoijui

0

Ich habe eine ähnliche Frage für log4 beantwortet. Siehe, wie kann ich mit junit testen, dass eine Warnung mit log4 protokolliert wurde?

Dies ist neuer und Beispiel mit Log4j2 (getestet mit 2.11.2) und Junit 5;

    package com.whatever.log;

    import org.apache.logging.log4j.Level;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.core.Logger;
    import org.apache.logging.log4j.core.*;
    import org.apache.logging.log4j.core.appender.AbstractAppender;
    import org.apache.logging.log4j.core.config.Configuration;
    import org.apache.logging.log4j.core.config.LoggerConfig;
    import org.apache.logging.log4j.core.config.plugins.Plugin;
    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
    import org.apache.logging.log4j.core.config.plugins.PluginElement;
    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;

    import java.util.ArrayList;
    import java.util.List;
    import static org.junit.Assert.*;

class TestLogger {

    private TestAppender testAppender;
    private LoggerConfig loggerConfig;
    private final Logger logger = (Logger)
            LogManager.getLogger(ClassUnderTest.class);

    @Test
    @DisplayName("Test Log Junit5 and log4j2")
    void test() {
        ClassUnderTest.logMessage();
        final LogEvent loggingEvent = testAppender.events.get(0);
        //asset equals 1 because log level is info, change it to debug and
        //the test will fail
        assertTrue(testAppender.events.size()==1,"Unexpected empty log");
        assertEquals(Level.INFO,loggingEvent.getLevel(),"Unexpected log level");
        assertEquals(loggingEvent.getMessage().toString()
                ,"Hello Test","Unexpected log message");
    }

    @BeforeEach
    private void setup() {
        testAppender = new TestAppender("TestAppender", null);

        final LoggerContext context = logger.getContext();
        final Configuration configuration = context.getConfiguration();

        loggerConfig = configuration.getLoggerConfig(logger.getName());
        loggerConfig.setLevel(Level.INFO);
        loggerConfig.addAppender(testAppender,Level.INFO,null);
        testAppender.start();
        context.updateLoggers();
    }

    @AfterEach
    void after(){
        testAppender.stop();
        loggerConfig.removeAppender("TestAppender");
        final LoggerContext context = logger.getContext();
        context.updateLoggers();
    }

    @Plugin( name = "TestAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
    static class TestAppender extends AbstractAppender {

        List<LogEvent> events = new ArrayList();

        protected TestAppender(String name, Filter filter) {
            super(name, filter, null);
        }

        @PluginFactory
        public static TestAppender createAppender(
                @PluginAttribute("name") String name,
                @PluginElement("Filter") Filter filter) {
            return new TestAppender(name, filter);
        }

        @Override
        public void append(LogEvent event) {
            events.add(event);
        }
    }

    static class ClassUnderTest {
        private static final Logger LOGGER =  (Logger) LogManager.getLogger(ClassUnderTest.class);
        public static void logMessage(){
            LOGGER.info("Hello Test");
            LOGGER.debug("Hello Test");
        }
    }
}

Verwenden der folgenden Maven-Abhängigkeiten

 <dependency>
 <artifactId>log4j-core</artifactId>
  <packaging>jar</packaging>
  <version>2.11.2</version>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>

Ich habe dies versucht und einen Fehler in der Setup-Methode in der Zeile loggerConfig = configuration.getLoggerConfig (logger.getName ()) erhalten. Der Fehler ist, dass nicht auf org.apache.logging.log4j.spi.LoggerContextShutdownEnabled-Klassendatei für org.apache.logging.log4j.spi.LoggerContextShutdownEnabled nicht gefunden werden kann
carlos palma

Ich habe den Code überprüft und einige kleinere Änderungen vorgenommen, aber es hat bei mir funktioniert. Ich schlage vor, dass Sie die Abhängigkeiten überprüfen und sicherstellen, dass alle Importe korrekt sind
Haim Raman

Hallo Haim. Am Ende habe ich die Logback-Lösung implementiert ... aber ich denke, Sie haben Recht, um diese zu implementieren, musste ich einen Import bereinigen, den ich von einer anderen log4j-Version gemacht hatte.
Carlos Palma

-1

Wenn Sie log4j2 verwenden, konnte ich mit der Lösung von https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ bestätigen , dass Nachrichten protokolliert wurden.

Die Lösung lautet wie folgt:

  • Definieren Sie einen log4j-Appender als ExternalResource-Regel

    public class LogAppenderResource extends ExternalResource {
    
    private static final String APPENDER_NAME = "log4jRuleAppender";
    
    /**
     * Logged messages contains level and message only.
     * This allows us to test that level and message are set.
     */
    private static final String PATTERN = "%-5level %msg";
    
    private Logger logger;
    private Appender appender;
    private final CharArrayWriter outContent = new CharArrayWriter();
    
    public LogAppenderResource(org.apache.logging.log4j.Logger logger) {
        this.logger = (org.apache.logging.log4j.core.Logger)logger;
    }
    
    @Override
    protected void before() {
        StringLayout layout = PatternLayout.newBuilder().withPattern(PATTERN).build();
        appender = WriterAppender.newBuilder()
                .setTarget(outContent)
                .setLayout(layout)
                .setName(APPENDER_NAME).build();
        appender.start();
        logger.addAppender(appender);
    }
    
    @Override
    protected void after() {
        logger.removeAppender(appender);
    }
    
    public String getOutput() {
        return outContent.toString();
        }
    }
  • Definieren Sie einen Test, der Ihre ExternalResource-Regel verwendet

    public class LoggingTextListenerTest {
    
        @Rule public LogAppenderResource appender = new LogAppenderResource(LogManager.getLogger(LoggingTextListener.class)); 
        private LoggingTextListener listener = new LoggingTextListener(); //     Class under test
    
        @Test
        public void startedEvent_isLogged() {
        listener.started();
        assertThat(appender.getOutput(), containsString("started"));
        }
    }

Vergessen Sie nicht, log4j2.xml als Teil von src / test / resources zu haben

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.