Domanda

Ho qualche codice sottoposto a test che chiama su un logger Java per segnalare il proprio stato. Nel codice di test JUnit, vorrei verificare che la voce di registro corretto è stato fatto in questo logger. Qualcosa lungo 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 questo potrebbe essere fatto con un registratore appositamente adattato (o gestore, o formattatore), ma io preferirei di riutilizzare una soluzione che già esiste. (E, ad essere onesti, non mi è chiaro come ottenere al LogRecord da un registratore, ma supponiamo che questo è possibile.)

È stato utile?

Soluzione 2

Grazie mille per questi (sorprendentemente) risposte rapide e disponibile; mi hanno messo sulla strada giusta per la mia soluzione.

Il codice di base erano Voglio usare questo, utilizza java.util.logging come meccanismo di logger, e non mi sento a casa abbastanza in quei codici di cambiare completamente che a log4j o logger interfacce / facciate. Ma sulla base di questi suggerimenti, I 'hacked-up' un estensione j.u.l.handler e che funziona come una festa.

Un breve riassunto di seguito. 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, è possibile memorizzare quanto vi piace / vuole / bisogno dal LogRecord, o li spinge in una pila fino ad ottenere un overflow.

Nella preparazione per il test JUnit, si crea un java.util.logging.Logger e aggiungere una nuova LogHandler quali ad 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 JUnit-test) senza la registrazione inutili accade. Evitare qualunque sia il codice sottoposto a test deve utilizzare questo registratore, eseguire il test e assertEquality:

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

(Naturalmente, si potrebbe spostare gran parte di questo lavoro in un metodo @Before e fare assortiti altri miglioramenti, ma che sarebbe ingombrare questa presentazione.)

Altri suggerimenti

Ho bisogno di questo più volte pure. Ho messo insieme un piccolo campione di sotto, che ci si vuole per adattarsi alle vostre esigenze. In sostanza, si crea il proprio Appender e aggiungerlo al logger che si desidera. Se ci si vuole raccogliere tutto, il logger principale è un buon punto di partenza, ma è possibile utilizzare una più specifica se si desidera. Non dimenticare di rimuovere l'Appender quando hai finito, altrimenti si potrebbe creare una perdita di memoria. Qui di seguito ho fatto all'interno del test, ma setUp o @Before e tearDown o @After potrebbero essere posti migliori, a seconda delle esigenze.

Inoltre, l'attuazione di seguito raccoglie tutto in un List in memoria. Se accedi molto si potrebbe prendere in considerazione l'aggiunta di un filtro per eliminare le voci noiosi, o di scrivere il registro in un file temporaneo sul disco (Suggerimento: LoggingEvent è Serializable, così si dovrebbe essere in grado di serializzare solo gli oggetti evento, se il vostro log dei messaggi è.)

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

Qui è una soluzione semplice ed efficace Logback.
Non necessita di aggiungere / creare qualsiasi nuova classe.
Esso si basa su ListAppender : un appender whitebox logback dove le voci di registro vengono aggiunte in un campo public List che potremmo usare in modo di rendere le nostre affermazioni.

Ecco un semplice esempio.

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

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

affermazioni JUnit non suonano molto adatti per far valere alcune proprietà specifiche degli elementi della lista.
librerie Matcher / asserzione AssertJ o Hamcrest sembra meglio 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 effetti si sta testando un effetto collaterale di una classe dipendente. Per unità di test è necessario solo per verificare che

  

logger.info()

è stato chiamato con il parametro corretto. Quindi utilizzare un quadro beffardo per emulare logger e che vi permetterà di testare il comportamento del proprio codice categoria.

Un'altra opzione è quella di prendere in giro Appender e verificare se il messaggio è stato registrato a questo appender. Esempio per Log4j 1.2.x e 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);
    }
}

Mocking è un'opzione qui, anche se sarebbe difficile, perché logger sono generalmente private static finale -. Così l'impostazione di un registratore di finto non sarebbe un pezzo di torta, o potrebbe richiedere la modifica della classe in prova

È possibile creare un costume Appender (o come si chiama), e registrarlo - attraverso un file di configurazione di prova per soli, o fase di esecuzione (in un certo senso, dipende dal quadro di registrazione). E poi si può ottenere che appender (in modo statico, se dichiarato nel file di configurazione, o dal suo riferimento attuale, se si sta collegarlo Runtime) e verificarne il contenuto.

Ispirato @ di RonaldBlaschke soluzione, sono arrivato fino 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"));
}

Si potrebbe probabilmente far utilizzare hamcrest in un modo più intelligente, ma ho lasciato a questo.

Ecco quello che ho fatto per 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();
    }
}

Poi nel genitore della mia classe TestNG unit test 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 / risorse e ho aggiunto un test appender:

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

e ha aggiunto questa appender al appender radice:

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

Ora nei miei corsi di prova 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, la Throwable.

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

Come già detto dagli altri che si potrebbe utilizzare un quadro di scherno. Per questo per fare il lavoro che dovete esporre il registratore nella tua classe (anche se avrei propably prefere per renderlo pacchetto privato invece di creare un setter pubblico).

L'altra soluzione è quella di creare un logger falso a mano. Devi scrivere il logger falso (più codice apparecchio), ma in questo caso io preferirei la maggiore leggibilità dei test contro il codice salvato dal quadro di scherno.

