it-swarm.com.de

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

Ich habe Code-under-Test, der einen Java-Logger aufruft, um seinen Status zu melden. Im JUnit-Testcode möchte ich überprüfen, ob der korrekte Protokolleintrag in diesem Logger gemacht wurde. Etwas in den folgenden Zeilen:

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

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

Ich vermute, 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 erneut zu verwenden. (Um ehrlich zu sein, ist es mir nicht klar, wie man von einem Logger an den LogRecord kommt, aber nehmen wir an, dass das möglich ist.)

134
Jon

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

Die Codebasis, bei der ich dies verwenden möchte, verwendet Java.util.logging als Protokollierungsmechanismus, und ich fühle mich in diesen Codes nicht zu Hause, um dies vollständig in log4j oder in Schnittstellen/Fassaden zu ändern. Auf der Grundlage dieser Vorschläge habe ich eine j.u.l.handler-Erweiterung "gehackt", und das funktioniert als Leckerbissen.

Eine kurze Zusammenfassung folgt. Java.util.logging.Handler erweitern:

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

Offensichtlich können Sie so viel speichern, wie Sie möchten/wollen/von der LogRecord aus benötigen, oder sie alle in einen Stapel schieben, bis Sie einen Überlauf erhalten.

In der Vorbereitung für den junit-test erstellen Sie einen Java.util.logging.Logger und fügen eine neue LogHandler hinzu:

@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 stummzuschalten, so dass (für diesen Junit-Testlauf) keine unnötige Protokollierung stattfindet. Machen Sie, was auch immer Ihr Code-under-Test benötigt, um diesen Logger zu verwenden, führen Sie den Test aus und bestätigen Sie mit EQUALITY:

    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 @Before-Methode verschieben und verschiedene andere Verbesserungen vornehmen, aber dies würde diese Präsentation stören.)

31
Jon

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 eigene Appender und 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 auch einen spezifischeren verwenden, wenn Sie möchten. Vergessen Sie nicht, den Appender zu entfernen, wenn Sie fertig sind. Andernfalls können Sie einen Speicherverlust verursachen. Im Folgenden habe ich es im Test gemacht, aber setUp oder @Before und tearDown oder @After können je nach Bedarf bessere Orte sein.

Die folgende Implementierung sammelt außerdem alles in einer List im Speicher. Wenn Sie viel protokollieren, sollten 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: LoggingEvent ist Serializable). Daher sollten Sie in der Lage sein, die Ereignisobjekte nur zu serialisieren, falls dies der Fall ist Logmeldung 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);
    }
}
124
Ronald Blaschke

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 spöttisches Framework, um den Logger zu emulieren. Auf diese Weise können Sie das Verhalten Ihrer eigenen Klasse testen.

16
djna

Hier ist eine einfache und effiziente Logback-Lösung.
Es muss keine neue Klasse hinzugefügt/erstellt werden.
Sie basiert auf ListAppender : einem Whitelebox-Logback-Appender, in dem Protokolleinträge in ein public List-Feld eingefügt werden, das wir für unsere Assertionen verwenden könnten. 

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
        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-Assertions klingen nicht besonders gut, um bestimmte Eigenschaften der Listenelemente zu bestätigen.
Matcher/Assertion-Bibliotheken als 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));
15
davidxxx

Eine weitere Option ist, Appender zu verspotten und zu überprüfen, ob die Nachricht in 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);
    }
}
12
Marcin

Mocking ist hier eine Option, obwohl dies schwierig sein würde, da Logger in der Regel ein privates statisches Finale sind. Das Setzen eines Mock-Loggers wäre also kein Kinderspiel oder würde eine Änderung der getesteten Klasse erfordern.

Sie können einen benutzerdefinierten Appender erstellen (oder wie auch immer er genannt wird) und ihn registrieren - entweder über eine Testdatei (nur Testdateien) oder über die Laufzeitumgebung (abhängig vom Protokollierungs-Framework) .. und dann diesen Appender (entweder statisch, wenn in der Konfigurationsdatei deklariert, oder anhand der aktuellen Referenz, wenn Sie die Laufzeitumgebung anschließen), und überprüfen Sie den Inhalt.

10
Bozho

Inspiriert von der @ RonaldBlaschke-Lösung kam ich zu folgendem Ergebnis:

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 erlaubt zu tun:

@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 wahrscheinlich Hamcrest auf eine intelligentere Weise verwenden, aber ich habe es dabei belassen.

5
slim

Hier ist was ich für Logback 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 Test-Unit-Testklasse 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 zum Root-Appender hinzu:

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

Jetzt kann ich in meinen Testklassen, die von meiner übergeordneten Testklasse ausgehen, den Appender abrufen und die letzte protokollierte Nachricht abrufen und die Nachricht, den Level und den Throwable überprüfen.

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

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

Die andere Lösung besteht darin, einen falschen 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 vorziehen.

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

Beeindruckend. Ich bin nicht sicher, warum das so schwer war. Ich habe festgestellt, dass ich keines der obigen 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));
  }
}
3
Dagmar

