(Это продолжение моего первоначального вопроса: Пользовательское приложение Log4j2 в Maven Surefire: возможно ли повторное использование в разных потоках?)
Я пытаюсь выполнить модульное тестирование результатов любого журнала, поступающего из Log4j2, в моем приложении Spring через JUnit5. Чтобы помочь в этом, я создал расширение JUnit5, которое создает WriterAppender
перед каждым тестом, который я могу использовать для проверки любых зарегистрированных сообщений.
Maven используется вместе с плагином Surefire. По умолчанию Surefire разделяет несколько потоков для ускорения тестирования. Однако было отмечено, что каждое из этих тестовых приложений также будет содержать выходные данные других тестов, которые выполняются в то же время.
(Я на себе убедился в этом.)
Было предложено использовать RoutingAppender
, который создает ListAppender
на основе значения, хранящегося в ThreadContext
, что я и пытаюсь сделать.
Я просмотрел несколько сообщений, тем и статей о создании такой установки. Похоже, что лучший способ сделать это - использовать конфигурацию XML, а не делать это программно, поскольку я не уверен, что это концептуально соответствует тому, что предпринимается.
Я попробовал следующую конфигурацию XML (и ее варианты):
log4j2.xml
<?xml version = "1.0" encoding = "UTF-8"?>
<Configuration status = "WARN" name = "RoutingForTests">
<Appenders>
<Routing name = "Routing">
<Routes pattern = "$${ctx:appenderName}">
<Route>
<List name = "$${ctx:appenderName}" />
</Route>
</Routes>
</Routing>
</Appenders>
<Loggers>
<Root level = "info">
<AppenderRef ref = "Routing" />
</Root>
</Loggers>
</Configuration>
ThreadContext
настраивается (правильно или неправильно) через зарегистрированное расширение JUnit5, например.
LoggerExtension.java
public class LoggerExtension implements BeforeEachCallback, AfterEachCallback {
// ...
@Override
public void beforeEach(ExtensionContext context) {
// ...
ThreadContext.put("appenderName", context.getUniqueId()); // I've even tried hardcoding this value
// ...
}
public void verifyMessage(String message) {
ListAppender appender = ListAppender.getListAppender(ThreadContext.get("appenderName")); // appender ends up null
// ...
}
}
Я также пробовал использовать жестко запрограммированное имя ListAppender
и ту же переменную поиска, но с одним $ вместо двух (но я думаю, что это приведет к пустому/нулевому значению, поскольку я не думаю, что ThreadContext
было правильно настройки на этом этапе, хотя я не могу быть уверен). Я также пытался установить key
вместо Route
, но безрезультатно.
Я даже пытался использовать ThreadId
из event
, но все попытки найти ListAppender
также не увенчались успехом.
Я предполагаю, что я либо что-то упускаю, либо неправильно понимаю, как это следует применять.
Я также пытался сделать это программно, но вышло что-то вроде путаницы.
Любая помощь будет оценена по достоинству, и я заранее извиняюсь, если это что-то простое, что я каким-то образом умудрился упустить из виду.
Заранее спасибо!
Источники:
Журналы являются важной частью любой системы, будь то сообщения, ориентированные на бизнес, или сообщения, ориентированные на поддержку/производительность. Как и любая другая подсистема, важно убедиться, что они работают правильно при развертывании. Последнее, что мне (или кому-либо еще) нужно, — это устранить проблему или иным образом предоставить ответы, которые можно получить из журналов, но их нет на месте. Береженого Бог бережет.
Я использовал Spring OutputCaptureExtension и отлично работает. Почему у вас это не работает?
Потому что я не хочу менять сигнатуру каждого модульного теста, который хочу проверить, и потому, что при этом фиксируются все выходные данные. Я хочу изолировать сами журналы и быть уверенным, что они поступают из самого Log4j. OutputCaptureExtension — мое последнее средство.
Приложение маршрутизации немного своеобразно, когда дело доходит до замены свойств:
pattern
элемента Routes
оцениваются дважды (во время настройки и каждый раз при регистрации события) и поэтому требуют экранирования $
, как вы это сделали,Route
оценивается только один раз, когда pattern
оценивается как новое значение. Поэтому не требуется экранировать $
.Вам следует использовать:
<Routing name = "Routing">
<Routes pattern = "$${ctx:appenderName}">
<Route>
<List name = "${ctx:appenderName}" />
</Route>
</Routes>
</Routing>
Чтобы получить приложение, специфичное для теста, используйте:
LoggerContext context = LoggerContext.getContext(false);
RoutingAppender routing = context.getConfiguration().getAppender("Routing");
AppenderControl appenderRef = routing.getAppenders().get(extensionContext.getUniqueId());
ListAppender list = (ListAppender) appenderRef.getAppender();
Примечание: приложения к спискам создаются лениво, если ни одно сообщение не было зарегистрировано, приложение не будет присутствовать.
Я попробовал использовать одиночный $ для имени приложения к списку, но это не сработало, поэтому я предполагаю, что проблема в том, как я пытаюсь получить приложение к списку. Сегодня я отсутствую у компьютера, но попробую завтра и тогда отчитаюсь. Еще раз спасибо за вашу помощь!
Приложение маршрутизации не регистрирует свои подприложения в основном контейнере приложения, поэтому ListAppender.getListAppender
не работает. Получить их можно только позвонив RoutingAppender.getAppenders()
.
Я обновил XML, и мне пришлось изменить последнюю строку на ListAppender listAppender = (ListAppender) routingAppender.getAppenders().get(ThreadContext.get("appenderName")).getAppender();
, но я получил сообщение об ошибке, сообщающее, что get(...) возвращает значение null. Я проверил карту, возвращенную getAppenders(), и она пуста. Есть ли что-нибудь еще, что я могу проверить? Я явно делаю что-то не так, но не знаю, что именно.
Я исправил код в ответе согласно вашему комментарию, и он должен работать. Обратите внимание, что appenderRef
будет null
, если ни одно сообщение не было зарегистрировано, поэтому вы можете добавить к этому утверждение.
Я проверил, что сообщение регистрируется, выдав сообщение на консоль сразу после вызова log.info
в тестируемом классе. Он все еще показывает, что list.getMessages()
пуст. Я также подтвердил, что сам ListAppender возвращается правильно, поскольку проверка его имени показывает правильное имя. Было бы нормально, если бы я связался с вами через DM здесь, чтобы поделиться более подробной информацией о настройке? Я уважаю, что вы, вероятно, очень заняты, поэтому я пойму, если это невозможно. В любом случае спасибо!
ListAppender
может работать двумя способами: если вы указали макет в своей конфигурации (например, <PatternLayout pattern = "%m"/>
, то ListAppender.getMessages()
будет непустым, в противном случае ListAppender.getEvents()
будет непустым. Напишите мне в Директ.
Ах, вот это было! getEvents()
содержало то, что мне было нужно. Я не знал о разнице между этим и getMessages()
. Огромное спасибо за вашу помощь!
Зачем вы ведете журналы модульного тестирования?