Як зробити твердження JUnit на повідомленні в реєстраторі


206

У мене є деякий тест на код, який вимагає реєстратора 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 від реєстратора, але припустимо, що це можливо.)

Відповіді:


142

Мені це також було потрібно кілька разів. Нижче я зібрав невеликий зразок, який ви хочете пристосувати до своїх потреб. В основному ви створюєте свій власний 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);
    }
}

4
Це чудово працює. Єдине вдосконалення, яке я хотів би зробити, - це зателефонувати logger.getAllAppenders(), потім перейти та зателефонувати appender.setThreshold(Level.OFF)кожному (і скинути їх, коли закінчите!). Це гарантує, що "погані" повідомлення, які ви намагаєтеся генерувати, не відображаються в тестових журналах і вироджують наступного розробника.
Кодерер

1
У Log4j 2.x трохи більш заплутаним , як вам потрібно , щоб створити плагін, подивіться на це: stackoverflow.com/questions/24205093 / ...
Paranza

1
Дякую за це Але якщо ви використовуєте LogBack, ви можете використовувати ListAppender<ILoggingEvent>замість створення власного користувальницького додатка.
sinujohn

2
але це не працює для slf4j! чи знаєте ви, як я можу це змінити, щоб він працював і з цим?
Шилан

3
@sd Якщо ви передасте Loggerна org.apache.logging.log4j.core.Logger(клас реалізації інтерфейсу), ви знову отримаєте доступ до нього setAppender()/removeAppender().
Девід

59

Ось просте та ефективне рішення для зворотного зв'язку.
Тут не потрібно додавати / створювати жодного нового класу.
Він покладається на 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
        // 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 не дуже адаптовані для затвердження певних властивостей елементів списку.
Бібліотеки відповідності / затвердження як 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));

Як зупинити тест, якщо він не введено помилки, якщо ви увійшли помилку?
Гілтерас

@Ghilteras Я не впевнений, що розумію. Помилка реєстрації не повинна зробити ваш тест невдалим. Що ти пояснюєш?
davidxxx

Також пам’ятайте, що не mockклас, який перевіряється. Вам потрібно зареєструвати це з newоператором
Дмитро Часовський,

35

Велике спасибі за ці (напрочуд) швидкі та корисні відповіді; вони поставили мене на правильний шлях для мого рішення.

Я хочу використовувати цю кодову базу, використовуючи java.util.logging як свій механізм реєстрації, і я не відчуваю себе вдома достатньою мірою, щоб повністю змінити це на log4j або на інтерфейси / фасади журналу. Але виходячи з цих пропозицій, я "зламав" розширення для юлхандлера, і це працює як частування.

Наступний короткий підсумок. Розширити 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, або натискати їх у стек, поки не отримаєте переповнення.

Під час підготовки до тесту з джуніту ви створюєте 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()- замовкнути нормальних обробників, щоб (для цього тестування з Джуніт-тесту) не траплялося зайвих журналів. Зробіть все, що потрібно для тестування коду, щоб використовувати цей реєстратор, запустіть тест і сформулюйте рівність:

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

(Звичайно, ви б перенесли велику частину цієї роботи в @Beforeметод і внесли різні вдосконалення, але це заплутало б цю презентацію.)


16

Іншим варіантом є знущання над програмою 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);
    }
}

16

Ви ефективно випробовуєте побічний ефект залежного класу. Для тестування одиниць потрібно лише переконатися в цьому

logger.info()

викликався з правильним параметром. Тому використовуйте глузливий фреймворк для емуляції реєстратора, що дозволить вам перевірити поведінку власного класу.


3
Як ви знущаєтесь із приватного статичного остаточного поля, яке визначається більшістю реєстраторів? Powermockito?
Стефано Л

Стефано: Це останнє поле якось ініціалізувалося, я бачив різні підходи до введення Макету, а не до реальної речі. Напевно, потрібен якийсь рівень дизайну для перевірки в першу чергу. blog.codecentric.de/en/2011/11/…
djna

Як сказав Мехді, можливо, використання відповідного обробника може бути достатньо,
djna

11

Знущання тут є варіантом, хоча це було б важко, тому що реєстратори, як правило, є приватним статичним фіналом - тому встановлення макетного реєстратора не було б шматочком пирога або вимагало б модифікації тестуваного класу.

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


10

Надихнувшись рішенням Рональда Блашке, я придумав таке:

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

Можливо, ви могли б змусити його використовувати хомкрест більш розумним способом, але я це покинув.


6

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

5

Як було сказано з інших, ви можете використовувати глузливі рамки. Для того, щоб зробити роботу, ви повинні виставити реєстратор у своєму класі (хоча я б вважав за краще зробити пакет приватним, а не створювати публічний сетер).

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

Я б зробив щось подібне:

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

5

Ого. Я не впевнений, чому це було так важко. Я виявив, що не зміг використати жодного з зразків коду вище, тому що я використовував 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));
  }
}

4

Ось що я зробив для зворотного зв'язку.

Я створив клас 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();
    }
}

