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

StackOverflow https://stackoverflow.com/questions/1827677

  •  11-09-2019
  •  | 
  •  

Вопрос

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

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

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

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

Это было полезно?

Решение 2

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

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

Далее следует краткое резюме.Расширять 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-теста) не происходило ненужного протоколирования.Делайте все, что требуется вашему тестируемому коду для использования этого регистратора, запустите тест и подтвердите качество:

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

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

Другие советы

Я тоже нуждался в этом несколько раз.Ниже я собрал небольшой образец, который вы могли бы приспособить к своим потребностям.По сути, вы создаете свой собственный 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);
    }
}

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

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

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

Самый нижний класс :

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

public class FooTest {

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

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

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

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

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

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

Утверждения JUnit звучат не очень приспособленными для утверждения некоторых конкретных свойств элементов списка.
Библиотеки сопоставления / утверждения, такие как 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));

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

logger.info()

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

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

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

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

Вдохновленный решением @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"));
}

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

Вот что я сделал для logback.

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

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

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>

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

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

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

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

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

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

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

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

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

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

Что касается меня, вы можете упростить свой тест, используя 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)
        );
    }
}

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

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

Пример:

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

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

Используя 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.    
}

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

Используйте приведенный ниже код.Я использую тот же код для моего весеннего интеграционного теста, где я использую 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);
        }
    }));
}

если вы используете java.util.logging.Logger эта статья может быть очень полезной, она создает новый обработчик и делает утверждения в выходных данных журнала:http://octodecillion.com/blog/jmockit-test-logging/

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

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

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

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

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

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

  • Класс объекта ведения журнала должен предоставлять подходящий внутренний API, который ваш бизнес-объект может использовать для выражения произошедшего события с использованием объектов вашей модели предметной области, а не текстовых строк.
  • Реализация вашего класса ведения журнала отвечает за создание текстовых представлений этих объектов домена и предоставление подходящего текстового описания события, а затем пересылку этого текстового сообщения в низкоуровневую платформу ведения журнала (такую как JUL, log4j или slf4j).
  • Ваша бизнес-логика отвечает только за вызов правильных методов внутреннего API вашего класса logger, передачу правильных объектов домена для описания фактически произошедших событий.
  • Ваш конкретный класс ведения журнала implements ан interface, который описывает внутренний API, который может использовать ваша бизнес-логика.
  • Ваш класс (ы), который реализует бизнес-логику и должен выполнять ведение журнала, имеет ссылку на объект ведения журнала, которому нужно делегировать.Класс ссылки - это абстрактный 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);
    }
 }

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

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

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

    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

Если вы используете 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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top