Pregunta

Tengo un código bajo prueba que solicita un registrador de Java para informar su estado.En el código de prueba JUnit, me gustaría verificar que se realizó la entrada de registro correcta en este registrador.Algo parecido a lo siguiente:

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

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

Supongo que esto podría hacerse con un registrador (o controlador o formateador) especialmente adaptado, pero preferiría reutilizar una solución que ya existe.(Y, para ser honesto, no tengo claro cómo acceder al logRecord desde un registrador, pero supongamos que eso es posible).

¿Fue útil?

Solución 2

Muchas gracias por estos (sorprendentemente) respuestas rápidas y útiles; me pusieron en el camino correcto para mi solución.

El código base fueron quiero usar esto, utiliza java.util.logging como su mecanismo registrador, y no me siento como en casa lo suficiente en esos códigos para cambiar por completo que a log4j o al registrador de interfaces / fachadas. Pero en base a estas sugerencias, 'hackeado-up' una extensión j.u.l.handler y que funciona como un regalo.

Un breve resumen a continuación. Extender 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(){}
}

Obviamente, puede almacenar tanto como te gusta / querer / necesidad de la LogRecord, o empujar a todos en una pila hasta que obtenga un desbordamiento.

En la preparación para el junit-test, se crea un java.util.logging.Logger y añadir un nuevo LogHandler como a ella:

@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 llamada a setUseParentHandlers() es para silenciar los controladores normales, de modo que (para esta ejecución de la prueba junit) ningún registro innecesario sucede. No sea cual sea su código bajo prueba tiene que utilizar este registrador, ejecute la prueba y assertEquality:

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

(Por supuesto, debería mover gran parte de este trabajo en un método @Before y hacer una variedad de otras mejoras, pero que sería el desorden de esta presentación.)

Otros consejos

he necesitado esto varias veces también. He creado una pequeña muestra a continuación, que te gustaría para adaptarse a sus necesidades. Básicamente, se crea su propio Appender y la agrega al registro prefieres. Si desea desea recoger todo, el registrador de la raíz es un buen lugar para empezar, pero se puede utilizar una forma más específica, si lo desea. No olvide quitar la Appender de cuando haya terminado, de lo contrario puede crear una pérdida de memoria. A continuación lo he hecho dentro de la prueba, pero setUp o @Before y tearDown o @After podrían ser mejores lugares, dependiendo de sus necesidades.

Además, la aplicación a continuación recoge todo en un List en la memoria. Si se está acumulando una gran cantidad se podría considerar la adición de un filtro para dejar entradas aburridas, o para escribir el registro en un archivo temporal en el disco (Pista: LoggingEvent es Serializable, por lo que debe ser capaz de simplemente serializar los objetos de eventos, si su registro de mensajes es.)

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

Esta es una solución simple y eficaz Logback.
No requiere añadir / crear cualquier nueva clase.
Se basa en ListAppender : un appender whitebox logback donde las entradas del registro se agregan en un campo public List que para que pudiéramos utilizar para hacer nuestras afirmaciones.

Este es un ejemplo sencillo.

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

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

JUnit afirmaciones no suenan muy adecuadas para hacer valer algunas propiedades específicas de los elementos de la lista.
bibliotecas matcher / afirmación como AssertJ o Hamcrest parece mejor para que:

Con AssertJ sería:

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

Efectivamente está probando un efecto secundario de una clase dependiente. Para las pruebas unitarias sólo tiene que verificar que

  

logger.info()

fue llamado con el parámetro correcto. Por lo tanto utilizar un marco de burla para emular registrador y que permitirá poner a prueba el comportamiento de su propia clase.

Otra opción es para burlarse y Appender de comprobar si el mensaje se registra en este appender. Ejemplo para Log4j 1.2.x y 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);
    }
}

El imitar es una opción aquí, aunque sería difícil, ya que los registradores son generalmente privada estática final -. Por lo que establecer un registrador de simulacro no sería un pedazo de pastel, o requeriría la modificación de la clase bajo prueba

