Як перехопити реєстрацію SLF4J (з реєстрацією) за допомогою тесту JUnit?


76

Чи можна якось перехопити реєстрацію (SLF4J + logback) і отримати InputStream(або щось інше, що читається) за допомогою тесту JUnit ...?

Відповіді:


40

Ви можете створити власний додаток

public class TestAppender extends AppenderBase<LoggingEvent> {
    static List<LoggingEvent> events = new ArrayList<>();
    
    @Override
    protected void append(LoggingEvent e) {
        events.add(e);
    }
}

та налаштуйте logback-test.xml, щоб використовувати його. Тепер ми можемо перевірити реєстрацію подій з нашого тесту:

@Test
public void test() {
    ...
    Assert.assertEquals(1, TestAppender.events.size());
    ...
}

ПРИМІТКА. Використовуйте, ILoggingEventякщо ви не отримуєте жодних результатів - міркування див. У розділі коментарів.


16
Зверніть увагу, якщо ви використовуєте logback classic + slf4j, вам потрібно використовувати ILoggingEventзамість LoggingEvent. Це те, що мені вдалося.
etech

6
@Evgeniy Dorofeev Не могли б ви показати, як налаштувати logback-test.xml?
hipokito

1
Я припускаю, вам потрібно очистити eventsпісля кожного виконання тесту.
Андрій Караїванський,

2
@hipokito Ви можете використовувати згаданий [тут] ( logback.qos.ch/manual/configuration.html ) у sample0.xml. Не забудьте змінити додаток до вашої реалізації
coding_idiot

@EvgeniyDorofeev ти можеш мені допомогти в цьому? stackoverflow.com/questions/48551083/…
Bhavya Arora

74

API Slf4j не забезпечує такого шляху, але Logback пропонує просте рішення.

Ви можете використовувати 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");
    }
}

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

Ви також можете використовувати бібліотеки Matcher / assertion як AssertJ або Hamcrest.

З AssertJ це було б:

import org.assertj.core.api.Assertions;

Assertions.assertThat(listAppender.list)
          .extracting(ILoggingEvent::getFormattedMessage, ILoggingEvent::getLevel)
          .containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));

2
Дуже дякую! Це саме те, що я шукав!
Олі,

5
Я отримую ClassCastException для Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);. Я використовую LoggerFactoryof org.slf4j.LoggerFactoryі Loggerofch.qos.logback.classic.Logger
Hiren

@Hiren З чим саме пов'язане повідомлення про помилку?
davidxxx

7
Важливо зазначити, що замість ILoggingEvent :: getMessage ви повинні використовувати ILoggingEvent :: getFormattedMessage Якщо ваш журнал містить значення параметра. Інакше ваше твердження не вдасться, оскільки значення буде відсутнє.
Роберт Мейсон,

4
якщо ви використовуєте SLF4Jце рішення, в підсумку буде піднято SLF4J: Class path contains multiple SLF4J bindings.попередження, оскільки у вас є і SLF4J, і logback.classic
Ghilteras

16

Ви можете використовувати slf4j-test з http://projects.lidalia.org.uk/slf4j-test/ . Він замінює всю реалізацію slf4j з реєстрацією на власну реалізацію api slf4j для тестів і надає api для затвердження щодо реєстрації подій.

приклад:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <configuration>
        <classpathDependencyExcludes>
          <classpathDependencyExcludes>ch.qos.logback:logback-classic</classpathDependencyExcludes>
        </classpathDependencyExcludes>
      </configuration>
    </plugin>
  </plugins>
</build>

public class Slf4jUser {

    private static final Logger logger = LoggerFactory.getLogger(Slf4jUser.class);

    public void aMethodThatLogs() {
        logger.info("Hello World!");
    }
}

public class Slf4jUserTest {

    Slf4jUser slf4jUser = new Slf4jUser();
    TestLogger logger = TestLoggerFactory.getTestLogger(Slf4jUser.class);

