programing

JUnit 테스트를 통해 SLF4J(로그백 포함) 로깅을 대행 수신하려면 어떻게 해야 합니까?

newsource 2022. 9. 24. 12:40

JUnit 테스트를 통해 SLF4J(로그백 포함) 로깅을 대행 수신하려면 어떻게 해야 합니까?

로깅을 + 「SLF4J + logback」을할 수 ?InputStreamJUnit JUnit은

Slf4j API는 이러한 방법을 제공하지 않지만 Logback은 간단한 솔루션을 제공합니다.

로그 엔트리가 추가되는 화이트박스 로그백어펜더를 사용할 수 있습니다.public List우리가 우리의 주장을 하는데 사용할 수 있는 분야입니다.

여기 간단한 예가 있습니다.

Foo 클래스:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Foo {

    static final Logger LOGGER = LoggerFactory.getLogger(Foo .class);

    public void doThat() {
        logger.info("start");
        //...
        logger.info("finish");
    }
}

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

AssertJ 또는 Hamcrest로 Matcher/Assertion 라이브러리를 사용할 수도 있습니다.

AssertJ를 사용하면 다음과 같습니다.

import org.assertj.core.api.Assertions;

Assertions.assertThat(listAppender.list)
          .extracting(ILoggingEvent::getFormattedMessage, ILoggingEvent::getLevel)
          .containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));

커스텀 어펜더를 작성할 수 있습니다.

public class TestAppender extends AppenderBase<LoggingEvent> {
    static List<LoggingEvent> events = new ArrayList<>();
    
    @Override
    protected void append(LoggingEvent e) {
        events.add(e);
    }
}

를 사용하도록 logback-test.xml 을 설정합니다.이제 테스트에서 로깅 이벤트를 확인할 수 있습니다.

@Test
public void test() {
    ...
    Assert.assertEquals(1, TestAppender.events.size());
    ...
}

" " 를 사용합니다.ILoggingEvent출력이 표시되지 않는 경우는, 코멘트 섹션을 참조해 주세요.

slf4j-test는 http://projects.lidalia.org.uk/slf4j-test/ 에서 사용할 수 있습니다.전체 logback slf4j 구현을 자체 테스트용 slf4j api 구현으로 대체하고 로깅 이벤트에 대해 단언할 수 있는 api를 제공합니다.

예:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <configuration>
        <classpathDependencyExcludes>
          <classpathDependencyExcludes>ch.qos.logback:logback-classic</classpathDependencyExcludes>
        </classpathDependencyExcludes>
      </configuration>
    </plugin>
  </plugins>
</build>
public class Slf4jUser {
    
    private static final Logger logger = LoggerFactory.getLogger(Slf4jUser.class);
    
    public void aMethodThatLogs() {
        logger.info("Hello World!");
    }
}
public class Slf4jUserTest {
    
    Slf4jUser slf4jUser = new Slf4jUser();
    TestLogger logger = TestLoggerFactory.getTestLogger(Slf4jUser.class);
    
    @Test
    public void aMethodThatLogsLogsAsExpected() {
        slf4jUser.aMethodThatLogs();
    
        assertThat(logger.getLoggingEvents(), is(asList(info("Hello World!"))));
    }
    
    @After
    public void clearLoggers() {
        TestLoggerFactory.clear();
    }
}

JUnit 5 포함

private ListAppender<ILoggingEvent> logWatcher;

@BeforeEach
void setup() {
  logWatcher = new ListAppender<>();
  logWatcher.start();
  ((Logger) LoggerFactory.getLogger(MyClass.class)).addAppender(logWatcher);
}

주의: MyClass.class - Prod 클래스가 되어야 합니다.로그 출력은 다음에서 예상됩니다.

사용: (AssertJ의 예)

@Test
void myMethod_logs2Messages() {

  ...
  int logSize = logWatcher.list.size();
  assertThat(logWatcher.list.get(logSize - 2).getFormattedMessage()).contains("EXPECTED MSG 1");
  assertThat(logWatcher.list.get(logSize - 1).getFormattedMessage()).contains("EXPECTED MSG 2");
}

파기:

성능을 향상시키려면 분리를 권장합니다.

@AfterEach
void teardown() {
  ((Logger) LoggerFactory.getLogger(MyClass.class)).detachAndStopAllAppenders();
}

Import:

import org.slf4j.LoggerFactory;
import ch.qos.logback.core.read.ListAppender;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.Logger;

to: @.credits to: @davidxxxxxxxx「」에 대해서는, 을 참조해 주세요.import ch.qos.logback...상세: https://stackoverflow.com/a/52229629/601844

