Como fazer uma declaração JUnit em uma mensagem em um criador de logs

206

Eu tenho algum código em teste que solicita que um criador de logs Java relate seu status. No código de teste JUnit, gostaria de verificar se a entrada de log correta foi feita nesse criador de logs. Algo ao longo das seguintes linhas:

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

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

Suponho que isso possa ser feito com um criador de logs (ou manipulador ou formatador especialmente adaptado), mas prefiro reutilizar uma solução que já existe. (E, para ser sincero, não está claro para mim como obter o logRecord de um criador de logs, mas suponha que isso seja possível.)

Jon
fonte

Respostas:

142

Eu também precisei disso várias vezes. Reunimos uma pequena amostra abaixo, que você deseja ajustar às suas necessidades. Basicamente, você cria o seu próprio Appendere o adiciona ao criador de logs que deseja. Se você deseja coletar tudo, o registrador raiz é um bom ponto de partida, mas você pode usar um mais específico, se desejar. Não se esqueça de remover o Appender quando terminar, caso contrário, você poderá criar um vazamento de memória. Abaixo, fiz isso dentro do teste, mas setUpou @Beforee / tearDownou @Afterpodem ser lugares melhores, dependendo de suas necessidades.

Além disso, a implementação abaixo coleta tudo Listna memória. Se você estiver registrando muito, considere adicionar um filtro para eliminar entradas chatas ou gravar o log em um arquivo temporário no disco (Dica: LoggingEventis Serializable, portanto, você poderá serializar apenas os objetos de evento, se sua mensagem de log é.)

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
fonte
4
Isso funciona muito bem. A única melhoria que eu faria seria ligar logger.getAllAppenders(), depois avançar e chamar appender.setThreshold(Level.OFF)cada um (e redefini-los quando terminar!). Isso garante que as mensagens "ruins" que você está tentando gerar não apareçam nos logs de teste e assustem o próximo desenvolvedor.
Coderer
1
Em Log4j 2.x é um pouco mais complicado que você precisa para criar um plugin, ter um olhar para isto: stackoverflow.com/questions/24205093/...
paranza
1
Obrigado por isso. Mas se você estiver usando o LogBack, poderá usar em ListAppender<ILoggingEvent>vez de criar seu próprio aplicativo personalizado.
precisa saber é
2
mas isso não funciona para slf4j! você sabe como posso mudar isso para trabalhar com isso também?
Shilan
3
@sd Se você converter Loggerpara org.apache.logging.log4j.core.Logger(a classe de implementação da interface), terá acesso setAppender()/removeAppender()novamente.
David Moles
59

Aqui está uma solução Logback simples e eficiente.
Não é necessário adicionar / criar nenhuma nova classe.
Ele se baseia em ListAppender: um aplicativo de logback de caixa branca, onde as entradas de log são adicionadas em um public Listcampo que poderíamos usar para fazer nossas afirmações.

Aqui está um exemplo simples.

Classe Foo:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Foo {

    static final Logger LOGGER = LoggerFactory.getLogger(Foo .class);

    public void doThat() {
        LOGGER.info("start");
        //...
        LOGGER.info("finish");
    }
}

Classe FooTest:

import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;

public class FooTest {

    @Test
    void doThat() throws Exception {
        // get Logback Logger 
        Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);

        // create and start a ListAppender
        ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
        listAppender.start();

        // add the appender to the logger
        // addAppender is outdated now
        fooLogger.addAppender(listAppender);

        // call method under test
        Foo foo = new Foo();
        foo.doThat();

        // JUnit assertions
        List<ILoggingEvent> logsList = listAppender.list;
        assertEquals("start", logsList.get(0)
                                      .getMessage());
        assertEquals(Level.INFO, logsList.get(0)
                                         .getLevel());

        assertEquals("finish", logsList.get(1)
                                       .getMessage());
        assertEquals(Level.INFO, logsList.get(1)
                                         .getLevel());
    }
}

As asserções JUnit não parecem muito adaptadas para afirmar algumas propriedades específicas dos elementos da lista.
As bibliotecas de correspondências / asserções como AssertJ ou Hamcrest parecem melhores para isso:

Com AssertJ seria:

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
fonte
Como você para que o teste falhe se você registrar um erro?
Ghilteras
@ Gilteras Não sei ao certo. Registrar um erro não deve fazer com que seu teste falhe. O que você explica?
Davidxxx 13/12/18
Além disso, lembre-se de não fazer mocka classe que está sendo testada. Você precisa instanciá-lo com newoperador
Dmytro Chasovskyi
35

Muito obrigado por estas (surpreendentemente) respostas rápidas e úteis; eles me colocaram no caminho certo para a minha solução.

