Как сделать утверждение JUnit в сообщении в журнале

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

Ответ 1

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

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

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

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

Ответ 2

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

Кроме того, реализация ниже собирает все в List в памяти. Если вы регистрируетесь много, вы можете подумать о добавлении фильтра для удаления скучных записей или записать журнал во временный файл на диске (подсказка: LoggingEvent is 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);
    }
}

Ответ 3

Вот простое и эффективное решение Logback.
Не требует добавления/создания нового класса.
Он опирается на ListAppender: приложение для входа в систему whitebox, где записи журнала добавляются в поле 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");
    }
}

Класс 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 не очень приспособлены для утверждения некоторых специфических свойств элементов списка.
Для этого лучше подходят библиотеки Matcher/Assertion, такие как 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));

Ответ 4

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

logger.info()

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

Ответ 5

Другим вариантом является mock 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);
    }
}

Ответ 6

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

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

Ответ 7

Вдохновленный решением @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 более умным способом, но я оставил его на этом.

Ответ 8

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

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

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

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

Ответ 9

Вот что я сделал для 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 unit test я создал метод:

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>

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

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

Ответ 10

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

Ответ 11

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

Ответ 12

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

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

Ответ 13

API для Log4J2 немного отличается. Также вы можете использовать свой асинхронный 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

Ответ 14

Еще одна идея, которая стоит упомянуть, хотя это более старая тема, заключается в создании производителя 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();
}

Ответ 15

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

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

Ответ 16

Отказывание 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")));
}

Ответ 17

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

Ответ 18

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

Ответ 19

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

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

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

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

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

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

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

Затем вы можете проверить, что ваши бизнес-логические классы правильно сообщают интерфейсу ведения журналов о событиях, создавая mock logger, который реализует внутренний 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);
    }
 }

Ответ 20

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

Ответ 21

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

Ответ 22

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