로거의 메시지에 대해 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원하는 로거에 추가합니다. 모든 것을 수집하려면 루트 로거를 시작하는 것이 좋지만 원하는 경우 더 구체적으로 사용할 수 있습니다. 완료되면 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);
    }
}

4
이것은 잘 작동합니다. 내가 할 수있는 유일한 개선은을 호출 logger.getAllAppenders()한 다음 단계별로 호출 appender.setThreshold(Level.OFF)하고 각각을 호출 하는 것입니다 (완료되면 재설정하십시오!). 이렇게하면 생성하려는 "잘못된"메시지가 테스트 로그에 표시되지 않고 다음 개발자를 놀라게 할 수 있습니다.
Coderer

1
Log4j 2.x에서는 플러그인을 만들어야하므로 약간 더 복잡합니다. stackoverflow.com/questions/24205093/…
paranza

1
고마워 그러나 LogBack을 사용하는 경우 ListAppender<ILoggingEvent>사용자 정의 어 펜더를 작성 하는 대신 사용할 수 있습니다 .
sinujohn

2
그러나 이것은 slf4j에서 작동하지 않습니다! 그걸로 작동하도록 어떻게 바꿀 수 있는지 아십니까?
Shilan

3
@sd Loggerto org.apache.logging.log4j.core.Logger(인터페이스의 구현 클래스)를 캐스팅하면 setAppender()/removeAppender()다시 액세스 할 수 있습니다 .
David Moles

59

다음은 간단하고 효율적인 로그 백 솔루션입니다.
새 클래스를 추가하거나 만들 필요가 없습니다.
그것은에 의존 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));

오류를 기록하면 테스트 실패를 어떻게 중지합니까?
Ghilteras

@Ghilteras 이해가 잘되지 않습니다. 오류를 기록하면 테스트가 실패하지 않아야합니다. 당신은 무엇을 설명합니까?
davidxxx

또한 mock시험을 치르는 수업 을하지 않아야 합니다. new연산자 로 인스턴스화해야합니다
Dmytro Chasovskyi

35

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

코드베이스는 이것을 사용하고, 로거 메커니즘으로 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방법 으로 옮기고 여러 가지 개선 사항을 만들지 만이 프리젠 테이션을 어지럽 힐 수 있습니다.)


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
대부분의 로거가 정의 된 비공개 정적 최종 필드를 어떻게 조롱 했습니까? 파워 mockito? 재미를 ...
스테파노 L에게

스테파노 : 그 마지막 필드는 어떻게 든 초기화되었는데, 나는 실제보다는 Mocks를 주입하는 다양한 접근법을 보았습니다. 아마도 처음에는 테스트 가능성을 위해 일정 수준의 디자인이 필요합니다. blog.codecentric.de/ko/2011/11/…
djna

Mehdi가 말했듯이, 적절한 핸들러를 사용하는 것만으로도 충분할 수 있습니다.
djna

11

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

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


10

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


6

log4j2의 경우 AppenderSkeleton을 더 이상 사용할 수 없으므로 솔루션이 약간 다릅니다. 또한 Mockito 또는 유사한 라이브러리를 사용하여 ArgumentCaptor로 Appender를 작성하면 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

와. 이것이 왜 그렇게 어려운지 잘 모르겠습니다. slf4j에서 log4j2를 사용하고 있었기 때문에 위의 코드 샘플을 사용할 수 없습니다. 이것은 내 솔루션입니다.

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

그런 다음 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();
    }
}

src / test / resources에 logback-test.xml 파일이 정의되어 있고 테스트 어 펜더를 추가했습니다.

<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 (Jupiter)의 경우 Spring의 OutputCaptureExtension 이 매우 유용합니다. Spring Boot 2.2부터 사용할 수 있으며 spring-boot-test artifact 에서 사용할 수 있습니다 .

예 (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
이것은 나를 위해 일했습니다. 'when (mockAppender.getName ()). thenReturn ( "MOCK")'줄이 필요하지 않았습니다.
Mayank Raghav

1

Log4J2 용 API는 약간 다릅니다. 또한 비동기 어 펜더를 사용 중일 수 있습니다. 나는 이것을 위해 래치 된 어 펜더를 만들었습니다.

    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일을하지 않으면 구현 클래스로 캐스팅 할 수 있어야 합니다.이 클래스 는 해당 메소드를 노출시킵니다.

MockitoAssertJ 의 예는 다음같습니다 .

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

Appender를 모의하면 로그 라인을 캡처하는 데 도움이 될 수 있습니다. 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의 올바른 메소드를 호출하는 것만 담당합니다.
  • 비즈니스 로깅에서 사용할 수있는 내부 API를 설명 implements하는 구체적 로깅 클래스interface
  • 비즈니스 로직을 구현하고 로깅을 수행해야하는 클래스에는 위임 할 로깅 오브젝트에 대한 참조가 있습니다. 참조 클래스는 초록 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에 대한 비슷한 질문에 대답했다. 어떻게 할 수 있는지 테스트 할 수있다.

이것은 Log4j2 (2.1.21.2로 테스트)와 junit 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");
        }
    }
}

다음 Maven 종속성 사용

 <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 ()) 라인의 setup 메소드에서 오류가 발생했습니다. 오류는 org.apache.logging.log4j.spi.LoggerContextShutdownEnabled에 대한 org.apache.logging.log4j.spi.LoggerContextShutdownEnabled 클래스 파일에 액세스 할 수 없습니다.
Carlos Palma

코드를 검토하고 약간 변경했지만 나에게 도움이되었습니다. 의존성을 확인하고 모든 수입품이 정확한지 확인하시기 바랍니다
Haim Raman

안녕하세요. 나는 로그 백 솔루션을 구현하게되었지만 ... 당신이 옳다고 생각합니다. 그것을 구현하기 위해서는 다른 log4j 버전으로 만든 가져 오기를 정리해야했습니다.
카를로스 팔마

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

src / test / resources의 일부로 log4j2.xml을 갖는 것을 잊지 마십시오

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.