    @Test
    public void aMethodThatLogsLogsAsExpected() {
        slf4jUser.aMethodThatLogs();

        assertThat(logger.getLoggingEvents(), is(asList(info("Hello World!"))));
    }

    @After
    public void clearLoggers() {
        TestLoggerFactory.clear();
    }
}

Дякую за цю альтернативну відповідь! Це виглядає дуже корисно, і я цілком імовірно спробую цей підхід і в майбутньому! На жаль, я вже прийняв іншу відповідь, яка також є правильною.
carlspring 02.03.16

Повний приклад використання slf4j-testпакету lidalia можна знайти тут: github.com/jaegertracing/jaeger-client-java/pull/378/files
Debosmit Ray

1
Це рішення чудово працює, якщо ви не використовуєте Spring. Якщо ви використовуєте Spring, він видасть клас, який не знайдено, виняток (JoranConfigurator).
Jesus H

7

Простим рішенням може бути знущання над додатком за допомогою Mockito (наприклад)

MyClass.java

@Slf4j
class MyClass {
    public void doSomething() {
        log.info("I'm on it!");
    }
}

MyClassTest.java

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.verify;         

@RunWith(MockitoJUnitRunner.class)
public class MyClassTest {    

    @Mock private Appender<ILoggingEvent> mockAppender;
    private MyClass sut = new MyClass();    

    @Before
    public void setUp() {
        Logger logger = (Logger) LoggerFactory.getLogger(MyClass.class.getName());
        logger.addAppender(mockAppender);
    }

    @Test
    public void shouldLogInCaseOfError() {
        sut.doSomething();

        verify(mockAppender).doAppend(ArgumentMatchers.argThat(argument -> {
            assertThat(argument.getMessage(), containsString("I'm on it!"));
            assertThat(argument.getLevel(), is(Level.INFO));
            return true;
        }));

    }

}

ПРИМІТКА. Я використовую твердження, а не повернення, falseоскільки це полегшує читання коду та (можливої) помилки, але це не спрацює, якщо у вас є кілька перевірок. У цьому випадку вам потрібно повернути, booleanвказавши, чи відповідає значення очікуваному.


це працює, якщо я використовую анімації lombok.extern.slf4j, такі як Slf4j? як ви глузуєте або підглядаєте лісоруб, якщо він навіть не є об’єктом у моїх класах? тобто log.error використовується лише шляхом надання анотації Slf4j для мого класу ...
ennth

@ennth Це повинно працювати, тому що ви вводите макет із статичним методом LoggerFactory.getLogger (). addAppender (mockAppender). Що працює так само, коли ви створюєте реєстратор за допомогою Ломбока
snovelli

2
Маючи таку ж непрацюючу проблему. Що таке «імпорт» для класів Logger та LoggerFactory? Чому статичний імпорт перелічено, а інші ні?
Дірк Шумахер

5

Незважаючи на те, що створення користувальницького додатка для зворотного зв’язку є хорошим рішенням, це лише перший крок, ви врешті-решт закінчите розробкою / винаходом slf4j-test , а якщо підете трохи далі: spf4j-slf4j-test або іншими фреймворками, яких я не маю ще не знаю.

Врешті-решт вам доведеться турбуватися про те, скільки подій ви зберігаєте в пам'яті, провалите модульні тести, коли помилка реєструється (і не стверджується), зробіть журнали налагодження доступними при невдалому тестуванні тощо ...

Застереження: Я автор spf4j-slf4j-test, я написав цей серверний сервіс для кращого тестування spf4j , що є гарним місцем для пошуку прикладів використання spf4j-slf4j-test. Однією з головних переваг, яких я досягнув, було зменшення продуктивності збірки (яка обмежена Тревісом), при цьому маючи всі необхідні деталі, коли трапляється несправність.


4

Я б порекомендував просту багаторазову реалізацію шпигуна, яку можна включити в тест як правило JUnit:

