Как сделать JUnit assert для сообщения в логгере


206

У меня есть тестируемый код, который вызывает регистратор Java, чтобы сообщить о своем состоянии. В тестовом коде JUnit я хотел бы убедиться, что в этом логгере была сделана правильная запись в журнале. Что-то вроде следующего:

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

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

Я полагаю, что это можно сделать с помощью специально адаптированного регистратора (или обработчика, или форматера), но я бы предпочел повторно использовать решение, которое уже существует. (И, если честно, мне не ясно, как получить logRecord от регистратора, но предположим, что это возможно.)

Ответы:


142

Мне это тоже нужно было несколько раз. Ниже я собрал небольшой образец, который вы бы хотели адаптировать к вашим потребностям. По сути, вы создаете свой собственный Appenderи добавляете его в нужный вам регистратор. Если вы хотите собрать все, корневой журнал - хорошее место для начала, но вы можете использовать более конкретный, если хотите. Не забудьте удалить Appender, когда вы закончите, иначе вы можете создать утечку памяти. Ниже я сделал это в рамках теста, но setUpили @Beforeи, tearDownили, @Afterможет быть, лучше, в зависимости от ваших потребностей.

Кроме того, реализация ниже собирает все Listв памяти. Если вы ведете много журналов, вы можете рассмотреть возможность добавления фильтра для удаления скучных записей или записи журнала во временный файл на диске (подсказка: LoggingEventесть Serializable, так что вы можете просто сериализовать объекты событий, если ваше сообщение журнала является.)

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

4
Это прекрасно работает. Единственное улучшение, которое я хотел бы сделать, - это позвонить logger.getAllAppenders(), затем пройти и позвонить appender.setThreshold(Level.OFF)каждому (и сбросить их, когда вы закончите!). Это гарантирует, что «плохие» сообщения, которые вы пытаетесь сгенерировать, не появятся в журналах тестов и не пугает следующего разработчика.
Coderer

1
В Log4j 2.x немного более запутанный, так как вам нужно создать плагин, посмотрите на это: stackoverflow.com/questions/24205093/…
paranza

1
Спасибо за это. Но если вы используете LogBack, вы можете использовать ListAppender<ILoggingEvent>вместо создания своего собственного приложения.
sinujohn

2
но это не работает для slf4j! Вы знаете, как я могу изменить это, чтобы работать с этим также?
Шилан

3
@sd Если вы приведете Loggerк org.apache.logging.log4j.core.Logger(класс реализации для интерфейса), вы получите доступ setAppender()/removeAppender()снова.
Дэвид

60

Вот простое и эффективное решение Logback.
Не требует добавления / создания нового класса.
Он опирается на ListAppender: приложение для входа в систему «белого ящика», где записи журнала добавляются в public Listполе, которое мы могли бы использовать для подтверждения.

Вот простой пример.

Класс фу:

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

Класс 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());
    }
}

Утверждения JUnit не очень приспособлены для утверждения некоторых специфических свойств элементов списка.
Библиотеки соответствия / утверждения как AssertJ или Hamcrest выглядят лучше для этого:

С AssertJ это будет:

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

Как остановить тест от провала, если вы регистрируете ошибку?
Гильтерас

@Ghilteras Я не уверен, чтобы понять. Регистрация ошибки не должна сделать ваш тест неудачным. Что вы объясняете?
davidxxx

Кроме того, помните, чтобы не mockкласс, который тестируется. Вам нужно создать экземпляр с newоператором
Дмитрий Часовский

35

Большое спасибо за эти (удивительно) быстрые и полезные ответы; они поставили меня на правильный путь для моего решения.

Кодовая база, в которой я хочу использовать это, использует java.util.logging в качестве механизма ведения журнала, и я не чувствую себя как дома в этих кодах, чтобы полностью изменить это на log4j или на интерфейсы / фасады регистратора. Но, основываясь на этих предложениях, я «взломал» расширение julhandler, и это работает как удовольствие.

Краткое резюме следует. Расширить 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(){}
}