A base de código onde eu quero usar isso, usa java.util.logging como mecanismo de logger, e não me sinto em casa o suficiente nesses códigos para mudar completamente isso para log4j ou para interfaces / fachadas de logger. Mas com base nessas sugestões, eu 'hackeei' uma extensão de manipulador de mensagens e isso funciona como um deleite.

Um breve resumo segue. Estender 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, você pode armazenar o quanto quiser / desejar / precisar do LogRecord, ou empurrá-los todos para uma pilha até obter um estouro.

Na preparação para o teste de junção, você cria um java.util.logging.Loggere adiciona um novo LogHandlera ele:

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

A chamada para setUseParentHandlers()é silenciar os manipuladores normais, para que (nesta execução de teste de junção) não ocorra log desnecessário. Faça o que seu código em teste precisar para usar esse criador de logs, execute o teste e asserEquality:

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

(Obviamente, você moveria grande parte deste trabalho para um @Beforemétodo e faria várias outras melhorias, mas isso atrapalharia esta apresentação.)

Jon
fonte
16

Outra opção é zombar do Appender e verificar se a mensagem foi registrada neste appender. Exemplo para Log4j 1.2.xe mockito:

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

public class MyTest {

    private final Appender appender = mock(Appender.class);
    private final Logger logger = Logger.getRootLogger();

    @Before
    public void setup() {
        logger.addAppender(appender);
    }

    @Test
    public void test() {
        // when
        Logger.getLogger(MyTest.class).info("Test");

        // then
        ArgumentCaptor<LoggingEvent> argument = ArgumentCaptor.forClass(LoggingEvent.class);
        verify(appender).doAppend(argument.capture());
        assertEquals(Level.INFO, argument.getValue().getLevel());
        assertEquals("Test", argument.getValue().getMessage());
        assertEquals("MyTest", argument.getValue().getLoggerName());
    }

    @After
    public void cleanup() {
        logger.removeAppender(appender);
    }
}
Marcin
fonte
16

Efetivamente, você está testando um efeito colateral de uma classe dependente. Para teste de unidade, você precisa apenas verificar se

logger.info()

foi chamado com o parâmetro correto. Portanto, use uma estrutura de simulação para emular o criador de logs e isso permitirá que você teste o comportamento de sua própria classe.

djna
fonte
3
Como você zombou de um campo final estático particular, que a maioria dos criadores de logs é definida? Powermockito? Divirta-se ..
Stefano L
Stefano: Esse campo final foi inicializado de alguma forma, eu já vi várias abordagens para injetar Mocks, em vez da coisa real. Provavelmente requer que algum nível de design para testabilidade em primeiro lugar. blog.codecentric.de/en/2011/11/…
djna
Como Mehdi disse, possivelmente utilizando um manipulador apropriado pode ser suficiente,
djna
11

Zombar é uma opção aqui, embora seja difícil, porque os registradores geralmente são finais estáticos particulares - portanto, definir um logger simulado não seria fácil, ou exigiria modificação da classe em teste.

Você pode criar um Appender personalizado (ou como quer que seja chamado) e registrá-lo - por meio de um arquivo de configuração somente de teste ou em tempo de execução (de certa forma, dependente da estrutura de registro). E então você pode obter esse appender (estaticamente, se declarado no arquivo de configuração ou por sua referência atual, se você estiver conectando-o em tempo de execução) e verificar seu conteúdo.

Bozho
fonte
10

Inspirado na solução de @ RonaldBlaschke, eu vim com isso:

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

}

... o que permite que você faça:

@Rule public Log4JTester logTest = new Log4JTester();

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

Você provavelmente poderia usar o hamcrest de uma maneira mais inteligente, mas eu deixei assim.

fino
fonte
6

Para log4j2, a solução é um pouco diferente porque o AppenderSkeleton não está mais disponível. Além disso, o uso do Mockito ou de uma biblioteca semelhante para criar um Appender com um ArgumentCaptor não funcionará se você estiver esperando várias mensagens de log porque o MutableLogEvent é reutilizado em várias mensagens de log. A melhor solução que encontrei para o 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());
    }
}
Joseph
fonte
5

Como mencionado nos outros, você pode usar uma estrutura de zombaria. Para que isso funcione, é necessário expor o criador de logs em sua classe (embora eu prefira torná-lo privado em vez de criar um setter público).

A outra solução é criar um logger falso manualmente. Você precisa escrever o logger falso (mais código de fixação), mas neste caso eu preferiria a legibilidade aprimorada dos testes contra o código salvo da estrutura de simulação.

Eu faria algo assim:

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

Uau. Não sei por que isso foi tão difícil. Descobri que não consegui usar nenhum dos exemplos de código acima porque estava usando o log4j2 sobre o slf4j. Esta é a minha solução:

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