Тоді в батьківському моєму тестовому класі тен-одиниці я створив метод:

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 / ресурси, і я додав тестовий додаток:

<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>

Тепер у моїх тестових класах, які поширюються з мого батьківського тестового класу, я можу отримати додаток і отримати останнє повідомлення, зафіксоване та перевірити повідомлення, рівень, який можна передати.

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

Я не бачу, де визначено метод getAppender?!?
bioinfornatics

getAppender - метод на ch.qos.logback.classic.Logger
kfox

4

Для Junit 5 (Юпітер) весна OutputCaptureExtension весни досить корисна. Він доступний з Spring Boot 2.2 і доступний в артефакті тестування весни .

Приклад (взято з javadoc):

@ExtendWith(OutputCaptureExtension.class)
class MyTest {
    @Test
    void test(CapturedOutput output) {
        System.out.println("ok");
        assertThat(output).contains("ok");
        System.err.println("error");
    }

    @AfterEach
    void after(CapturedOutput output) {
        assertThat(output.getOut()).contains("ok");
        assertThat(output.getErr()).contains("error");
    }
}

Я вважаю, що записи журналів не схожі на getOut()або getErr().
Рам

Це відповідь, яку я шукав (хоча питання не пов'язане з весняним завантаженням)!
helleye

3

Що стосується мене , ви можете спростити тестування за допомогою 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)
        );
    }
}

Тому ми маємо приємну гнучкість для тестів з різною кількістю повідомлень


1
Щоб не повторювати майже однакові кодові блоки, хочемо додати, що майже 1to1 працює для мене для Log4j2. Просто змінюючи імпорт на "org.apache.logging.log4j.core", передавайте реєстратор на "org.apache.logging.log4j.core.Logger", додайте when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); та змініть LoggingEvent -> LogEvent
Aliaksei Yatsau

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

1
Це працювало для мене. Рядок "коли (mockAppender.getName ()). Тоді повернення (" MOCK ")" мені не потрібен.
Mayank Raghav

1

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

1

Зауважте, що у Log4J 2.x, публічний інтерфейс org.apache.logging.log4j.Loggerне включає setAppender()та removeAppender()методи.

Але якщо ви не робите нічого надто фантазійного, ви повинні мати можливість передавати це класу впровадження org.apache.logging.log4j.core.Logger, який викриває ці методи.

Ось приклад із Mockito та AssertJ :

// Import the implementation class rather than the API interface
import org.apache.logging.log4j.core.Logger;
// Cast logger to implementation class to get access to setAppender/removeAppender
Logger log = (Logger) LogManager.getLogger(MyClassUnderTest.class);

// Set up the mock appender, stubbing some methods Log4J needs internally
Appender appender = mock(Appender.class);
when(appender.getName()).thenReturn("Mock Appender");
when(appender.isStarted()).thenReturn(true);

log.addAppender(appender);
try {
    new MyClassUnderTest().doSomethingThatShouldLogAnError();
} finally {
    log.removeAppender(appender);
}

// Verify that we got an error with the expected message
ArgumentCaptor<LogEvent> logEventCaptor = ArgumentCaptor.forClass(LogEvent.class);
verify(appender).append(logEventCaptor.capture());
LogEvent logEvent = logEventCaptor.getValue();
assertThat(logEvent.getLevel()).isEqualTo(Level.ERROR);
assertThat(logEvent.getMessage().getFormattedMessage()).contains(expectedErrorMessage);

0

Ще одна ідея, яку варто згадати, хоча це старіша тема, - це створити виробника 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();
}

0

Використовуючи 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.    
}

0

Знущання над додатком може допомогти захопити рядки журналу. Знайти зразок на сайті: 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")));
}

0

Скористайтеся наведеним нижче кодом. Я використовую той самий код для мого тесту на весняну інтеграцію, де я використовую журнал назад для ведення журналів. Використовуйте метод 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);
        }
    }));
}


0

Ви можете спробувати перевірити дві речі.

  • Коли в оператора моєї програми є подія, яка цікавить, чи виконує моя програма відповідну операцію реєстрації, яка може повідомити оператора про цю подію.
  • Коли моя програма виконує операцію ведення журналу, чи містить у своєму повідомленні журнал правильний текст.

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

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

Зауважте, що наявність вашого програмного коду (можливо, реалізація деякої логіки бізнесу) безпосередньо виклик інтерфейсу реєстрації тексту - це погана конструкція (але, на жаль, дуже поважна). Код, відповідальний за ділову логіку, також визначає певну політику ведення журналів та текст журнальних повідомлень. Він поєднує ділову логіку з кодом інтерфейсу користувача (так, повідомлення журналу є частиною інтерфейсу користувача вашої програми). Ці речі повинні бути окремими.