vorrei fare qualcosa di simile a questo:

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

Per log4j2 la soluzione è leggermente diversa perché AppenderSkeleton non è più disponibile. Inoltre, utilizzando Mockito, o una libreria simile per creare un Appender con un'ArgumentCaptor non funziona se vi aspettate di più messaggi di registrazione in quanto la MutableLogEvent viene riutilizzato su più messaggi di log. La soluzione migliore 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());
    }
}

Wow. Non sono sicuro perché questo era così difficile. Ho scoperto che ero in grado di utilizzare uno dei esempi di codice di cui sopra, perché stavo usando log4j2 sopra 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));
  }
}

Per quanto mi è possibile semplificare il test utilizzando JUnit con Mockito. Propongo seguente soluzione per esso:

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

E 'per questo che abbiamo bella flessibilità per le prove con la quantità messaggio

Un'altra idea degno di nota, anche se è un argomento vecchio, è la creazione di un produttore di CDI per iniettare il vostro registratore in modo che il beffardo diventa facile. (E si dà anche il vantaggio di non dover dichiarare la "dichiarazione di tutto il logger" più, ma questo è off-topic)

Esempio:

La creazione del registratore di 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 {
}

Utilizzando il registratore nel codice di produzione:

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

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

Test della logger nel codice di prova (dando un esempio EasyMock):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

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

Utilizzando JMockit (1.21) sono stato in grado di scrivere questo semplice test. Il test consente di verificare un messaggio di errore specifico è chiamato solo una 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.    
}

Mocking l'Appender può aiutare a catturare le righe di log. Trova campione: 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")));
}

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

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

se si utilizza java.util.logging.Logger questo articolo potrebbe essere molto utile, si crea un nuovo gestore e fare asserzioni sul registro di uscita: http://octodecillion.com/blog/jmockit-test-logging/

Ci sono due cose che si può essere cercando di testare.

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

Queste due cose sono in realtà cose diverse, e quindi potrebbero essere testati separatamente. Tuttavia, il test il secondo (il testo dei messaggi) è così problematico, vi consiglio di non farlo affatto. Una prova di un messaggio di testo in ultima analisi, consisterà di verificare che una stringa di testo (il testo del messaggio atteso) è lo stesso, oppure può essere banalmente derivato da, la stringa di testo utilizzati nel codice di registrazione.

  • Quei test non logica del programma di test a tutti, mettono alla prova soltanto che una risorsa (una stringa) è equivalente a un'altra risorsa.
  • I test sono fragili; anche un tweak minore per la formattazione di un messaggio di log rompe i test.
  • I test sono incompatibili con l'internazionalizzazione (traduzione) dei vostri test interface.The registrazione assumere v'è un solo possibile testo del messaggio, e quindi solo una possibile linguaggio umano.

Si noti che avere il vostro codice di programma (l'attuazione di alcune logica di business, forse) chiamando direttamente l'interfaccia di registrazione di testo è cattiva progettazione (ma purtroppo molto commom). Il codice che è responsabile per la logica di business è anche decidere qualche criterio di registrazione e il testo dei messaggi di log. Si mescola logica di business con il codice di interfaccia utente (sì, i messaggi di log sono parte dell'interfaccia utente del programma). Quelle cose devono essere separati.

Raccomando, quindi, la logica di business non genera direttamente il testo dei messaggi di log. Invece l'hanno delegare a un oggetto di registrazione.

  • La classe dell'oggetto registrazione dovrebbe fornire un adeguato API interna, che l'oggetto di business può utilizzare per esprimere l'evento che si è verificato utilizzando oggetti del vostro modello di dominio, non stringhe di testo.
  • L'implementazione della classe di registrazione è responsabile della produzione di rappresentazioni di testo di quegli oggetti di dominio e il rendering di una descrizione adeguata della manifestazione, quindi inoltrare il messaggio di testo al quadro di registrazione a basso livello (come luglio, log4j o slf4j) .
  • la logica di business è responsabile solo per chiamare i metodi corretti di API interna della classe logger, passando gli oggetti di dominio corretto, per descrivere gli eventi reali che si sono verificati.
  • La classe di registrazione concreta implements un interface, che descrive l'API interno la logica di business può utilizzare.
  • La classe (es) che implementa la logica di business e deve eseguire la registrazione ha un riferimento all'oggetto registrazione per delegare. La classe di riferimento è la interface astratto.
  • iniezione Usa dipendenza per impostare il riferimento allo strumento.

Si può quindi verificare che i corsi di logica di business in modo corretto dire l'interfaccia di registrazione di eventi, con la creazione di un registratore finto, che implementa l'API di registrazione interno, e con l'iniezione di dipendenza nella fase di messa a punto di test.

In questo modo:

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

Quello che ho fatto, se tutto quello che voglio fare è vedere che qualche stringa è stato registrato (al contrario di verifica delle dichiarazioni di registro esatto che è semplicemente troppo fragile) è di reindirizzare StdOut ad un buffer, fare un contiene, quindi reimpostare 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);

L'API per Log4J2 è leggermente differente. Inoltre si potrebbero utilizzare la sua appender asincrona. Ho creato un agganciato appender 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());
    }
}

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

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

La soluzione è questa:

  • Definire un appender log4j come una 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 / risorse

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top