[java] 로거의 메시지에 대해 JUnit 어설 션을 수행하는 방법

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를 얻는 방법은 분명하지 않지만 가능하다고 가정하십시오.)



답변

나는 이것을 여러 번 필요로했다. 아래에 작은 샘플을 준비했습니다. 필요에 맞게 조정하고 싶습니다. 기본적으로, 자신을 작성하여 Appender원하는 로거에 추가합니다. 모든 것을 수집하려면 루트 로거를 시작하는 것이 좋지만 원하는 경우 더 구체적으로 사용할 수 있습니다. 완료되면 Appender를 제거해야합니다. 그렇지 않으면 메모리 누수가 발생할 수 있습니다. 나는 그러나, 시험에서 그것을 한 적이 아래 setUp또는 @Before하고 tearDown또는 @After필요에 따라 더 나은 곳이 될 수 있습니다.

또한 아래 구현 List은 메모리의 모든 것을 수집 합니다. 많이 로깅하는 경우 지루한 항목을 삭제하거나 디스크의 임시 파일에 로그를 기록하는 필터를 추가하는 것을 고려할 수 있습니다 (힌트 : LoggingEventis 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);
    }
}


답변

다음은 간단하고 효율적인 로그 백 솔루션입니다.
새 클래스를 추가하거나 만들 필요가 없습니다.
그것은에 의존 ListAppender하십시오 화이트 박스 logback 펜더 로그 항목이 추가되는 경우 public List우리가 우리의 주장을 확인하는 데 사용할 수있는 해당 필드.

다음은 간단한 예입니다.

푸 클래스 :

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와 같은 Matcher / assertion 라이브러리는 다음과 같이 더 좋습니다.

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


답변

이 (놀랍게도) 빠르고 유용한 답변에 감사드립니다. 그들은 내 솔루션에 맞는 길로 나를 안내했습니다.

코드베이스는 이것을 사용하고, 로거 메커니즘으로 java.util.logging을 사용하고 있었고, log4j 또는 로거 인터페이스 / 외관으로 코드를 완전히 변경하기에 충분하지 않습니다. 그러나 이러한 제안에 따라 julhandler 확장을 ‘해킹’했으며 이는 대우로 작동합니다.

다음은 간단한 요약입니다. 연장 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-test 준비 과정에서 a를 작성 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 테스트 실행에 대한) 불필요한 로깅이 발생하지 않도록, 일반 핸들러를 침묵하는 것입니다. 테스트중인 코드가이 로거를 사용하는 데 필요한 모든 작업을 수행하고 테스트를 실행하고 동일성을 지정하십시오.

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

(물론이 작품의 많은 부분을 @Before방법 으로 옮기고 여러 가지 개선 사항을 만들지 만이 프리젠 테이션을 어지럽 힐 수 있습니다.)


답변

또 다른 옵션은 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);
    }
}


답변

효과적으로 당신은 종속 클래스의 부작용을 테스트하고 있습니다. 단위 테스트의 경우 다음을 확인하기 만하면됩니다.

logger.info()

올바른 매개 변수로 호출되었습니다. 따라서 모의 프레임 워크를 사용하여 로거를 에뮬레이트하면 자신의 클래스 동작을 테스트 할 수 있습니다.


답변

로거는 일반적으로 비공개 정적 최종이기 때문에 조롱하는 것은 어려운 일이지만, 모의 로거를 설정하는 것은 케이크 조각이 아니거나 테스트중인 클래스를 수정해야합니다.

테스트 전용 구성 파일 또는 런타임 (로깅 프레임 워크에 따라)을 통해 사용자 정의 Appender (또는 호출 된 항목)를 작성하고 등록 할 수 있습니다. 그런 다음 해당 어 펜더 (구성 파일에 선언 된 경우 정적으로 또는 런타임을 연결하는 경우 현재 참조로)를 가져 와서 내용을 확인할 수 있습니다.


답변

@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를 사용하도록 만들 수는 있지만 이것으로 남겨 두었습니다.