Como fazer uma declaração JUnit em uma mensagem em um logger

Eu tenho alguns códigos em teste que chama um registrador Java para relatar seu status. No código de teste JUnit, gostaria de verificar se a input de log correta foi feita neste logger. 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 poderia ser feito com um registrador (ou manipulador, ou formatador) especialmente adaptado, mas eu preferiria reutilizar uma solução que já existe. (E, para ser honesto, não está claro para mim como chegar ao logRegistro de um logger, mas suponha que isso seja possível.)

Eu precisei disso várias vezes também. Eu juntei uma pequena amostra abaixo, que você gostaria de ajustar às suas necessidades. Basicamente, você cria seu próprio Appender e o adiciona ao logger desejado. Se você quiser coletar tudo, o criador de logs raiz é um bom lugar para começar, mas você pode usar um mais específico, se quiser. Não se esqueça de remover o Appender quando terminar, caso contrário você poderá criar um memory leaks. Abaixo eu fiz isso dentro do teste, mas setUp ou tearDown e tearDown ou @After podem ser lugares melhores, dependendo de suas necessidades.

Além disso, a implementação abaixo coleta tudo em uma List na memory. Se você estiver registrando muito, talvez considere adicionar um filtro para eliminar inputs chatas ou gravar o log em um arquivo temporário em disco (Dica: LoggingEvent é Serializable , portanto, você deve ser capaz de apenas serializar os objects de evento, se 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 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 log = new ArrayList(); @Override public boolean requiresLayout() { return false; } @Override protected void append(final LoggingEvent loggingEvent) { log.add(loggingEvent); } @Override public void close() { } public List getLog() { return new ArrayList(log); } } 

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

A base de código era eu quero usar isso, usa java.util.logging como seu mecanismo logger, e eu não me sinto em casa o suficiente nesses códigos para alterar completamente isso para log4j ou para interfaces / fachadas logger. Mas com base nessas sugestões, eu ‘hackearei’ uma extensão de analista e isso funciona como um tratamento.

Um breve resumo segue. Estenda 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 / querer / precisar do LogRecord , ou empurrá-los todos em uma pilha até obter um estouro.

Na preparação para o junit-test, você cria um java.util.logging.Logger e adiciona um novo LogHandler a 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() é para silenciar os manipuladores normais, para que (para esta execução de teste de junit) não ocorra um registro desnecessário. Faça o que seu código sob teste precisa para usar este logger, execute o teste e assertEquality:

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

(Naturalmente, você moveria grande parte desse trabalho para um método @Before e faria outras melhorias, mas isso traria essa apresentação.)

Efetivamente, você está testando um efeito colateral de uma class dependente. Para testes unitários, você precisa apenas verificar

logger.info()

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

Zombar é uma opção aqui, embora seja difícil, porque os registradores geralmente são finais estáticos privados – portanto, configurar um registrador simulado não seria fácil ou exigiria a modificação da class em teste.

Você pode criar um Appender personalizado (ou o que for chamado) e registrá-lo – por meio de um arquivo de configuração somente de teste ou de tempo de execução (de certo modo, dependente da estrutura de criação de log). E, em seguida, você pode obter esse anexador (seja estaticamente, se declarado no arquivo de configuração ou por sua referência atual, se estiver conectando-o em tempo de execução) e verificar seu conteúdo.

Outra opção é simular o Appender e verificar se a mensagem foi registrada neste appender. Exemplo para o 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 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); } } 

Aqui está o que eu fiz para o logback.

Eu criei uma class TestAppender:

 public class TestAppender extends AppenderBase { private Stack events = new Stack(); @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 class de teste de unidade testng eu 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 appender de teste:

   %m%n   

e adicionou este appender ao appender raiz:

      

Agora, nas minhas classs de teste que se estendem da minha class de teste pai, posso obter o anexador e obter a última mensagem registrada e verificar a mensagem, o nível, o lance.

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

Inspirado pela solução de @RonaldBlaschke, eu criei isto:

 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 matcher) { for(LoggingEvent event : appender.events) { if(matcher.matches(event.getMessage())) { return; } } fail("No event matches " + matcher); } private static class TestAppender extends AppenderSkeleton { List events = new ArrayList(); @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 usá-lo de maneira mais inteligente, mas deixei isso de lado.

Como mencionado pelos outros, você poderia usar uma estrutura de zombaria. Para isto fazer o trabalho você tem que expor o logger em sua class (embora eu provavelmente preferiria torná-lo pacote privado em vez de criar um setter público).

A outra solução é criar um logger falso à mão. Você tem que escrever o logger falso (mais código de fixture), mas neste caso eu preferiria a melhor legibilidade dos testes contra o código salvo do framework de mocking.

Eu faria algo assim:

 class FakeLogger implements ILogger { public List infos = new ArrayList(); public List errors = new ArrayList(); 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()); } } 

