Come eseguire un'asserzione JUnit su un messaggio in un logger


206

Ho un codice sotto test che chiama un logger Java per segnalarne lo stato. Nel codice di test JUnit, vorrei verificare che in questo logger sia stata effettuata la voce di registro corretta. Qualcosa seguendo le seguenti linee:

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

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

Suppongo che ciò potrebbe essere fatto con un logger (o gestore, o formatter) appositamente adattato, ma preferirei riutilizzare una soluzione già esistente. (E, ad essere sincero, non mi è chiaro come accedere a logRecord da un logger, ma supponiamo che sia possibile.)

Risposte:


142

Ne ho avuto bisogno anche più volte. Di seguito ho raccolto un piccolo campione, che vorresti adattare alle tue esigenze. Fondamentalmente, si crea il proprio Appendere lo si aggiunge al logger desiderato. Se desideri raccogliere tutto, il logger di root è un buon punto di partenza, ma puoi utilizzare uno più specifico se lo desideri. Non dimenticare di rimuovere l'Appender al termine, altrimenti potresti creare una perdita di memoria. Di seguito l'ho fatto all'interno del test, ma setUpo @Beforee tearDowno @Afterpotrebbe essere posto migliore, a seconda delle tue esigenze.

Inoltre, l'implementazione di seguito raccoglie tutto Listin memoria. Se stai registrando molto potresti prendere in considerazione l'aggiunta di un filtro per eliminare voci noiose o per scrivere il registro in un file temporaneo su disco (Suggerimento: LoggingEventè Serializable, quindi dovresti essere in grado di serializzare solo gli oggetti evento, se il tuo messaggio di registro è.)

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
Funziona benissimo. L'unico miglioramento che vorrei fare è chiamare logger.getAllAppenders(), quindi scorrere e chiamare appender.setThreshold(Level.OFF)ciascuno (e ripristinarli quando hai finito!). Questo si assicura che i messaggi "cattivi" che si sta tentando di generare non vengano visualizzati nei registri dei test e spaventano lo sviluppatore successivo.
Coderer

1
In Log4j 2.x è leggermente più contorto in quanto è necessario creare un plugin, dai un'occhiata a questo: stackoverflow.com/questions/24205093/…
paranza

1
Grazie per questo. Ma se si utilizza LogBack, è possibile utilizzare ListAppender<ILoggingEvent>invece di creare il proprio appender personalizzato.
sinujohn,

2
ma questo non funziona per slf4j! sai come posso cambiarlo per lavorare anche con quello?
Shilan,

3
@sd Se si esegue il cast Loggerdi org.apache.logging.log4j.core.Logger(la classe di implementazione per l'interfaccia) si accede setAppender()/removeAppender()nuovamente a.
David Moles,

60

Ecco una soluzione Logback semplice ed efficiente.
Non richiede di aggiungere / creare alcuna nuova classe.
Si basa su ListAppender: un appender di logback di whitebox in cui vengono aggiunte voci di registro in un public Listcampo che potremmo così utilizzare per fare le nostre asserzioni.

Qui c'è un semplice esempio.

Classe Foo:

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

Classe FooTest:

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

Le asserzioni di JUnit non sembrano molto adattate per affermare alcune proprietà specifiche degli elementi dell'elenco.
Le librerie di abbinamento / asserzioni come AssertJ o Hamcrest sembrano migliori per questo:

Con AssertJ sarebbe:

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

In che modo si interrompe il test se si registra un errore?
Ghilteras,

@Ghilteras Non sono sicuro di capire. La registrazione di un errore non dovrebbe far fallire il test. Cosa spieghi?
davidxxx,

Inoltre, ricorda di non essere mockla classe sotto test. Devi istanziarlo con l' newoperatore
Dmytro Chasovskyi il

35

Grazie mille per queste risposte (sorprendentemente) veloci e utili; mi hanno messo sulla strada giusta per la mia soluzione.

La base di codice in cui voglio usarlo, usa java.util.logging come meccanismo di logger, e in quei codici non mi sento abbastanza a mio agio da cambiarlo completamente in log4j o interfacce / facciate del logger. Ma sulla base di questi suggerimenti, ho "modificato" un'estensione julhandler e che funziona come un piacere.

Segue un breve riassunto. Estendere 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(){}
}

