Cómo hacer una afirmación JUnit en un mensaje en un registrador

206

Tengo un código bajo prueba que llama a 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 en las siguientes líneas:

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 sincero, no tengo claro cómo llegar al logRecord desde un registrador, pero supongamos que eso es posible).

Jon
fuente

Respuestas:

142

También he necesitado esto varias veces. He reunido una pequeña muestra a continuación, que querrás ajustar a tus necesidades. Básicamente, crea el suyo propio Appendery lo agrega al registrador que desea. Si desea recopilar todo, el registrador raíz es un buen lugar para comenzar, pero puede usar uno más específico si lo desea. No olvide quitar el Appender cuando haya terminado, de lo contrario podría crear una pérdida de memoria. A continuación, lo hice dentro de la prueba, pero setUpo @Beforey tearDowno @Afterpodrían ser mejores lugares, según sus necesidades.

Además, la implementación a continuación recopila todo en una Listmemoria. Si está registrando mucho, puede considerar agregar un filtro para descartar entradas aburridas o escribir el registro en un archivo temporal en el disco (Sugerencia: LoggingEventes Serializable, por lo que debería poder serializar los objetos de evento, si su mensaje de registro 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);
    }
}
Ronald Blaschke
fuente
44
Esto funciona muy bien. La única mejora que haría es llamar logger.getAllAppenders(), luego pasar y llamar appender.setThreshold(Level.OFF)a cada uno (¡y restablecerlos cuando haya terminado!). Esto asegura que los mensajes "malos" que intentas generar no aparezcan en los registros de prueba y asusten al próximo desarrollador.
Coderer
1
En Log4j 2.x es un poco más complicado ya que necesita crear un complemento, eche un vistazo a esto: stackoverflow.com/questions/24205093/…
paranza
1
Gracias por esto. Pero si está utilizando LogBack, puede usarlo en ListAppender<ILoggingEvent>lugar de crear su propio accesorio personalizado.
Sinujohn
2
pero esto no funciona para slf4j! ¿sabes cómo puedo cambiarlo para que funcione también con eso?
Shilan
3
@sd Si convierte el Loggera org.apache.logging.log4j.core.Logger(la clase de implementación para la interfaz) tendrá acceso setAppender()/removeAppender()nuevamente.
David Moles
60

Aquí hay una solución Logback simple y eficiente.
No requiere agregar / crear ninguna clase nueva.
Se basa en ListAppender: un complemento de registro de caja blanca donde las entradas de registro se agregan en un public Listcampo que podríamos usar para hacer nuestras afirmaciones.

Aquí hay un ejemplo simple.

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 de prueba:

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

Las aserciones JUnit no suenan muy adaptadas para afirmar algunas propiedades específicas de los elementos de la lista.
Las bibliotecas de Matcher / aserción como AssertJ o Hamcrest parecen mejores para eso:

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));
davidxxx
fuente
¿Cómo evita que la prueba falle si registra un error?
Ghilteras
@Ghilteras No estoy seguro de entender. Registrar un error no debería hacer que su prueba falle. Que explicas
davidxxx
Además, recuerde no mockla clase que está bajo prueba. Necesitas instanciarlo con el newoperador
Dmytro Chasovskyi
35

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

La base de código en la que quiero usar esto, usa java.util.logging como mecanismo de registro, y no me siento lo suficientemente en casa en esos códigos para cambiar completamente eso a log4j o a las interfaces / fachadas de registro. Pero en base a estas sugerencias, `` pirateé '' una extensión de julhandler y eso funciona como un regalo.

Sigue un breve resumen. Ampliar 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 todo lo que quiera / desee / necesite del LogRecord, o empujarlos a todos en una pila hasta que obtenga un desbordamiento.

En la preparación para la prueba junit, creas java.util.logging.Loggery le agregas una nueva LogHandler:

@Test tester() {
    Logger logger = Logger.getLogger("my junit-test logger");
    LogHandler handler = new LogHandler();
    handler.setLevel(Level.ALL);
    logger.setUseParentHandlers(false);
    logger.addHandler(handler);
    logger.setLevel(Level.ALL);

La llamada a setUseParentHandlers()es silenciar los manejadores normales, para que (para esta ejecución de prueba junit) no ocurra un registro innecesario. Haga lo que necesite su código bajo prueba para usar este registrador, ejecute la prueba y afirme Equality:

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

(Por supuesto, movería gran parte de este trabajo a un @Beforemétodo y realizaría otras mejoras variadas, pero eso saturaría esta presentación).

Jon
fuente
16

Otra opción es simular Appender y verificar si el mensaje se registró en este appender. Ejemplo para Log4j 1.2.xy 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);
    }
}
Marcin
fuente
16

Efectivamente, está probando un efecto secundario de una clase dependiente. Para las pruebas unitarias solo necesita verificar que

logger.info()

fue llamado con el parámetro correcto. Por lo tanto, use un marco de imitación para emular el registrador y eso le permitirá probar el comportamiento de su propia clase.

