web-dev-qa-db-ja.com

JUnitテストを介してSLF4J(ログバック付き)ロギングをインターセプトする方法は?

何らかの方法でロギング(SLF4J + logback)をインターセプトし、JUnitテストケースを介してInputStream(または読み取り可能な何か)を取得することは可能ですか?

44
carlspring

カスタムアペンダーを作成できます

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

Slf4j APIはそのような方法を提供しませんが、Logbackはシンプルなソリューションを提供します。

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

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

http://projects.lidalia.org.uk/slf4j-test/ からslf4j-testを使用できます。ログバック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();
    }
}
11
Oleg Majewski

カスタムログバックアペンダーを作成することは良い解決策ですが、それは最初のステップにすぎません。最終的に slf4j-test を開発/再発明することになり、さらに進んだ場合は spf4j -slf4j-test または私がまだ知らない他のフレームワーク。

最終的には、メモリに保持するイベントの数を心配する必要があります。エラーが記録されたときにアサートされずに単体テストに失敗し、テストの失敗時にデバッグログを使用できるようにします。

免責事項:私はspf4j-slf4j-testの作成者です。このバックエンドは spf4j をより良くテストできるように作成しました。これはspf4j-slf4j-テスト。私が達成した主な利点の1つは、障害が発生したときに必要なすべての詳細を保持しながら、ビルド出力を減らすことです(Travisで制限されています)。

3
user2179737

LOGGER.error(message、exception)のようなログ行をテストするときに問題が発生しました。

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 matchersライブラリに依存しないという利点もあります。

2
daemon_nio

簡単な解決策は、アペンダーを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を返すのではなく、アサーションを使用していますが、複数の検証がある場合は機能しません。その場合、値が期待どおりかどうかを示すブール値を返す必要があります。

1
snovelli

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()(または他のカスタムメソッド)を呼び出して、記録されたイベントを確認します。

1
oberlies