Как сделать JUnit assert для сообщения в логгере - PullRequest
156 голосов
/ 01 декабря 2009

У меня есть тестируемый код, который вызывает регистратор 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 от регистратора, но предположим, что это возможно.)

Ответы [ 21 ]

130 голосов
/ 01 декабря 2009

Мне это тоже нужно было несколько раз. Ниже я собрал небольшой образец, который вы бы хотели адаптировать к вашим потребностям. По сути, вы создаете свой собственный Appender и добавляете его в нужный вам регистратор. Если вы хотите собрать все, корневой регистратор - хорошее место для начала, но вы можете использовать более конкретный, если хотите. Не забудьте удалить 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);
    }
}
32 голосов
/ 02 декабря 2009

Большое спасибо за эти (удивительно) быстрые и полезные ответы; они поставили меня на правильный путь для моего решения.

Кодовая база, где я хочу использовать это, использует java.util.logging в качестве механизма ведения журнала, и я не чувствую себя как дома в этих кодах, чтобы полностью изменить это на log4j или на интерфейсы / фасады logger Но, основываясь на этих предложениях, я «взломал» расширение 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) не происходило ненужного ведения журнала. Сделайте все, что нужно вашему тестируемому коду, чтобы использовать этот регистратор, запустите тест и assertEquality:

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

(Конечно, вы бы переместили большую часть этой работы в метод @Before и внесли множество других улучшений, но это загромождает эту презентацию.)

24 голосов
/ 12 августа 2018

Вот простое и эффективное решение Logback.
Не требует добавления / создания нового класса.
Он опирается на ListAppender: приложение для входа в систему whitebox, где записи журнала добавляются в поле 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());
    }
}

Утверждения 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));
16 голосов
/ 01 декабря 2009

Эффективно вы тестируете побочный эффект зависимого класса. Для модульного тестирования вам нужно только убедиться, что

logger.info()

вызван с правильным параметром. Следовательно, используйте эмулирующую среду для эмуляции логгера, и это позволит вам протестировать поведение вашего собственного класса.

13 голосов
/ 23 февраля 2015

Другой вариант - смоделировать Appender и проверить, было ли сообщение зарегистрировано для этого 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);
    }
}
10 голосов
/ 01 декабря 2009

Mocking - вариант здесь, хотя это было бы сложно, потому что регистраторы, как правило, являются частными static final - так что настройка mock logger не будет легкой задачей или потребует модификации тестируемого класса.

Вы можете создать собственный Appender (или как он там называется) и зарегистрировать его - либо через файл конфигурации только для тестирования, либо во время выполнения (в зависимости от структуры ведения журнала). И затем вы можете получить этот appender (либо статически, если он объявлен в файле конфигурации, либо по его текущей ссылке, если вы подключаете его во время выполнения), и проверить его содержимое.

5 голосов
/ 27 ноября 2015

Вдохновленный решением @ 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"));
}

Вероятно, вы могли бы заставить его использовать хамкрест более умным способом, но я оставил это на этом.

4 голосов
/ 01 декабря 2009

Как уже упоминалось, вы можете использовать фальшивый фреймворк. Чтобы это работало, вы должны выставить регистратор в своем классе (хотя я бы предпочел сделать его закрытым, а не создавать открытый сеттер).

Другое решение - создать фальшивый регистратор вручную. Вы должны написать фальшивый логгер (больше кода фикстуры), но в этом случае я бы предпочел улучшенную читаемость тестов по сравнению с сохраненным кодом из фреймворка.

Я бы сделал что-то вроде этого:

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());
    }
}
4 голосов
/ 27 июля 2017

Для log4j2 решение немного отличается, потому что AppenderSkeleton больше не доступен. Кроме того, использование Mockito или аналогичной библиотеки для создания Appender с 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());
    }
}
4 голосов
/ 03 апреля 2013

Вот что я сделал для входа.

Я создал класс 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 / 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>

Теперь в моих тестовых классах, которые выходят за пределы моего родительского тестового класса, я могу получить appender и зарегистрировать последнее сообщение и проверить сообщение, уровень, бросаемый.

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