Aqui está o que eu fiz para o logback.

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

Em seguida, no pai da minha classe de teste de unidade de teste, criei um 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();
    }
}

Eu tenho um arquivo logback-test.xml definido em src / test / resources e adicionei um aplicativo de teste:

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

e adicionou este appender ao appender raiz:

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

Agora, nas minhas classes de teste que se estendem da minha classe de teste principal, posso obter o appender, registrar a última mensagem e verificar a mensagem, o nível e o lançamento.

ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
kfox
fonte
Não vejo onde o método getAppender está definido?!?
bioinfornatics
O getAppender é um método em um arquivo ch.qos.logback.classic.Logger
kfox
4

Para o dia 5 de junho (Júpiter), o OutputCaptureExtension da Spring é bastante útil. Está disponível desde o Spring Boot 2.2 e está disponível no artefato spring-boot-test .

Exemplo (extraído 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
fonte
Acredito que as instruções de log sejam diferentes getOut()ou getErr().
Ram
Esta é a resposta que eu estava procurando (embora a pergunta não esteja relacionada à bota de mola)!
helleye
3

Quanto a mim, você pode simplificar seu teste usando JUnitwith Mockito. Proponho a seguinte solução para isso:

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

É por isso que temos boa flexibilidade para testes com diferentes quantidades de mensagens

Dmytro Melnychuk
fonte
1
Para não repetir quase os mesmos blocos de código, quero acrescentar que quase 1to1 funciona para mim no Log4j2. Basta alterar as importações para "org.apache.logging.log4j.core", converter o registrador em "org.apache.logging.log4j.core.Logger", adicionar when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); e alterar 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
fonte
1
Isso funcionou para mim. A linha 'when (mockAppender.getName ()). ThenReturn ("MOCK")' 'não era necessária para mim.
Mayank Raghav
1

A API para Log4J2 é um pouco diferente. Além disso, você pode estar usando seu aplicativo assíncrono. Eu criei um appender bloqueado para isso:

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

Use-o assim:

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

Observe que no Log4J 2.x, a interface pública org.apache.logging.log4j.Loggernão inclui os métodos setAppender()e removeAppender().

Mas se você não estiver fazendo algo muito sofisticado, poderá convertê-lo na classe de implementação org.apache.logging.log4j.core.Logger, que expõe esses métodos.

Aqui está um exemplo com Mockito e AssertJ :

// Import the implementation class rather than the API interface
import org.apache.logging.log4j.core.Logger;
// Cast logger to implementation class to get access to setAppender/removeAppender
Logger log = (Logger) LogManager.getLogger(MyClassUnderTest.class);

// Set up the mock appender, stubbing some methods Log4J needs internally
Appender appender = mock(Appender.class);
when(appender.getName()).thenReturn("Mock Appender");
when(appender.isStarted()).thenReturn(true);

log.addAppender(appender);
try {
    new MyClassUnderTest().doSomethingThatShouldLogAnError();
} finally {
    log.removeAppender(appender);
}

// Verify that we got an error with the expected message
ArgumentCaptor<LogEvent> logEventCaptor = ArgumentCaptor.forClass(LogEvent.class);
verify(appender).append(logEventCaptor.capture());
LogEvent logEvent = logEventCaptor.getValue();
assertThat(logEvent.getLevel()).isEqualTo(Level.ERROR);
assertThat(logEvent.getMessage().getFormattedMessage()).contains(expectedErrorMessage);
David Moles
fonte
0

Outra idéia que vale a pena mencionar, embora seja um tópico mais antigo, é a criação de um produtor de CDI para injetar seu criador de logs para que a zombaria se torne fácil. (E também oferece a vantagem de não precisar mais declarar a "declaração do logger inteiro", mas isso é fora de tópico)

Exemplo:

Criando o logger para injetar:

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

O qualificador:

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

Usando o logger no seu código de produção:

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

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

Testando o logger no seu código de teste (dando um exemplo do easyMock):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

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

Usando o Jmockit (1.21), consegui escrever este teste simples. O teste garante que uma mensagem de erro específica seja chamada apenas uma 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
fonte
0

Zombar do Appender pode ajudar a capturar as linhas de log. Encontre exemplo em: 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
fonte
0

Use o código abaixo. Estou usando o mesmo código para meu teste de integração de primavera, em que estou usando o log back para log. Use o método assertJobIsScheduled para afirmar o texto impresso no 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);
        }
    }));
}
SUMIT
fonte
0

Há duas coisas que você pode estar tentando testar.

  • Quando há um evento de interesse do operador do meu programa, ele executa uma operação de registro apropriada, que pode informar o operador desse evento.
  • Quando meu programa executa uma operação de log, a mensagem de log que ele produz possui o texto correto.