Очевидно, вы можете хранить столько, сколько хотите / хотите / нужно от LogRecord , или помещать их все в стек, пока не получите переполнение.

При подготовке к тесту junit вы создаете java.util.logging.Loggerи добавляете 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);

Требование setUseParentHandlers()состоит в том, чтобы заставить замолчать обычные обработчики, чтобы (для этого запуска теста junit) не происходило ненужного ведения журнала. Сделайте все, что нужно вашему тестируемому коду, чтобы использовать этот регистратор, запустите тест и assertEquality:

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

(Конечно, вы бы переместили большую часть этой работы в @Beforeметод и внесли множество других улучшений, но это загромождает эту презентацию.)


16

Другой вариант - смоделировать Appender и проверить, было ли сообщение зарегистрировано для этого Appender. Пример для Log4j 1.2.x и 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);
    }
}

16

По сути, вы тестируете побочный эффект зависимого класса. Для модульного тестирования вам нужно только убедиться, что

logger.info()

был вызван с правильным параметром. Следовательно, используйте эмулирующую среду для эмуляции логгера, и это позволит вам проверить поведение вашего класса.


3
Как вы издевались над приватным статическим финальным полем, которое определяется большинством регистраторов? Powermockito? Веселитесь ..
Стефано Л

Стефано: Это последнее поле как-то инициализировано, я видел разные подходы к инъекциям Mocks, а не реальные вещи. Вероятно, требуется определенный уровень дизайна для тестируемости в первую очередь. blog.codecentric.de/en/2011/11/…
djna

Как сказал Мехди, возможно, достаточно использовать подходящего обработчика,
djna

11

Пересмешка - вариант здесь, хотя это было бы трудно, потому что регистраторы, как правило, являются частными static static final - так что установка фиктивного регистратора не будет легкой задачей или потребует модификации тестируемого класса.

Вы можете создать пользовательский Appender (или как он там называется) и зарегистрировать его - либо через файл конфигурации только для тестирования, либо во время выполнения (в зависимости от структуры ведения журнала). И затем вы можете получить этот appender (либо статически, если он объявлен в файле конфигурации, либо по его текущей ссылке, если вы подключаете его во время выполнения), и проверить его содержимое.


10

Вдохновленный решением @ RonaldBlaschke, я придумал это:

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

}

... который позволяет вам делать:

@Rule public Log4JTester logTest = new Log4JTester();

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

Вероятно, вы могли бы сделать так, чтобы он использовал хамкрест более умным способом, но я оставил это на этом.


6

Для log4j2 решение немного отличается, потому что AppenderSkeleton больше не доступен. Кроме того, использование Mockito или подобной библиотеки для создания Appender с ArgumentCaptor не будет работать, если вы ожидаете нескольких сообщений регистрации, поскольку MutableLogEvent повторно используется в нескольких сообщениях журнала. Лучшее решение, которое я нашел для 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());
    }
}

5

Как уже упоминалось от других, вы можете использовать насмешливую структуру. Чтобы это работало, вы должны выставить регистратор в своем классе (хотя я бы предпочел сделать его закрытым, а не создавать открытый сеттер).

Другое решение - создать фальшивый регистратор вручную. Вы должны написать фальшивый регистратор (больше кода фикстуры), но в этом случае я бы предпочел улучшенную читаемость тестов по сравнению с сохраненным кодом из фреймворка.

Я бы сделал что-то вроде этого:

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

5

Вот это да. Я не уверен, почему это было так сложно. Я обнаружил, что не смог использовать ни один из приведенных выше примеров кода, потому что я использовал log4j2 вместо slf4j. Это мое решение:

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

4

Вот что я сделал для входа.

Я создал класс 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();
    }
}

Затем в родительском классе тестового модуля я создал метод:

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

У меня есть файл logback-test.xml, определенный в src / test / resources, и я добавил тестового приложения:

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

и добавил этот appender к корневому appender:

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

Теперь в моих тестовых классах, которые выходят из моего родительского тестового класса, я могу получить appender и зарегистрировать последнее сообщение, а также проверить сообщение, уровень, throwable.

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