간단한 해결책은 Mockito를 사용하여 지원자를 조롱하는 것입니다(예:

MyClass.java

@Slf4j
class MyClass {
    public void doSomething() {
        log.info("I'm on it!");
    }
}

MyClassTest.java

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.verify;         

@RunWith(MockitoJUnitRunner.class)
public class MyClassTest {    

    @Mock private Appender<ILoggingEvent> mockAppender;
    private MyClass sut = new MyClass();    

    @Before
    public void setUp() {
        Logger logger = (Logger) LoggerFactory.getLogger(MyClass.class.getName());
        logger.addAppender(mockAppender);
    }

    @Test
    public void shouldLogInCaseOfError() {
        sut.doSomething();

        verify(mockAppender).doAppend(ArgumentMatchers.argThat(argument -> {
            assertThat(argument.getMessage(), containsString("I'm on it!"));
            assertThat(argument.getLevel(), is(Level.INFO));
            return true;
        }));

    }

}

반환하지 을 사용하고 있습니다.false코드나 (오류) 에러는 읽기 쉬워지지만, 복수의 검증이 있는 경우는 동작하지 않습니다.는 돌려줘야 요.boolean이치노

커스텀 logback appender를 작성하는 것은 좋은 해결책이지만, 첫 번째 단계일 뿐이며, 결국 slf4j-test를 개발/재발명하게 됩니다.또한 spf4j-slf4j-test 등 아직 모르는 프레임워크도 있습니다.

최종적으로 메모리에 보관하고 있는 이벤트의 수, 에러가 기록되었을 때의 장치 테스트 실패(단언되지 않았을 경우), 테스트 실패 시 디버깅로그를 사용할 수 있도록 하는 등의 문제가 발생합니다.

면책사항:저는 spf4j-slf4j-test의 저자이며, spf4j-slf4j-test를 보다 잘 테스트하기 위해 이 백엔드를 작성했습니다.이 백엔드는 spf4j-slf4j-test를 사용하는 방법에 대한 예를 보기 좋은 장소입니다.제가 달성한 주요 장점 중 하나는 실패 시 필요한 모든 세부 사항을 그대로 유지하면서 빌드 출력을 줄였다는 것입니다(Travis 제한).

JUnit 규칙으로 테스트에 포함할 수 있는 간단하고 재사용 가능한 스파이 구현을 권장합니다.

public final class LogSpy extends ExternalResource {

    private Logger logger;
    private ListAppender<ILoggingEvent> appender;

    @Override
    protected void before() {
        appender = new ListAppender<>();
        logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); // cast from facade (SLF4J) to implementation class (logback)
        logger.addAppender(appender);
        appender.start();
    }

    @Override
    protected void after() {
        logger.detachAppender(appender);
    }

    public List<ILoggingEvent> getEvents() {
        if (appender == null) {
            throw new UnexpectedTestError("LogSpy needs to be annotated with @Rule");
        }
        return appender.list;
    }
}

테스트에서는 다음과 같은 방법으로 스파이를 활성화합니다.

@Rule
public LogSpy log = new LogSpy();

불러log.getEvents()(또는 기타 커스텀 방식)을 사용하여 로깅된 이벤트를 확인합니다.

LOGER.error(메시지, 예외)와 같은 로그 행을 테스트할 때 문제가 발생했습니다.

http://projects.lidalia.org.uk/slf4j-test/에 기재되어 있는 솔루션은 예외에 대해서도 주장하려고 합니다.스택 트레이스를 재작성하는 것은 쉽지 않습니다(또한 제 의견으로는 가치가 없습니다).

나는 다음과 같이 해결했다.

import org.junit.Test;
import org.slf4j.Logger;
import uk.org.lidalia.slf4jext.LoggerFactory;
import uk.org.lidalia.slf4jtest.TestLogger;
import uk.org.lidalia.slf4jtest.TestLoggerFactory;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
import static uk.org.lidalia.slf4jext.Level.ERROR;
import static uk.org.lidalia.slf4jext.Level.INFO;


public class Slf4jLoggerTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(Slf4jLoggerTest.class);


    private void methodUnderTestInSomeClassInProductionCode() {
        LOGGER.info("info message");
        LOGGER.error("error message");
        LOGGER.error("error message with exception", new RuntimeException("this part is not tested"));
    }





    private static final TestLogger TEST_LOGGER = TestLoggerFactory.getTestLogger(Slf4jLoggerTest.class);

    @Test
    public void testForMethod() throws Exception {
        // when
        methodUnderTestInSomeClassInProductionCode();

        // then
        assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").contains(
                tuple(INFO, "info message"),
                tuple(ERROR, "error message"),
                tuple(ERROR, "error message with exception")
        );
    }

}

이것은 Hamcrest matcers 라이브러리에 의존하지 않아도 된다는 장점도 있습니다.

이는 lamda를 사용하는 대체 수단으로서 로그 캡처 로직을 테스트 간에 재사용할 수 있도록 하고(그 실장을 캡슐화함) 불필요하게 합니다.@BeforeEach/@AfterEach(제안된 솔루션 중 일부에서는 부속품이 분리되지 않아 메모리 누수가 발생할 수 있습니다).

테스트 대상 코드:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyService {

    private static final Logger LOG = LoggerFactory.getLogger(MyService.class);


    public void doSomething(String someInput) {
        ...
        LOG.info("processing request with input {}", someInput);
        ...
    }
}

대행 수신 도우미:

package mypackage.util

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.slf4j.LoggerFactory;

import java.util.List;

public class LogInterceptor {

    public static List<ILoggingEvent> interceptLogs(Class<?> klass, Runnable runnable) {
        final Logger logger = (Logger) LoggerFactory.getLogger(klass);
        final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
        listAppender.start();
        logger.addAppender(listAppender);
        try {
            runnable.run();
            return listAppender.list;
        } finally {
            logger.detachAppender(listAppender);
        }
    }
}

테스트 스위트:


import static mypackage.util.LogInterceptor.interceptLogs;

public class MyServiceTest {

  private MyService myService; 
  ...

  @Test
  void doSomethingLogsLineWithTheGivenInput() {
        List<ILoggingEvent> logs = interceptLogs(
                myService.getClass(),
                () -> myService.doSomething("foo")
        );

        assertThat(logs).isNotEmpty();
        ILoggingEvent logEntry = logs.get(0);
        assertThat(logEntry.getFormattedMessage()).isEqualTo("Processing request with input foo");
        assertThat(logEntry.getLevel()).isEqualTo(Level.INFO);
  }

}

언급URL : https://stackoverflow.com/questions/29076981/how-to-intercept-slf4j-with-logback-logging-via-a-junit-test