Essas duas coisas são realmente diferentes e, portanto, podem ser testadas separadamente. No entanto, testar o segundo (o texto das mensagens) é tão problemático que eu recomendo não fazê-lo. Um teste de um texto de mensagem consistirá em verificar se uma sequência de texto (o texto esperado da mensagem) é a mesma ou pode ser derivada trivialmente da sequência de texto usada no seu código de registro.

  • Esses testes não testam a lógica do programa, apenas testam se um recurso (uma string) é equivalente a outro recurso.
  • Os testes são frágeis; até um pequeno ajuste na formatação de uma mensagem de log interrompe seus testes.
  • Os testes são incompatíveis com a internacionalização (tradução) da sua interface de log. Os testes assumem que há apenas um texto de mensagem possível e, portanto, apenas um idioma humano possível.

Observe que o código do seu programa (implementando alguma lógica de negócios, talvez) chamando diretamente a interface de log de texto é um design ruim (mas infelizmente muito comum). O código responsável pela lógica de negócios também está decidindo alguma política de log e o texto das mensagens de log. Ele combina a lógica comercial com o código da interface do usuário (sim, as mensagens de log fazem parte da interface do usuário do seu programa). Essas coisas devem ser separadas.

Portanto, recomendo que a lógica comercial não gere diretamente o texto das mensagens de log. Em vez disso, delegar para um objeto de log.

  • A classe do objeto de log deve fornecer uma API interna adequada, que seu objeto de negócios pode usar para expressar o evento que ocorreu usando objetos do seu modelo de domínio, não cadeias de texto.
  • A implementação da sua classe de log é responsável por produzir representações de texto desses objetos de domínio e renderizar uma descrição de texto adequada do evento, encaminhando a mensagem de texto para a estrutura de log de baixo nível (como JUL, log4j ou slf4j).
  • Sua lógica de negócios é responsável apenas por chamar os métodos corretos da API interna da sua classe de criador de logs, passando os objetos de domínio corretos, para descrever os eventos reais que ocorreram.
  • Sua classe de log concreta implementsan interface, que descreve a API interna que sua lógica de negócios pode usar.
  • Suas classes que implementam a lógica de negócios e devem executar o log têm uma referência ao objeto de log ao qual delegar. A classe da referência é o resumo interface.
  • Use injeção de dependência para configurar a referência para o criador de logs.

Em seguida, você pode testar se suas classes de lógica de negócios informam corretamente a interface de log sobre eventos, criando um criador de log simulado, que implementa a API de log interno, e usando a injeção de dependência na fase de configuração do seu teste.

Como isso:

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

O que eu fiz se tudo que eu quero fazer é ver que alguma string foi registrada (em vez de verificar as instruções de log exatas que são muito quebradiças) é redirecionar o StdOut para um buffer, fazer a contém e redefinir o 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
fonte
1
Eu tentei isso com java.util.logging(embora eu tenha usado System.setErr(new PrintStream(buffer));, porque ele registra no stderr), mas não funciona (o buffer permanece vazio). se eu usá- System.err.println("foo")lo diretamente, ele funciona, portanto, presumo que o sistema de registro mantenha sua própria referência do fluxo de saída, do qual é retirado System.err; portanto, minha chamada para System.setErr(..)não tem efeito na saída do registro, como ocorre após a inicialização do sistema de registro.
hoijui 22/05/19
0

Respondi a uma pergunta semelhante para log4j, veja como posso testar com junit que um aviso foi registrado com log4

Isso é mais recente e exemplo com o Log4j2 (testado com 2.11.2) e 5 de junho;

    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 as seguintes dependências do 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
fonte
Eu tentei isso e recebi um erro dentro do método de instalação na linha loggerConfig = configuration.getLoggerConfig (logger.getName ()); O erro é que não é possível acessar o arquivo de classe org.apache.logging.log4j.spi.LoggerContextShutdownEnabled do arquivo org.apache.logging.log4j.spi.LoggerContextShutdownEnabled não encontrado
carlos palma
Revisei o código e fiz algumas pequenas alterações, mas funcionou para mim. Sugiro que você verifique as dependências e verifique se todas as importações estão corretas
Haim Raman
Olá, Haim. Acabei implementando a solução de logback ... mas acho que você está certo, para implementar aquela que eu precisava limpar uma importação que fiz de outra versão do log4j.
carlos palma
-1

Se você estiver usando o log4j2, a solução de https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ me permitiu afirmar que as mensagens foram registradas.

A solução é assim:

  • Definir um aplicativo log4j como uma regra 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 um teste que use sua regra 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"));
        }
    }

Não se esqueça de ter o log4j2.xml como parte do src / test / resources

Greg7000
fonte