Se puede crear una costumbre Appender de (o como se llame), y registrarlo - ya sea a través de un archivo de configuración de prueba de sólo, o en tiempo de ejecución (de una manera dependiente del marco de registro). Y entonces usted puede conseguir que appender (de forma estática, si se declara en el archivo de configuración, o por su referencia actual, si está conectando es tiempo de ejecución), y verificar su contenido.

Inspirado por la solución de @ RonaldBlaschke, se me ocurrió esto:

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

}

... lo que le permite hacer:

@Rule public Log4JTester logTest = new Log4JTester();

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

Probablemente se podría hacer que use hamcrest de una manera más inteligente, pero me ha dejado en esto.

Esto es lo que hice para logback.

He creado una clase 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();
    }
}

A continuación, en el padre de mi clase de prueba de unidad TestNG creé un método:

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

Tengo un archivo logback-test.xml definido en src / test / recursos y he añadido un appender prueba:

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

y añadido este appender a la appender raíz:

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

Ahora en mis clases de prueba que se extienden desde mi clase de prueba matriz puedo conseguir el appender y obtener el último mensaje registrado y verificar el mensaje, el nivel, el throwable.

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

Como se ha mencionado a los demás se puede utilizar un marco de burla. Para esto para hacer el trabajo que tiene que exponer el registrador en su clase (aunque me gustaría además probablemente prefere para que sea Programa privado en lugar de crear un regulador público).

La otra solución es crear un registrador de falsa con la mano. Tienes que escribir el registrador falsa (más código accesorio) pero en este caso yo preferiría la legibilidad mejorada de las pruebas contra el código salvado del marco de burla.

Me gustaría hacer algo como esto:

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

Para log4j2 la solución es ligeramente diferente porque AppenderSkeleton ya no está disponible. Además, el uso Mockito, o una biblioteca similar para crear un Appender de con un ArgumentCaptor no funcionará si usted está esperando múltiples mensajes de registro debido a que el MutableLogEvent se vuelve a utilizar en múltiples mensajes de registro. La mejor solución que encontré para log4j2 es:

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. Estoy seguro de por qué esto era tan difícil. He encontrado que era incapaz de utilizar cualquiera de los ejemplos de código anterior porque yo estaba usando log4j2 sobre slf4j. Esta es mi solución:

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

En cuanto a mí puede simplificar su prueba mediante el uso de JUnit con Mockito. Propongo siguiente solución para ello:

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

Es por eso que tenemos buena flexibilidad para las pruebas con diferente cantidad de mensajes

Otra idea que vale la pena mencionar, aunque es un tema antiguo, es crear un productor de CDI para inyectar su registrador para que la burla sea más fácil.(Y también ofrece la ventaja de no tener que declarar más la "declaración completa del registrador", pero eso está fuera de tema)

Ejemplo:

Creando el registrador para inyectar:

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

El clasificado:

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

Usando el registrador en su código de producción:

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

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

Probar el registrador en su código de prueba (dando un ejemplo de easyMock):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

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

El uso de Jmockit (1,21) que era capaz de escribir esta sencilla prueba. La prueba se asegura un mensaje de error específico se llama sólo una vez.

@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.    
}

Burlándose de la Appender de puede ayudar a capturar las líneas de registro. Encuentra muestra en: 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")));
}

Utilice el código de abajo. Estoy usando mismo código para mi prueba de integración primavera donde estoy usando registro de vuelta para el registro. El uso del método assertJobIsScheduled hacer valer el texto impreso en el 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);
        }
    }));
}

si está utilizando java.util.logging.Logger este artículo podría ser muy útil, se crea un nuevo controlador y hacer afirmaciones sobre la salida del registro: http://octodecillion.com/blog/jmockit-test-logging/