djna
fuente
3
¿Cómo te burlarías de un campo final estático privado, que la mayoría de los registradores están definidos? Powermockito? Diviértete ..
Stefano L
Stefano: Ese campo final se inicializó de alguna manera, he visto varios enfoques para inyectar simulacros en lugar de los reales. Probablemente requiere un cierto nivel de diseño para la comprobabilidad en primer lugar. blog.codecentric.de/en/2011/11/…
djna
Como dijo Mehdi, posiblemente usar un
controlador
11

Aquí burlarse es una opción, aunque sería difícil, porque los registradores son generalmente estáticos privados, por lo que configurar un registrador simulado no sería pan comido, o requeriría la modificación de la clase bajo prueba.

Puede crear un Appender personalizado (o como se llame) y registrarlo, ya sea a través de un archivo de configuración solo de prueba o en tiempo de ejecución (de alguna manera, dependiendo del marco de registro). Y luego puede obtener ese apéndice (ya sea estáticamente, si se declara en el archivo de configuración, o por su referencia actual, si lo está conectando en tiempo de ejecución), y verificar su contenido.

Bozho
fuente
10

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

}

... que te 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 podrías hacer que use Hamcrest de una manera más inteligente, pero lo dejé en esto.

Delgado
fuente
6

Para log4j2, la solución es ligeramente diferente porque AppenderSkeleton ya no está disponible. Además, el uso de Mockito o una biblioteca similar para crear un Appender con un ArgumentCaptor no funcionará si espera múltiples mensajes de registro porque MutableLogEvent se reutiliza en varios 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());
    }
}
Joseph
fuente
5

Como se mencionó de los demás, podría usar un marco burlón. Para que esto funcione, debe exponer el registrador en su clase (aunque preferiría que el paquete sea privado en lugar de crear un setter público).

La otra solución es crear un registrador falso a mano. Tiene que escribir el registrador falso (más código de dispositivo) pero en este caso preferiría la legibilidad mejorada de las pruebas contra el código guardado del marco de imitación.

Haría 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());
    }
}
Arne Deutsch
fuente
5

Guau. No estoy seguro de por qué esto fue tan difícil. Descubrí que no podía usar ninguno de los ejemplos de código anteriores porque 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));
  }
}
Dagmar
fuente
4

Esto es lo que hice para el inicio de sesión.

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

Luego, en el padre de mi clase de prueba de unidad de prueba, 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 / resources y agregué un apéndice de prueba:

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

y agregó este apéndice al apéndice 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 la clase de prueba de mi padre, puedo obtener el apéndice y obtener el último mensaje registrado y verificar el mensaje, el nivel, lo que se puede lanzar.

ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
kfox
fuente
No veo donde se define el método getAppender?!?
bioinfornatics
getAppender es un método en un ch.qos.logback.classic.Logger
kfox
4

Para Junit 5 (Jupiter) Spring's OutputCaptureExtension es bastante útil. Está disponible desde Spring Boot 2.2 y está disponible en el artefacto spring-boot-test .

Ejemplo (tomado de 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");
    }
}
aemaem
fuente
Creo que las declaraciones de registro son diferentes getOut()o getErr().
Ram
¡Esta es la respuesta que estaba buscando (aunque la pregunta no está relacionada con el arranque de primavera)!
helleye
3

En cuanto a mí puede simplificar su prueba mediante el uso JUnitde Mockito. Propongo la 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 una buena flexibilidad para las pruebas con diferentes cantidades de mensajes.

Dmytro Melnychuk
fuente
1
Para no repetir casi los mismos bloques de código, quiero agregar que casi 1to1 funciona para mí para Log4j2. Simplemente cambiando las importaciones a "org.apache.logging.log4j.core", when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); convierta el registrador a "org.apache.logging.log4j.core.Logger", agregue y cambie 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");
      }
    }));
  }
}
Kusum
fuente
1
Esto funcionó para mí. La línea 'when (mockAppender.getName ()). ThenReturn ("MOCK")' no era necesaria para mí.
Mayank Raghav
1

La API para Log4J2 es ligeramente diferente. También es posible que esté utilizando su apéndice asíncrono. Creé un apéndice enganchado 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());
    }
}

Úselo así:

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

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

    }//appender removed
robbo
fuente
1

Tenga en cuenta que en Log4J 2.x, la interfaz pública org.apache.logging.log4j.Loggerno incluye los métodos setAppender()y removeAppender().

Pero si no está haciendo algo demasiado elegante, debería poder lanzarlo a la clase de implementación org.apache.logging.log4j.core.Logger, que expone esos métodos.

Aquí hay un ejemplo con Mockito y 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);
David Moles
fuente
0

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 da la ventaja de no tener que declarar la "declaración de registrador completo", pero eso está fuera de tema)

Ejemplo:

Crear el registrador para inyectar:

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

El calificador:

@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();
}
GregD
fuente
0

Usando Jmockit (1.21) pude escribir esta simple prueba. La prueba asegura que se llame un mensaje de ERROR específico solo 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.    
}
Yarix
fuente
0

Burlarse del Appender puede ayudar a capturar las líneas de registro. Encuentre una 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")));
}
nishant
fuente
0