Bei log4j2 ist die Lösung etwas anders, da AppenderSkeleton nicht mehr verfügbar ist. Die Verwendung von Mockito oder einer ähnlichen Bibliothek zum Erstellen eines Appenders mit einem ArgumentCaptor funktioniert darüber hinaus nicht, wenn Sie mehrere Protokollierungsnachrichten erwarten, da das MutableLogEvent für mehrere Protokollnachrichten verwendet 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());
    }
}
2
joseph

Für mich können Sie Ihren Test vereinfachen, indem Sie JUnit mit Mockito..__ verwenden. Ich schlage folgende Lösung dafür 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 Nice Flexibilität für Tests mit unterschiedlicher Nachrichtenmenge

2

Das Verspotten des Appenders kann dabei helfen, die Protokollzeilen zu erfassen. 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
nishant

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
Yarix

Was ich getan habe, wenn ich alles tun möchte, ist, dass ein String protokolliert wurde (im Gegensatz zur Überprüfung der genauen Protokollanweisungen, der nur zu spröde ist), StdOut in einen Puffer umzuleiten, einen Inhalt zu erstellen und 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);
0
Cheefachi

wenn Sie Java.util.logging.Logger verwenden, kann dieser Artikel sehr hilfreich sein. Er erstellt einen neuen Handler und gibt Assertions im Protokoll aus. Ausgabe: http://octodecillion.com/blog/jmockit-test-logging/

0
Mehdi Karamosly

Verwenden Sie den folgenden Code. Ich verwende denselben Code für meinen Frühling-Integrationstest, bei dem ich Protokoll zurück 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
SUMIT

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

  • Wenn ein Ereignis für den Bediener meines Programms von Interesse ist, führt mein Programm eine geeignete Protokollierungsoperation aus, die den Bediener über dieses Ereignis informieren kann.
  • Wenn mein Programm eine Protokollierungsoperation ausführt, hat die Protokollnachricht, die es erzeugt, den richtigen Text.

Diese beiden Dinge sind tatsächlich verschiedene Dinge und könnten daher separat getestet werden. Das Testen des zweiten (des Textes der Nachrichten) ist jedoch so problematisch, dass ich davon abraten sollte. Ein Test eines Nachrichtentexts besteht letztendlich darin, zu prüfen, ob eine Textzeichenfolge (der erwartete Nachrichtentext) mit der im Protokollierungscode verwendeten Textzeichenfolge identisch ist oder von ihr abgeleitet werden kann.

  • Diese Tests testen die Programmlogik überhaupt nicht, sie testen 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 es nur einen möglichen Nachrichtentext und somit nur eine mögliche menschliche Sprache gibt.

Beachten Sie, dass der Programmcode (der möglicherweise einige Geschäftslogik implementiert) die Textprotokollierungsschnittstelle direkt aufruft, ein schlechtes Design ist (aber leider sehr verbreitet). Code, der für die Geschäftslogik verantwortlich ist, entscheidet auch über einige Protokollierungsrichtlinien und den Text der Protokollnachrichten. Es mischt Geschäftslogik mit Benutzeroberflächencode (ja, Protokollnachrichten 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, die Ihr Geschäftsobjekt verwenden kann, um das Ereignis darzustellen, das unter Verwendung von Objekten Ihres Domänenmodells und nicht mit Textzeichenfolgen aufgetreten ist.
  • Die Implementierung Ihrer Protokollierungsklasse ist dafür verantwortlich, Textdarstellungen dieser Domänenobjekte zu erstellen, eine geeignete Textbeschreibung des Ereignisses anzuzeigen und diese Textnachricht an das Protokollierungs-Framework auf niedriger Ebene (z. B. JUL, log4j oder slf4j) weiterzuleiten.
  • Ihre Geschäftslogik ist nur dafür verantwortlich, die korrekten Methoden der internen API Ihrer Logger-Klasse aufzurufen und die korrekten Domänenobjekte zu übergeben, um die tatsächlich aufgetretenen Ereignisse zu beschreiben.
  • Ihre konkrete Protokollierungsklasse implements und interface beschreibt die interne API, die Ihre Geschäftslogik verwenden kann.
  • Ihre Klassen, die Geschäftslogik implementieren und die Protokollierung durchführen müssen, verfügen über einen Verweis auf das Protokollierungsobjekt, an das delegiert werden soll. Die Klasse der Referenz ist die abstrakte interface.
  • Verwenden Sie die Abhängigkeitsinjektion, um die Referenz zum Logger einzurichten.

Sie können dann testen, ob Ihre Geschäftslogikklassen die Protokollierungsschnittstelle korrekt über Ereignisse informieren, indem Sie einen Mock-Logger erstellen, der die interne Protokollierungs-API implementiert, und in der Einrichtungsphase Ihres Tests 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
Raedwald

Eine andere erwähnenswerte Idee, obwohl es ein älteres Thema ist, ist die Erstellung eines CDI-Produzenten, der Ihren Logger einfügt, damit das Spott leicht wird. (Und es bietet auch den Vorteil, dass die "gesamte Logger-Anweisung" nicht mehr deklariert werden muss, aber das ist Off-Topic)

Beispiel:

Erstellen des Loggers zum Einfügen: 

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

Der Qualifikant:

@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 (mit einem easyMock-Beispiel):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

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

Die API für Log4J2 unterscheidet sich geringfügig. Möglicherweise verwenden Sie auch den asynchronen Appender. Ich habe dafür einen verriegelten 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
0
robbo