public final class LogSpy extends ExternalResource {

    private Logger logger;
    private ListAppender<ILoggingEvent> appender;

    @Override
    protected void before() {
        appender = new ListAppender<>();
        logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); // cast from facade (SLF4J) to implementation class (logback)
        logger.addAppender(appender);
        appender.start();
    }

    @Override
    protected void after() {
        logger.detachAppender(appender);
    }

    public List<ILoggingEvent> getEvents() {
        if (appender == null) {
            throw new UnexpectedTestError("LogSpy needs to be annotated with @Rule");
        }
        return appender.list;
    }
}

У своєму тесті ви активували б шпигуна таким чином:

@Rule
public LogSpy log = new LogSpy();

Зателефонуйте log.getEvents()(або інші, нестандартні методи), щоб перевірити зареєстровані події.


2
Для того, щоб це працювало, вам потрібно import ch.qos.logback.classic.Logger;замість того, щоб import org.slf4j.LoggerFactory;інакше addAppender()недоступно. Мені знадобився час, щоб це зрозуміти.
Урс

Не працює у мене. Виглядає так, ніби правило застосовується неправильно: під час налагодження я знайшов before()і after()ніколи до нього не дійшов, отже додаток ніколи не створюється / не додається, а UnexpectedTestError спрацьовує. Будь-які ідеї, що я роблю не так? Чи потрібно правило поміщати в певний пакет? Також, будь ласка, додайте до вашої відповіді розділ імпорту, оскільки деякі об’єкти / інтерфейси мають неоднозначні назви.
Philzen

2

У мене були проблеми при тестуванні журналу, наприклад: LOGGER.error (повідомлення, виняток) .

Рішення, описане в http://projects.lidalia.org.uk/slf4j-test/, намагається також стверджувати щодо винятку, і відтворити стек непросто (і, на мій погляд, ні до чого).

Я вирішив таким чином:

import org.junit.Test;
import org.slf4j.Logger;
import uk.org.lidalia.slf4jext.LoggerFactory;
import uk.org.lidalia.slf4jtest.TestLogger;
import uk.org.lidalia.slf4jtest.TestLoggerFactory;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
import static uk.org.lidalia.slf4jext.Level.ERROR;
import static uk.org.lidalia.slf4jext.Level.INFO;


public class Slf4jLoggerTest {

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


    private void methodUnderTestInSomeClassInProductionCode() {
        LOGGER.info("info message");
        LOGGER.error("error message");
        LOGGER.error("error message with exception", new RuntimeException("this part is not tested"));
    }





    private static final TestLogger TEST_LOGGER = TestLoggerFactory.getTestLogger(Slf4jLoggerTest.class);

    @Test
    public void testForMethod() throws Exception {
        // when
        methodUnderTestInSomeClassInProductionCode();

        // then
        assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").contains(
                tuple(INFO, "info message"),
                tuple(ERROR, "error message"),
                tuple(ERROR, "error message with exception")
        );
    }

}

Це також має ту перевагу, що не залежить від бібліотеки збіжників Hamcrest .


2

З JUnit5 + AssertJ

private ListAppender<ILoggingEvent> logWatcher;

@BeforeEach
void setup() {
  this.logWatcher = new ListAppender<>();
  this.logWatcher.start();
  ((Logger) LoggerFactory.getLogger(MyClass.class)).addAppender(this.logWatcher);
}


@Test
void myMethod_logs2Messages() {

  ...
  int logSize = logWatcher.list.size();
  assertThat(logWatcher.list.get(logSize - 2).getFormattedMessage()).contains("EXPECTED MSG 1");
  assertThat(logWatcher.list.get(logSize - 1).getFormattedMessage()).contains("EXPECTED MSG 2");
}

зараховує: відповідь @ davidxxx. Детальніше див. Його import ch.qos.logback...: https://stackoverflow.com/a/52229629/601844

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.