Чи можна якось перехопити реєстрацію (SLF4J + logback) і отримати InputStream
(або щось інше, що читається) за допомогою тесту JUnit ...?
Чи можна якось перехопити реєстрацію (SLF4J + logback) і отримати InputStream
(або щось інше, що читається) за допомогою тесту JUnit ...?
Відповіді:
Ви можете створити власний додаток
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
якщо ви не отримуєте жодних результатів - міркування див. У розділі коментарів.
events
після кожного виконання тесту.
sample0.xml
. Не забудьте змінити додаток до вашої реалізації
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));
Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);
. Я використовую LoggerFactory
of org.slf4j.LoggerFactory
і Logger
ofch.qos.logback.classic.Logger
SLF4J
це рішення, в підсумку буде піднято SLF4J: Class path contains multiple SLF4J bindings.
попередження, оскільки у вас є і SLF4J, і logback.classic
Ви можете використовувати 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();
}
}
slf4j-test
пакету lidalia можна знайти тут: github.com/jaegertracing/jaeger-client-java/pull/378/files
Простим рішенням може бути знущання над додатком за допомогою Mockito (наприклад)
@Slf4j
class MyClass {
public void doSomething() {
log.info("I'm on it!");
}
}
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
вказавши, чи відповідає значення очікуваному.
Незважаючи на те, що створення користувальницького додатка для зворотного зв’язку є хорошим рішенням, це лише перший крок, ви врешті-решт закінчите розробкою / винаходом slf4j-test , а якщо підете трохи далі: spf4j-slf4j-test або іншими фреймворками, яких я не маю ще не знаю.
Врешті-решт вам доведеться турбуватися про те, скільки подій ви зберігаєте в пам'яті, провалите модульні тести, коли помилка реєструється (і не стверджується), зробіть журнали налагодження доступними при невдалому тестуванні тощо ...
Застереження: Я автор spf4j-slf4j-test, я написав цей серверний сервіс для кращого тестування spf4j , що є гарним місцем для пошуку прикладів використання spf4j-slf4j-test. Однією з головних переваг, яких я досягнув, було зменшення продуктивності збірки (яка обмежена Тревісом), при цьому маючи всі необхідні деталі, коли трапляється несправність.
Я б порекомендував просту багаторазову реалізацію шпигуна, яку можна включити в тест як правило 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()
(або інші, нестандартні методи), щоб перевірити зареєстровані події.
import ch.qos.logback.classic.Logger;
замість того, щоб import org.slf4j.LoggerFactory;
інакше addAppender()
недоступно. Мені знадобився час, щоб це зрозуміти.
before()
і after()
ніколи до нього не дійшов, отже додаток ніколи не створюється / не додається, а UnexpectedTestError спрацьовує. Будь-які ідеї, що я роблю не так? Чи потрібно правило поміщати в певний пакет? Також, будь ласка, додайте до вашої відповіді розділ імпорту, оскільки деякі об’єкти / інтерфейси мають неоднозначні назви.
У мене були проблеми при тестуванні журналу, наприклад: 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 .
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
ILoggingEvent
замістьLoggingEvent
. Це те, що мені вдалося.