Ovviamente, puoi archiviare quanto vuoi / desideri / desideri dal LogRecord, oppure inserirli tutti in uno stack fino a ottenere un overflow.

Nella preparazione per il test junit, si crea un java.util.logging.Loggere si aggiunge un nuovo LogHandlerad esso:

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

La chiamata a setUseParentHandlers()è di mettere a tacere i gestori normali, in modo che (per questa esecuzione di test junit) non avvenga alcuna registrazione non necessaria. Fai qualunque cosa il tuo codice sotto test abbia bisogno per usare questo logger, esegui il test e asserisci

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

(Naturalmente, sposteresti gran parte di questo lavoro in un @Beforemetodo e apporteresti altri miglioramenti assortiti, ma ciò ingombrerebbe questa presentazione.)


16

Un'altra opzione è quella di deridere l'Appender e verificare se il messaggio è stato registrato in questo appender. Esempio per Log4j 1.2.xe 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

In effetti stai testando un effetto collaterale di una classe dipendente. Per i test unitari devi solo verificarlo

logger.info()

è stato chiamato con il parametro corretto. Quindi usa un framework beffardo per emulare il logger e questo ti permetterà di testare il comportamento della tua classe.


3
Come hai deriso un campo finale statico privato, definito dalla maggior parte dei logger? Powermockito? Buon divertimento ..
Stefano L

Stefano: Quell'ultimo campo è stato inizializzato in qualche modo, ho visto vari approcci per iniettare Mock piuttosto che la cosa reale. Probabilmente richiede un certo livello di progettazione per la testabilità in primo luogo. blog.codecentric.de/it/2011/11/…
djna

Come disse Mehdi, forse usare un Handler adatto potrebbe essere sufficiente,
djna

11

Il deridere è un'opzione qui, anche se sarebbe difficile, perché i logger sono generalmente finali statici privati ​​- quindi impostare un logger fittizio non sarebbe un gioco da ragazzi, o richiederebbe una modifica della classe sotto test.

È possibile creare un Appender personalizzato (o come si chiama) e registrarlo - tramite un file di configurazione di solo test o runtime (in un certo senso, a seconda del framework di registrazione). E poi puoi ottenere quell'appender (o staticamente, se dichiarato nel file di configurazione, o tramite il suo riferimento attuale, se lo stai collegando in fase di esecuzione) e verificarne il contenuto.


10

Ispirato dalla soluzione di @ RonaldBlaschke, ho pensato a questo:

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

}

... che ti permette di fare:

@Rule public Log4JTester logTest = new Log4JTester();

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

Probabilmente potresti usarlo in modo più intelligente, ma l'ho lasciato a questo.


6

Per log4j2 la soluzione è leggermente diversa perché AppenderSkeleton non è più disponibile. Inoltre, l'utilizzo di Mockito o di una libreria simile per creare un Appender con ArgumentCaptor non funzionerà se si prevedono più messaggi di registrazione perché MutableLogEvent viene riutilizzato su più messaggi di registro. La migliore soluzione che ho trovato per log4j2 è:

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

Come menzionato dagli altri, potresti usare un quadro beffardo. Perché ciò funzioni, devi esporre il logger nella tua classe (anche se preferirei preferibilmente renderlo privato invece di creare un setter pubblico).

L'altra soluzione è creare manualmente un registratore falso. Devi scrivere il logger falso (più codice fixture) ma in questo caso preferirei la migliore leggibilità dei test rispetto al codice salvato dal framework beffardo.

Vorrei fare qualcosa del genere:

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

Wow. Non sono sicuro del perché sia ​​stato così difficile. Ho scoperto che non ero in grado di utilizzare nessuno degli esempi di codice sopra perché stavo usando log4j2 su slf4j. Questa è la mia soluzione:

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

Ecco cosa ho fatto per il logback.

Ho creato una classe TestAppender:

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

Quindi nel genitore della mia classe di test unit testng ho creato un metodo:

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

Ho un file logback-test.xml definito in src / test / resources e ho aggiunto un appender di test:

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

e ha aggiunto questo appender all'appender principale:

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

Ora nelle mie classi di test che si estendono dalla mia classe di test genitore posso ottenere l'appender e ottenere l'ultimo messaggio registrato e verificare il messaggio, il livello, il lancio.

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