Usa el siguiente código. Estoy usando el mismo código para mi prueba de integración de primavera donde estoy usando log back para iniciar sesión. Utilice el método ClaimJobIsScheduled para afirmar 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);
        }
    }));
}
SUMITAR
fuente
0

Hay dos cosas que podrías estar intentando probar.

  • Cuando hay un evento de interés para el operador de mi programa, ¿realiza mi programa una operación de registro adecuada, 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 cosas diferentes, por lo que podrían probarse por separado. Sin embargo, probar el segundo (el texto de los mensajes) es tan problemático que recomiendo no hacerlo. Una prueba del texto de un mensaje consistirá en comprobar que una cadena de texto (el texto del mensaje esperado) es la misma o puede derivarse trivialmente de la cadena de texto utilizada en su código de registro.

  • Esas pruebas no prueban la lógica del programa, solo prueban que un recurso (una cadena) es equivalente a otro recurso.
  • Las pruebas son frágiles; Incluso un pequeño cambio en el formato de un mensaje de registro interrumpe sus pruebas.
  • Las pruebas son incompatibles con la internacionalización (traducción) de su interfaz de registro. Las pruebas asumen que solo hay un texto de mensaje posible y, por lo tanto, solo un lenguaje humano posible.

Tenga en cuenta que tener el código de su programa (implementando alguna lógica de negocios, tal vez) llamando directamente a la interfaz de registro de texto es un diseño deficiente (pero desafortunadamente muy común). El código responsable de la lógica empresarial también decide algunas políticas de registro y el texto de los mensajes de registro. Mezcla lógica empresarial con código de interfaz de usuario (sí, los mensajes de registro son parte de la interfaz de usuario de su programa). Esas cosas deberían estar separadas.

Por lo tanto, recomiendo que la lógica de negocios no genere directamente el texto de los mensajes de registro. En su lugar, delegue en un objeto de registro.

  • La clase del objeto de registro debe proporcionar una API interna adecuada, que su objeto comercial puede usar para expresar el evento que ha ocurrido usando objetos de su modelo de dominio, no cadenas de texto.
  • La implementación de su clase de registro es responsable de producir representaciones de texto de esos objetos de dominio, y presentar una descripción de texto adecuada del evento, luego reenviar ese mensaje de texto al marco de registro de bajo nivel (como JUL, log4j o slf4j).
  • Su lógica de negocios es responsable solo de llamar a los métodos correctos de la API interna de su clase de registrador, pasando los objetos de dominio correctos, para describir los eventos reales que ocurrieron.
  • Su registro de hormigón de clase implementsunainterface , que describe la API interna que puede usar su lógica empresarial.
  • Su (s) clase (s) que implementa la lógica de negocios y debe realizar el registro tiene una referencia al objeto de registro para delegar. La clase de la referencia es el resumen interface.
  • Use la inyección de dependencia para configurar la referencia al registrador.

Luego, puede probar que sus clases de lógica de negocios informan correctamente a la interfaz de registro sobre los eventos, creando un registrador simulado, que implementa la API de registro interno, y utilizando la inyección de dependencia en la fase de configuración de su prueba.

Me gusta esto:

 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);
    }
 }
Raedwald
fuente
0

Lo que he hecho si todo lo que quiero hacer es ver que se haya registrado alguna cadena (en lugar de verificar las declaraciones de registro exactas que son demasiado frágiles) es redirigir StdOut a un búfer, hacer un contenido y luego restablecer 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);
Cheefachi
fuente
1
Intenté esto con java.util.logging(aunque lo usé System.setErr(new PrintStream(buffer));, porque se registra en stderr), pero no funciona (el búfer permanece vacío). si lo uso System.err.println("foo")directamente, funciona, por lo que supongo que el sistema de registro mantiene su propia referencia de la secuencia de salida, que toma System.err, por lo que mi llamada a System.setErr(..)no tiene ningún efecto en la salida del registro, como sucede después del inicio del sistema de registro.
hoijui
0

Respondí una pregunta similar para log4j ver cómo-puedo-probar-con-junit-that-a-warning-was-log-with-log4

Esto es más reciente y un ejemplo con Log4j2 (probado con 2.11.2) y 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");
        }
    }
}

Usando las siguientes dependencias de 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>
Haim Raman
fuente
Intenté esto y obtuve un error dentro del método de configuración en la línea loggerConfig = configuration.getLoggerConfig (logger.getName ()); El error no puede acceder al archivo de clase org.apache.logging.log4j.spi.LoggerContextShutdownEnabled para org.apache.logging.log4j.spi.LoggerContextShutdownEnabled no encontrado
carlos palma
Revisé el código e hice algunos cambios menores, pero funcionó para mí. Sugiero que verifique las dependencias y se asegure de que todas las importaciones sean correctas
Haim Raman
Hola, Haim. Terminé implementando la solución logback ... pero creo que tienes razón, para implementar esa tuve que limpiar una importación que había hecho de otra versión log4j.
carlos palma
-1

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

La solución es así:

  • Defina un apéndice 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();
        }
    }
  • Defina una prueba que use su 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 olvide tener log4j2.xml como parte de src / test / resources

Greg7000
fuente