Я не вижу, где определяется метод getAppender?!?
биоинформатика

getAppender - это метод для ch.qos.logback.classic.Logger
kfox,

4

Для Junit 5 (Jupiter) SpringCaptureExtension весьма полезен. Он доступен начиная с Spring Boot 2.2 и доступен в тесте весенней загрузки артефакте .

Пример (взят из 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");
    }
}

Я считаю, что записи в журнале отличаются от getOut()или getErr().
Рам

Это ответ, который я искал (хотя вопрос не связан с весенней загрузкой)!
привет

3

Что касается меня , вы можете упростить тестирование с помощью JUnitс Mockito. Я предлагаю следующее решение для этого:

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

Вот почему у нас есть хорошая гибкость для тестов с различным количеством сообщений


1
Чтобы не повторять почти одинаковые блоки кода, хочу добавить, что у меня почти 1to1 работает для Log4j2. Просто изменив импорт на «org.apache.logging.log4j.core», when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); приведите регистратор на «org.apache.logging.log4j.core.Logger», добавьте и измените 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");
      }
    }));
  }
}

1
Это сработало для меня. Строка 'when (mockAppender.getName ()). ThenReturn ("MOCK") "мне не понадобилась.
Mayank Raghav

1

API для Log4J2 немного отличается. Также вы можете использовать его async appender. Я создал защелку для этого:

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

Используйте это так:

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

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

    }//appender removed

1

Обратите внимание , что в Log4J 2.x, открытый интерфейс org.apache.logging.log4j.Loggerне включает setAppender()и removeAppender()методы.

Но если вы не делаете ничего слишком сложного, вы должны иметь возможность привести его к классу реализации org.apache.logging.log4j.core.Logger, который предоставляет эти методы.

Вот пример с Mockito и 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);

0

Еще одна идея, о которой стоит упомянуть, хотя это и более старая тема, - это создание CDI-производителя для внедрения вашего логгера, чтобы сделать насмешку легкой. (И это также дает преимущество в том, что больше не нужно объявлять «весь оператор логгера», но это не по теме)

Пример:

Создание регистратора для внедрения:

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

Классификатор:

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

Используя регистратор в вашем производственном коде:

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

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

Тестирование регистратора в вашем тестовом коде (приведем пример easyMock):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

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

0

Используя Jmockit (1.21), я смог написать этот простой тест. Тест проверяет, что конкретное сообщение об ошибке вызывается только один раз.

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

0

Насмешка над Appender может помочь захватить строки журнала. Найдите образец на: 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")));
}

0

Используйте код ниже. Я использую тот же код для моего весеннего интеграционного теста, где я использую log back для регистрации. Используйте метод assertJobIsScheduled чтобы утверждать текст, напечатанный в журнале.

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


0

Есть две вещи, которые вы, возможно, пытаетесь проверить.

  • Когда есть событие, представляющее интерес для оператора моей программы, выполняет ли моя программа соответствующую операцию регистрации, которая может информировать оператора об этом событии.
  • Когда моя программа выполняет операцию регистрации, имеет ли сообщение, которое она производит, правильный текст.

Эти две вещи на самом деле разные вещи, и поэтому могут быть проверены отдельно. Тем не менее, тестирование второго (текст сообщения) настолько проблематично, что я рекомендую вообще не делать этого. В конечном итоге проверка текста сообщения будет состоять из проверки того, что одна текстовая строка (ожидаемый текст сообщения) совпадает или может быть тривиально получена из текстовой строки, используемой в коде регистрации.

  • Эти тесты вообще не проверяют логику программы, они только проверяют, что один ресурс (строка) эквивалентен другому ресурсу.
  • Испытания хрупкие; даже незначительная настройка форматирования сообщения журнала нарушает ваши тесты.
  • Тесты несовместимы с интернационализацией (переводом) вашего интерфейса регистрации. Тесты предполагают, что существует только один возможный текст сообщения и, следовательно, только один возможный человеческий язык.