Non vedo dove sia definito il metodo getAppender?!?
bioinfornatics,

getAppender è un metodo su ch.qos.logback.classic.Logger
kfox

4

Per Junit 5 (Jupiter) OutputCaptureExtension Spring è abbastanza utile. È disponibile da Spring Boot 2.2 ed è disponibile nel test di avvio a molla .

Esempio (tratto da 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");
    }
}

Credo che le dichiarazioni di registro siano diverse getOut()o getErr().
Ram

Questa è la risposta che stavo cercando (anche se la domanda non è relativa allo stivale a molla)!
helleye,

3

Per quanto mi riguarda puoi semplificare il tuo test usando JUnitcon Mockito. Propongo la seguente soluzione per questo:

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

Ecco perché abbiamo una buona flessibilità per i test con diverse quantità di messaggi


1
Per non ripetere quasi gli stessi blocchi di codice, voglio aggiungere che quasi 1to1 funziona per me per Log4j2. Basta cambiare le importazioni in "org.apache.logging.log4j.core", cast logger in "org.apache.logging.log4j.core.Logger", aggiungere when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); e modificare LoggingEvent -> 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
Questo ha funzionato per me. Riga 'quando (mockAppender.getName ()). ThenReturn ("MOCK")' non era necessario per me.
Mayank Raghav,

1

L'API per Log4J2 è leggermente diversa. Inoltre potresti usare il suo appender asincrono. Ho creato un appender bloccato per questo:

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

Usalo in questo modo:

        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

Si noti che in Log4J 2.x, l'interfaccia pubblica org.apache.logging.log4j.Loggernon include i metodi setAppender()e removeAppender().

Ma se non stai facendo nulla di troppo sofisticato, dovresti essere in grado di lanciarlo nella classe di implementazione org.apache.logging.log4j.core.Logger, che espone questi metodi.

Ecco un esempio con Mockito e 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

Un'altra idea degna di nota, sebbene sia un argomento più vecchio, è la creazione di un produttore di CDI per iniettare il tuo logger in modo che il beffardo diventi facile. (E offre anche il vantaggio di non dover più dichiarare "tutta la dichiarazione del logger", ma è fuori tema)

Esempio:

Creazione del logger da iniettare:

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

Il qualificatore:

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

Usando il logger nel tuo codice di produzione:

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

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

Test del logger nel codice di test (fornendo un esempio easyMock):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

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

0

Usando Jmockit (1.21) sono stato in grado di scrivere questo semplice test. Il test si assicura che un messaggio ERRORE specifico venga chiamato una sola volta.

@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

Deridere l'Appender può aiutare a catturare le linee di registro. Trova l'esempio su: 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

Usa il codice qui sotto. Sto usando lo stesso codice per il mio test di integrazione primaverile in cui sto usando il registro per la registrazione. Utilizzare il metodo assertJobIsScheduled per affermare il testo stampato nel registro.

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

Ci sono due cose che potresti provare a provare.

  • Quando c'è un evento di interesse per l'operatore del mio programma, il mio programma esegue un'operazione di registrazione appropriata, che può informare l'operatore di quell'evento.
  • Quando il mio programma esegue un'operazione di registrazione, il messaggio di registro che produce ha il testo corretto.

Queste due cose sono in realtà cose diverse e quindi potrebbero essere testate separatamente. Tuttavia, testare il secondo (il testo dei messaggi) è così problematico, sconsiglio affatto di farlo. Un test del testo di un messaggio consisterà in ultima analisi nel verificare che una stringa di testo (il testo del messaggio previsto) sia la stessa o possa essere banalmente derivata dalla stringa di testo utilizzata nel codice di registrazione.

  • Questi test non verificano affatto la logica del programma, ma verificano solo che una risorsa (una stringa) equivale a un'altra risorsa.
  • I test sono fragili; anche una piccola modifica alla formattazione di un messaggio di registro interrompe i test.
  • I test sono incompatibili con l'internazionalizzazione (traduzione) dell'interfaccia di registrazione. I test presuppongono che vi sia un solo testo di messaggio possibile e quindi solo un linguaggio umano possibile.