Hay dos cosas que usted podría estar tratando de probar.

  • Cuando hay un evento de interés para el operador de mi programa, qué mi programa de realizar una operación de registro adecuado, que puede informar al operador de ese evento.
  • Cuando mi programa realiza una operación de registro, ¿el mensaje de registro que produce tiene el texto correcto.

Esas dos cosas son en realidad las cosas diferentes, y por lo tanto podrían ser probados por separado. Sin embargo, las pruebas de la segunda (el texto de los mensajes) es tan problemático, recomiendo en contra de hacerlo en absoluto. Una prueba de un mensaje de texto en última instancia, consistir en la comprobación de que una cadena de texto (el texto del mensaje esperado) es la misma que, o se puede derivar trivialmente de, la cadena de texto utilizada en su código de registro.

  • Esas pruebas no la lógica del programa de prueba en absoluto, sólo prueba que un recurso (una cadena) es equivalente a otro recurso.
  • Las pruebas son frágiles; incluso un pequeño ajuste al formato de un mensaje de registro rompe sus pruebas.
  • Las pruebas son incompatibles con la internacionalización (traducción) de sus pruebas interface.The de registro asume que sólo hay un posible texto del mensaje, y por lo tanto sólo una posible lenguaje humano.

Tenga en cuenta que tener el código del programa (aplicación de algunas de lógica de negocio, tal vez) llamando directamente a la interfaz de registro de texto es un mal diseño (pero por desgracia muy común, conviene). El código que se encarga de la lógica de negocio también está decidiendo alguna política de registro y el texto de los mensajes de registro. Se mezcla la lógica de negocio con el código de interfaz de usuario (sí, los mensajes de registro son parte de la interfaz de usuario del programa). Esas cosas deben estar separados.

Por tanto, recomiendo que la lógica de negocio no genera directamente el texto de los mensajes de registro. En su lugar tiene que delegar en un objeto de registro.

  • La clase del objeto de registro debe proporcionar una API interno adecuado, que su objeto de negocio puede utilizar para expresar el evento que se ha producido el uso de objetos de su modelo de dominio, no cadenas de texto.
  • La implementación de la clase de registro es responsable de producir representaciones de texto de los objetos de dominio y renderizar una descripción de texto adecuado del evento, entonces el reenvío que mensaje de texto al marco de registro de nivel bajo (como JUL log4j o slf4j) .
  • Su lógica de negocio sólo es responsable de llamar a los métodos correctos de la API interna de su clase de logger, pasando los objetos de dominio correctos, para describir los acontecimientos reales que ocurrieron.
  • Su clase de registro de hormigón implements un interface, que describe la API interna de la lógica de negocio puede utilizar.
  • Su clase (s) que implementa la lógica de negocio y debe realizar el registro tiene una referencia al objeto de registro que puede delegar a. La clase de la referencia es el interface abstracto.
  • Uso de la inyección de dependencia para establecer la referencia al registrador.

A continuación, puede probar que sus clases de lógica de negocio correctamente cuentan la interfaz de registro acerca de los eventos, mediante la creación de un registrador de simulacro, que implementa la API de memoria interna de datos, y el uso de la inyección de dependencia en la fase de puesta en marcha de la prueba.

De esta manera:

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

Lo que he hecho, si todo lo que quiero hacer es ver que alguna cadena se registra (en oposición a la verificación de las declaraciones de registro exacto que es demasiado frágil) es redirigir StdOut a un búfer, hacer una contiene, restablezca 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);

El API para Log4J2 es ligeramente diferente. También puede que esté utilizando su appender asíncrono. He creado un aferré appender para esto:

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

Utilice esta manera:

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

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

    }//appender removed

Si está utilizando log4j2, la solución de https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ me ha permitido afirmar mensajes se registran.

La solución es la siguiente:

  • Definir un appender log4j como una regla 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();
        }
    }
    
  • Definir una prueba que utiliza la regla 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"));
        }
    }
    

No se olvide de tener log4j2.xml como parte de src / test / recursos

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top