Обратите внимание, что наличие в программном коде (возможно, реализующего некоторую бизнес-логику) прямого вызова интерфейса ведения текстового журнала - плохой дизайн (но, к сожалению, очень общий). Код, отвечающий за бизнес-логику, также определяет политику ведения журнала и текст сообщений журнала. Он смешивает бизнес-логику с кодом пользовательского интерфейса (да, сообщения журнала являются частью пользовательского интерфейса вашей программы). Эти вещи должны быть отдельными.

Поэтому я рекомендую, чтобы бизнес-логика напрямую не генерировала текст сообщений журнала. Вместо этого пусть он делегируется объекту регистрации.

  • Класс объекта ведения журнала должен предоставлять подходящий внутренний API, который ваш бизнес-объект может использовать для выражения события, которое произошло с использованием объектов вашей доменной модели, а не текстовых строк.
  • Реализация вашего класса ведения журналов отвечает за создание текстовых представлений этих объектов домена и отрисовку подходящего текстового описания события, а затем пересылку этого текстового сообщения в низкоуровневую структуру ведения журналов (например, JUL, log4j или slf4j).
  • Ваша бизнес-логика отвечает только за вызов правильных методов внутреннего API вашего класса регистратора, передачу правильных доменных объектов для описания реальных событий, которые произошли.
  • Ваш класс протоколирования бетон , который описывает внутренний API ваш бизнес - логика может использовать.implementsinterface
  • Ваш класс (классы), который реализует бизнес-логику и должен выполнять ведение журнала, имеет ссылку на объект ведения журнала для делегирования. Класс ссылки является абстрактным interface.
  • Используйте внедрение зависимости, чтобы установить ссылку на регистратор.

Затем вы можете проверить, правильно ли ваши классы бизнес-логики сообщают интерфейсу ведения журнала о событиях, создав фиктивный регистратор, который реализует внутренний API ведения журнала, и используя внедрение зависимостей на этапе настройки вашего теста.

Как это:

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

0

То, что я сделал, если все, что я хочу сделать, это увидеть, что какая-то строка была зарегистрирована (в отличие от проверки точных операторов журнала, которая слишком хрупкая), это перенаправить StdOut в буфер, создать содержимое, затем сбросить 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);

1
Я попробовал это с java.util.logging(хотя я использовал System.setErr(new PrintStream(buffer));, потому что он регистрирует в stderr), но это не работает (буфер остается пустым). если я использую System.err.println("foo")напрямую, это работает, поэтому я предполагаю, что система ведения журналов сохраняет собственную ссылку на выходной поток, из которого она получает System.err, поэтому мой вызов System.setErr(..)не влияет на вывод журнала, как это происходит после инициализации системы журналирования.
Hoijui

0

Я ответил на аналогичный вопрос для log4j: смотрите, как я могу протестировать с сообщением о том, что предупреждение было зарегистрировано с log4

Это новее и пример с Log4j2 (протестирован с 2.11.2) и 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");
        }
    }
}

Используя следующие 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>

Я попробовал это и получил ошибку в методе настройки в строке loggerConfig = configuration.getLoggerConfig (logger.getName ()); Ошибка не может получить доступ к файлу класса org.apache.logging.log4j.spi.LoggerContextShutdownEnabled для org.apache.logging.log4j.spi.LoggerContextShutdownEnabled не найден
Карлос Пальма

Я пересмотрел код и внес некоторые незначительные изменения, но он работал для меня. Я полагаю, что вы проверите зависимости и убедитесь, что все операции импорта выполнены правильно
Хаим Раман

Привет, Хаим. Я закончил тем, что реализовал решение для logback ... но я думаю, что вы правы, чтобы реализовать то, что мне пришлось очистить импорт, который я сделал из другой версии log4j.
Карлос Пальма

-1

Если вы используете log4j2, решение из https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ позволило мне утверждать, что сообщения были зарегистрированы.

Решение выглядит так:

  • Определите приложение log4j как правило 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();
        }
    }
  • Определите тест, который использует ваше правило 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"));
        }
    }

Не забудьте иметь log4j2.xml в составе src / test / resources

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.