Nota che avere il tuo codice di programma (forse implementando una logica di business, forse) chiamando direttamente l'interfaccia di registrazione del testo è un progetto scadente (ma sfortunatamente molto comune). Il codice responsabile della logica aziendale sta inoltre decidendo alcuni criteri di registrazione e il testo dei messaggi di registro. Combina la logica aziendale con il codice dell'interfaccia utente (sì, i messaggi di registro fanno parte dell'interfaccia utente del programma). Quelle cose dovrebbero essere separate.

Pertanto, raccomando che la logica aziendale non generi direttamente il testo dei messaggi di registro. Invece, delegalo a un oggetto di registrazione.

  • La classe dell'oggetto di registrazione deve fornire un'API interna adatta, che l'oggetto aziendale può utilizzare per esprimere l'evento che si è verificato utilizzando oggetti del modello di dominio, non stringhe di testo.
  • L'implementazione della classe di registrazione è responsabile della produzione di rappresentazioni di testo di tali oggetti di dominio e del rendering di un'adeguata descrizione di testo dell'evento, quindi inoltro di tale messaggio al framework di registrazione di basso livello (come JUL, log4j o slf4j).
  • La tua logica aziendale è responsabile solo di chiamare i metodi corretti dell'API interna della tua classe logger, passando gli oggetti di dominio corretti, per descrivere gli eventi reali che si sono verificati.
  • La tua classe di registrazione concreta implementsan interface, che descrive l'API interna che la tua logica aziendale può utilizzare.
  • Le classi che implementano la logica aziendale e devono eseguire la registrazione hanno un riferimento all'oggetto di registrazione a cui delegare. La classe del riferimento è l'abstractinterface .
  • Utilizzare l'iniezione delle dipendenze per impostare il riferimento al logger.

È quindi possibile verificare che le classi di logica aziendale comunichino correttamente l'interfaccia di registrazione degli eventi, creando un finto logger, che implementa l'API di registrazione interna e utilizzando l'iniezione delle dipendenze nella fase di impostazione del test.

Come questo:

 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

Quello che ho fatto se tutto quello che voglio fare è vedere che è stata registrata una stringa (invece di verificare le esatte istruzioni di registro che sono troppo fragili) è reindirizzare StdOut su un buffer, fare un contenuto, quindi ripristinare StdOut:

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
L'ho provato con java.util.logging(anche se l'ho usato System.setErr(new PrintStream(buffer));, perché registra su stderr), ma non funziona (il buffer rimane vuoto). se lo uso System.err.println("foo")direttamente, funziona, quindi presumo che il sistema di registrazione mantenga il proprio riferimento al flusso di output, da cui prende System.err, quindi la mia chiamata a System.setErr(..)non ha alcun effetto sull'output del registro, come accade dopo l'iniz del sistema di registro.
hoijui,

0

Ho risposto a una domanda simile per log4j: vedi come-posso-test-con-junit-che-un-avvertimento-era-registrato-con-log4

Questo è più recente ed esempio con Log4j2 (testato con 2.11.2) e 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");
        }
    }
}

Utilizzando le seguenti dipendenze di Maven

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

Ho provato questo e ho riscontrato un errore all'interno del metodo di installazione sulla riga loggerConfig = configuration.getLoggerConfig (logger.getName ()); L'errore non è in grado di accedere al file di classe org.apache.logging.log4j.spi.LoggerContextShutdownEnabled per org.apache.logging.log4j.spi.LoggerContextShutdownEnabled non trovato
carlos palma

Ho rivisto il codice e apportato alcune piccole modifiche, ma ha funzionato per me. Suggerisco di controllare le dipendenze e assicurarsi che tutte le importazioni siano corrette
Haim Raman,

Salve, Haim. Ho finito per implementare la soluzione di logback ... ma penso che tu abbia ragione, al fine di implementare quella che ho dovuto pulire un'importazione che avevo fatto di un'altra versione di log4j.
carlos palma,

-1

Se si utilizza log4j2, la soluzione di https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ mi ha permesso di affermare che i messaggi sono stati registrati.

La soluzione è questa:

  • Definire un appender log4j come regola ExternalResource

    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();
        }
    }
  • Definire un test che utilizza la regola ExternalResource

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

Non dimenticare di avere log4j2.xml come parte di src / test / resources

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.