Тому я рекомендую, щоб бізнес-логіка не створювала безпосередньо текст повідомлення журналу. Замість цього делегуйте його об'єкту реєстрації.

  • Клас об'єкта реєстрації повинен забезпечувати відповідний внутрішній API, який ваш бізнес-об’єкт може використовувати для вираження події, що сталася, використовуючи об’єкти вашої моделі домену, а не текстові рядки.
  • Реалізація класу журналу відповідає за створення текстових зображень цих об’єктів домену та надання відповідного текстового опису події, а потім пересилання цього текстового повідомлення в рамку журналу низького рівня (наприклад, JUL, log4j або slf4j).
  • Ваша бізнес-логіка відповідає лише за виклик правильних методів внутрішнього API вашого класу реєстратора, передачу правильних об'єктів домену, для опису фактичних подій, що сталися.
  • Ваш конкретний клас реєстрації implementsan interface, який описує внутрішній 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);
    }
 }

0

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

1
Я спробував це java.util.logging(хоч я і використовував System.setErr(new PrintStream(buffer));, тому що він реєструється у stderr), але це не працює (буфер залишається порожнім). якщо я використовую System.err.println("foo")безпосередньо, це працює, тому я припускаю, що система реєстрації зберігає власне посилання на вихідний потік, з якого він береться System.err, тому мій виклик System.setErr(..)не впливає на вихід журналу, як це відбувається після запуску системи журналу.
hoijui

0

Я відповів на подібне запитання для log4j див як можна-я-тестую-з-junit-що-попередження-був-входив-з-log4

Це новіше і є прикладом з Log4j2 (тестовано з 2.11.2) та 5 липня;

    package com.whatever.log;

    import org.apache.logging.log4j.Level;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.core.Logger;
    import org.apache.logging.log4j.core.*;
    import org.apache.logging.log4j.core.appender.AbstractAppender;
    import org.apache.logging.log4j.core.config.Configuration;
    import org.apache.logging.log4j.core.config.LoggerConfig;
    import org.apache.logging.log4j.core.config.plugins.Plugin;
    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
    import org.apache.logging.log4j.core.config.plugins.PluginElement;
    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;

    import java.util.ArrayList;
    import java.util.List;
    import static org.junit.Assert.*;

class TestLogger {

    private TestAppender testAppender;
    private LoggerConfig loggerConfig;
    private final Logger logger = (Logger)
            LogManager.getLogger(ClassUnderTest.class);

    @Test
    @DisplayName("Test Log Junit5 and log4j2")
    void test() {
        ClassUnderTest.logMessage();
        final LogEvent loggingEvent = testAppender.events.get(0);
        //asset equals 1 because log level is info, change it to debug and
        //the test will fail
        assertTrue(testAppender.events.size()==1,"Unexpected empty log");
        assertEquals(Level.INFO,loggingEvent.getLevel(),"Unexpected log level");
        assertEquals(loggingEvent.getMessage().toString()
                ,"Hello Test","Unexpected log message");
    }

    @BeforeEach
    private void setup() {
        testAppender = new TestAppender("TestAppender", null);

        final LoggerContext context = logger.getContext();
        final Configuration configuration = context.getConfiguration();

        loggerConfig = configuration.getLoggerConfig(logger.getName());
        loggerConfig.setLevel(Level.INFO);
        loggerConfig.addAppender(testAppender,Level.INFO,null);
        testAppender.start();
        context.updateLoggers();
    }

    @AfterEach
    void after(){
        testAppender.stop();
        loggerConfig.removeAppender("TestAppender");
        final LoggerContext context = logger.getContext();
        context.updateLoggers();
    }

    @Plugin( name = "TestAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
    static class TestAppender extends AbstractAppender {

        List<LogEvent> events = new ArrayList();

        protected TestAppender(String name, Filter filter) {
            super(name, filter, null);
        }

        @PluginFactory
        public static TestAppender createAppender(
                @PluginAttribute("name") String name,
                @PluginElement("Filter") Filter filter) {
            return new TestAppender(name, filter);
        }

        @Override
        public void append(LogEvent event) {
            events.add(event);
        }
    }

    static class ClassUnderTest {
        private static final Logger LOGGER =  (Logger) LogManager.getLogger(ClassUnderTest.class);
        public static void logMessage(){
            LOGGER.info("Hello Test");
            LOGGER.debug("Hello Test");
        }
    }
}

Використовуючи наступні основні залежності

 <dependency>
 <artifactId>log4j-core</artifactId>
  <packaging>jar</packaging>
  <version>2.11.2</version>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>

Я спробував це і отримав помилку всередині методу настройки в рядку loggerConfig = configuration.getLoggerConfig (logger.getName ()); Помилка не може отримати доступ до org.apache.logging.log4j.spi.LoggerContextShutdownEnabled файл класу для org.apache.logging.log4j.spi.LoggerContextShutdownEnabled не знайдено
carlos palma

Я переглянув код і вніс деякі незначні зміни, але він працював на мене. Я пропоную вам перевірити залежності та переконатись у правильності всіх імпортів
Хаїм Раман

Привіт, там, Хаїме. Я в кінцевому підсумку застосував рішення для зворотного зв'язку ... але, я думаю, ти маєш рацію, щоб реалізувати той, що мені довелося очистити імпорт, який я зробив з іншої версії log4j.
carlos palma

-1

Якщо ви використовуєте 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 / ресурси

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