Quanto a mim, você pode simplificar seu teste usando o JUnit com o 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 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 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

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 biblioteca semelhante para criar um Appender com um ArgumentCaptor não funcionará se você estiver esperando várias mensagens de log, pois 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 message = new ArrayList<>(); protected MockedAppender() { super("MockedAppender", null, null); } @Override public void append(LogEvent event) { message.add(event.getMessage().getFormattedMessage()); } } 

Uau. Não tenho certeza porque 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 é minha solução:

 public class SpecialLogServiceTest { @Mock private Appender appender; @Captor private ArgumentCaptor 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)); } } 

Outra ideia que vale a pena mencionar, embora seja um tópico mais antigo, é criar um produtor de CDI para injetar seu logger, para que o escárnio se torne fácil. (E também dá a vantagem de não ter mais que declarar a “declaração do registrador inteiro”, mas isso é fora do 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(); } 

Usando o Jmockit (1.21) eu consegui escrever este teste simples. O teste garante que uma mensagem específica de ERRO 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. } 

Zombando do Appender pode ajudar a capturar as linhas de log. Encontre a amostra 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 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"))); } 

Use o código abaixo. Eu estou usando o mesmo código para o meu teste de integração de mola, onde estou usando o log de volta para o registro. Use o método assertJobIsScheduled para declarar 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); } })); } 

Se você estiver usando java.util.logging.Logger este artigo pode ser muito útil, ele cria um novo manipulador e faz afirmações no log de saída: http://octodecillion.com/blog/jmockit-test-logging/

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

  • Quando há um evento de interesse para o operador do meu programa, meu programa 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 produz tem o texto correto.

Essas duas coisas são realmente coisas diferentes e, portanto, poderiam ser testadas separadamente. No entanto, testar o segundo (o texto das mensagens) é tão problemático que não recomendo fazê-lo. Um teste de um texto de mensagem consistirá, em última análise, em verificar se uma cadeia de texto (o texto da mensagem esperada) é igual ou pode ser derivada de forma trivial da cadeia de texto utilizada no seu código de registro.

  • Esses testes não testam a lógica do programa, eles apenas testam que um recurso (uma string) é equivalente a outro recurso.
  • Os testes são frágeis; mesmo um pequeno ajuste na formatação de uma mensagem de log quebra seus testes.
  • Os testes são incompatíveis com a internacionalização (tradução) da sua interface de registro. Os testes pressupõem que existe apenas um texto de mensagem possível e, portanto, apenas uma linguagem humana possível.

Note que ter seu código de programa (implementando alguma lógica de negócios, talvez) chamando diretamente a interface de log de texto é um design pobre (mas infelizmente muito comum). O código que é responsável pela lógica de negócios também está decidindo algumas políticas de log e o texto das mensagens de log. Ele mistura a lógica de negócios 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 de negócios não gere diretamente o texto das mensagens de log. Em vez disso, delegue para um object de log.

  • A class do object de log deve fornecer uma API interna adequada, que seu object de negócios pode usar para expressar o evento que ocorreu usando objects do modelo de domínio, não seqüências de texto.
  • A implementação de sua class de log é responsável por produzir representações de texto desses objects de domínio e renderizar uma descrição de texto adequada do evento, em seguida, encaminhar essa 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 methods corretos da API interna de sua class de agente de log, passando os objects de domínio corretos, para descrever os events reais que ocorreram.
  • Sua class de logging concreta implements uma interface , que descreve a API interna que sua lógica de negócios pode usar.
  • Sua class (es) que implementa a lógica de negócios e deve executar o logging tem uma referência ao object de log a ser delegado. A class da referência é a interface abstrata.
  • Use a injeção de dependência para configurar a referência para o logger.

Você pode testar se as classs de lógica de negócios informam corretamente à interface de log sobre events, criando um logger simulado, que implementa a API de log interna e usando a injeção de dependência na fase de configuração do 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); } } 

O que eu fiz, se tudo que eu quero fazer é ver que alguma string foi registrada (ao invés de verificar declarações de log exatas que são muito frágeis) é redirect StdOut para um buffer, fazer um 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); 

A API do Log4J2 é um pouco diferente. Além disso, você pode estar usando seu anexador asynchronous. Eu criei um anexador travado para isso:

  public static class LatchedAppender extends AbstractAppender implements AutoCloseable { private final List 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 awaitMessages() throws InterruptedException { assertTrue(latch.await(10, TimeUnit.SECONDS)); return messages; } @Override public void close() { stop(); loggerConfig.removeAppender(this.getName()); } } 

Use assim:

  try (LatchedAppender appender = new LatchedAppender(ClassUnderTest.class, 1)) { ClassUnderTest.methodThatLogs(); List events = appender.awaitMessages(); assertEquals(1, events.size()); //more assertions here }//appender removed 

Aqui está uma solução de Logback simples e eficiente.
Não é necessário adicionar / criar nenhuma nova class.
Ele se baseia no ListAppender : um anexador de log-in de whitebox foram inputs de log adicionadas em um campo de public List que poderíamos usar para fazer nossas asserçõ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 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 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ência / asserção 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));