Как сделать JUnit assert на сообщение в регистраторе
У меня есть некоторый код под тестом, который вызывает регистратор 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 от регистратора, но предположим, что это возможно.)
20 ответов:
мне это тоже было нужно несколько раз. Я собрал небольшой образец ниже, который вы хотели бы приспособить к вашим потребностям. В принципе, вы создаете свой собственный
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); } }
большое спасибо за эти (на удивление) быстрые и полезные ответы; они поставили меня на правильный путь для моего решения.
кодовая база была я хочу использовать это, использует java.утиль.ведение журнала как его механизм регистратора, и я не чувствую себя дома достаточно в этих кодах, чтобы полностью изменить это на 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-test) не происходило ненужного ведения журнала. Сделайте все, что ваш тестируемый код должен использовать этот регистратор, запустите тест и assertEquality:libraryUnderTest.setLogger(logger); methodUnderTest(true); // see original question. assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() ); }
(конечно, вы бы переместили большую часть этой работы в
@Before
метод и сделать различные другие улучшения, но это будет загромождать эту презентацию.)
фактически вы тестируете побочный эффект зависимого класса. Для модульного тестирования вам нужно только проверить, что
logger.info()
был вызван с правильным параметром. Поэтому используйте насмешливую структуру для эмуляции 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); } }
вот что я сделал для 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-тест.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(), "...");
вдохновленный решением @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 более умным способом, но я оставил его на этом.
Как уже упоминалось из других вы могли бы использовать насмешливую структуру. Для этого, чтобы сделать работу, вы должны выставить регистратор в своем классе (хотя я бы предпочел сделать его частным пакетом, а не создавать общедоступный сеттер).
другое решение - создать поддельный регистратор вручную. Вы должны написать поддельный регистратор (больше кода приспособления), но в этом случае я бы предпочел улучшенную читаемость тестов против сохраненного кода от издевательства рамки.
Я бы сделал что-то вроде этого:
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()); } }
Что касается меня, вы можете упростить свой тест с помощью
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) ); } }
вот почему у нас есть хороший гибкость для тестов с другое количество сообщений
Вау. Я не знаю, почему это было так трудно. Я обнаружил, что я был не в состоянии использовать любой из приведенных выше примеров кода, потому что я использовал 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)); } }
для 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()); } }
еще одна идея, о которой стоит упомянуть, хотя это более старая тема, - это создание производителя CDI для ввода вашего регистратора, чтобы насмешка стала легкой. (И это также дает преимущество в том, что больше не нужно объявлять "весь оператор 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/
есть две вещи, которые вы, возможно, пытается проверить.
- когда есть событие, представляющее интерес для оператора моей программы, выполняет ли моя программа соответствующую операцию регистрации, которая может сообщить оператору об этом событии.
- когда моя программа выполняет операцию ведения журнала, сообщение журнала, которое она создает, имеет правильный текст.
эти две вещи на самом деле разные вещи, и поэтому могут быть протестированы отдельно. Однако, тестирование второго (текст сообщений) настолько проблематично, что я рекомендую вообще не делать этого. Проверка текста сообщения в конечном итоге будет состоять из проверки того, что одна текстовая строка (ожидаемый текст сообщения) совпадает или может быть тривиально получена из текстовой строки, используемой в коде ведения журнала.
- эти тесты вообще не проверяют логику программы, они только проверяют, что один ресурс (строка) эквивалентен другому ресурсу.
- тесты неустойчивы; даже незначительная настройка форматирования сообщения журнала нарушает ваши тесты.
- тесты несовместимы с интернационализацией (переводом)вашего интерфейса регистрации.Тесты предполагают, что существует только один возможный текст сообщения и, следовательно, только один возможный человеческий язык.
обратите внимание, что наличие вашего программного кода (возможно, реализующего некоторую бизнес-логику), непосредственно вызывающего интерфейс текстового журнала, является плохим дизайном (но, к сожалению, очень commom). Код, который несет ответственность для бизнес-логики также решается некоторая политика ведения журнала и текст сообщений журнала. Он смешивает бизнес-логику с кодом пользовательского интерфейса (да, сообщения журнала являются частью пользовательского интерфейса вашей программы). Эти вещи должны быть разделены.
- класс объекта регистрации должен предоставлять подходящий внутренний API, который ваш бизнес-объект можно использовать для выражения события, которое произошло с использованием объектов модели домена, а не текстовых строк.
- реализация вашего класса ведения журнала отвечает за создание текстовых представлений этих объектов домена и отображение подходящего текстового описания события, а затем пересылку этого текстового сообщения в низкоуровневую структуру ведения журнала (например, JUL, log4j или slf4j).
- ваша бизнес-логика отвечает только за вызов правильных методов внутренний API вашего класса logger, передающий правильные объекты домена, чтобы описать фактические события, которые произошли.
- ваш конкретный класс регистрации
implements
aninterface
, который описывает внутренний 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); } }
что я сделал, если все, что я хочу сделать, это увидеть, что какая-то строка была зарегистрирована (в отличие от проверки точных операторов журнала, которые просто слишком хрупкие), чтобы перенаправить 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);
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
вот простое и эффективное решение Logback.
Он не требует чтобы добавить/создать новый класс.
Он полагается наListAppender
: приложение whitebox logback были добавлены записи журнала в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 не очень приспособлены для утверждения некоторых конкретные свойства элементов списка.
Библиотеки 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));