web-dev-qa-db-ja.com

SLF4Jログメッセージを単体テストする最良の方法は何ですか?

私はslf4jを使用していて、コードを単体テストして、特定の条件下で警告/エラーログメッセージが生成されることを確認します。これらは厳密な単体テストであることが望ましいので、ログメッセージが生成されることをテストするために、ファイルからログ構成をプルアップする必要はありません。私が使用しているモックフレームワークはMockitoです。

41
Javid Jamae

カスタムアペンダーで問題を解決できると思います。 org.Apache.log4j.Appenderを実装するテストアペンダーを作成し、log4j.propertiesにアペンダーを設定して、テストケースを実行するときにそれをロードします。

そのappenderからテストハーネスにコールバックすると、ログに記録されたメッセージを確認できます

11
Zsolt

特定の実装(log4jなど)に依存せずにslf4jをテストするには、 this SLF4J FAQ で説明されているように、独自のslf4jロギング実装を提供できます。実装では、ログに記録されたメッセージを記録し、ユニットテストによって検証のために問い合わせることができます。

slf4j-test パッケージはまさにこれを行います。ログに記録されたメッセージを取得するためのメソッドを提供するのは、メモリ内のslf4jロギング実装です。

18
eakst7

同時テスト実行がある環境で本当にうまく機能するSLF4Jのより良いテスト実装は https://github.com/portingle/slf4jtesting です。

私は、slf4jログテストと、同時テスト実行に関する既存のテストアプローチの制限についてのいくつかの議論に賛成しました。

私は自分の言葉をコードに入れることにしました。その結果がgit repoです。

9
John Lonergan

SLF4Jをモックする代わりに、重要なロギング呼び出しneedを配置して、より簡単にモックできる独自のメソッド内でテストすることができます。

本当にSLF4Jをモックしたい場合は、サービスオブジェクトにインジェクションするのではなく、SLF4J側からモックロガーを提供できる独自のプロバイダーを作成できます。

3
David Harkness

テストルールを作成します。

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.LoggerFactory;

import Java.util.List;
import Java.util.stream.Collectors;

public class LoggerRule implements TestRule {

  private final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
  private final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);

  @Override
  public Statement apply(Statement base, Description description) {
    return new Statement() {
      @Override
      public void evaluate() throws Throwable {
        setup();
        base.evaluate();
        teardown();
      }
    };
  }

  private void setup() {
    logger.addAppender(listAppender);
    listAppender.start();
  }

  private void teardown() {
    listAppender.stop();
    listAppender.list.clear();
    logger.detachAppender(listAppender);
  }

  public List<String> getMessages() {
    return listAppender.list.stream().map(e -> e.getMessage()).collect(Collectors.toList());
  }

  public List<String> getFormattedMessages() {
    return listAppender.list.stream().map(e -> e.getFormattedMessage()).collect(Collectors.toList());
  }

}

次にそれを使用します:

@Rule
public final LoggerRule loggerRule = new LoggerRule();

@Test
public void yourTest() {
    // ...
    assertThat(loggerRule.getFormattedMessages().size()).isEqualTo(2);
}
3
Andrew Feng

この質問が投稿されてからしばらく経ちましたが、同じような問題に遭遇しただけで私の解決策が役立つかもしれません。 @Zsoltによって提案されたソリューションの行に沿って、アペンダー、より具体的にはLogbackのListAppenderを使用します。ここにコードと構成を示します(Groovyコードですが、Javaに簡単に移植できます)。

ログアクセス用のGroovyクラス:

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

class LogAccess {

    final static String DEFAULT_PACKAGE_DOMAIN = Logger.ROOT_LOGGER_NAME
    final static String DEFAULT_APPENDER_NAME = 'LIST'
    final List<LoggingEvent> list

    LogAccess(String packageDomain = DEFAULT_PACKAGE_DOMAIN, String appenderName = DEFAULT_APPENDER_NAME) {
        Logger logger = (Logger) LoggerFactory.getLogger(packageDomain)
        ListAppender<LoggingEvent> appender = logger.getAppender(appenderName) as ListAppender<LoggingEvent>
        if (appender == null) {
            throw new IllegalStateException("'$DEFAULT_APPENDER_NAME' appender not found. Did you forget to add 'logback.xml' to the resources folder?")
        }
        this.list = appender.list
        this.clear()
    }

    void clear() {
        list.clear()
    }

    boolean contains(String logMessage) {
        return list.reverse().any { it.getFormattedMessage() == logMessage }
    }

    @Override
    String toString() {
        list.collect { it. getFormattedMessage() }
    }
}

Logback.xml設定のサンプル:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- These 2 'includes' tags ensure regular springboot console logging works as usual -->
    <!-- See https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-configure-logback-for-logging -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <appender name="LIST" class="ch.qos.logback.core.read.ListAppender"/>
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="LIST" />
    </root>
</configuration>

テスト:

LogAccess log = new LogAccess()
def expectedLogEntry = 'Expected Log Entry'
assert !log.contains(expectedLogEntry)
methodUnderTest()
assert log.contains(expectedLogEntry)

これをGroovy + Spockを使用したSpringBootプロジェクトで使用しますが、これがどのようなJava Logbackを使用するプロジェクトでも機能しない)理由はわかりません。

0
Claudio

私はこの投稿の上部に投稿する新しい回答があります(私の「古い」回答はまだこの投稿の下部にあります)(私の執筆時点では、私の「古い」回答は「0」でしたので、害はありません、ファウルなし!)

新しい答え:

これがGradleパッケージです:

  testImplementation 'com.portingle:slf4jtesting:1.1.3'

Mavenリンク:

https://mvnrepository.com/artifact/com.portingle/slf4jtesting

ドイツ語コード:

(以下のインポートとプライベートメソッドはMyTestClass(.Java)に入ります)

import static org.junit.Assert.assertNotNull;

import slf4jtest.LogLevel;
import slf4jtest.Settings;
import slf4jtest.TestLogger;
import slf4jtest.TestLoggerFactory;



@Test
public void myFirstTest() {


    org.slf4j.Logger unitTestLogger = this.getUnitTestLogger();
    ISomethingToTestObject testItem = new SomethingToTestObject (unitTestLogger);
    SomeReturnObject obj = testItem.myMethod("arg1");
    assertNotNull(wrapper);

    /* now here you would find items in the unitTestLogger */

    assertContains(unitTestLogger, LogLevel.DebugLevel, "myMethod was started");

}

// render nicer errors
private void assertContains(TestLogger unitTestLogger, LogLevel logLev, String expected) throws Error {
    if (!unitTestLogger.contains(logLev, expected)) {
        throw new AssertionError("expected '" + expected + "' but got '" + unitTestLogger.lines() + "'");
    }
}

// render nicer errors
private void assertNotContains(TestLogger unitTestLogger, LogLevel logLev, String expected) throws Error {
    if (unitTestLogger.contains(logLev, expected)) {
        throw new AssertionError("expected absence of '" + expected + "' but got '" + unitTestLogger.lines() + "'");
    }
}



    private TestLogger getUnitTestLogger() {
        TestLoggerFactory loggerFactory = Settings.instance()
                .enableAll() // necessary as by default only ErrorLevel is enabled
                .buildLogging();

        TestLogger returnItem = loggerFactory.getLogger(MyTestClasss.class.getName());
        assertNotNull(returnItem);
        return returnItem;
    }

=============================以下の古い回答..使用しないでください============= ===

以下は私の以前の答えです。以下のコードを変更しました...見つけた後、上記のパッケージ(上記のパッケージ)を使用するようにしました。

So here is my method.

First, I allow the logger to be injected.  But I provide a default as well:

```Java
package com.mycompany.myproject;

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

public class MyCoolClass { //implements IMyCoolClass {

    private static final String PROCESS_STARTED = "Process started. (key='%1$s')";

    private final Logger logger;

    public MyCoolClass() {
        this(LoggerFactory.getLogger(MyCoolClass.class));
    }

    public MyCoolClass(Logger lgr) {
        this.logger = lgr;
    }

    public doSomething(int key)
    {
        logger.info(String.format(PROCESS_STARTED, key));
        /*now go do something */
    }
}

Then I wrote a very basic in memory logger


```Java
import org.slf4j.Marker;

import Java.util.ArrayList;
import Java.util.Collection;

public class InMemoryUnitTestLogger implements org.slf4j.Logger {

    public Collection<String> informations = new ArrayList<String>();
    public Collection<String> errors = new ArrayList<String>();
    public Collection<String> traces = new ArrayList<String>();
    public Collection<String> debugs = new ArrayList<>();
    public Collection<String> warns = new ArrayList<>();

    public Collection<String> getInformations() {
        return informations;
    }

    public Collection<String> getErrors() {
        return errors;
    }

    public Collection<String> getTraces() {
        return traces;
    }

    public Collection<String> getDebugs() {
        return debugs;
    }

    public Collection<String> getWarns() {
        return warns;
    }


    @Override
    public String getName() {
        return "FakeLoggerName";
    }

    @Override
    public boolean isTraceEnabled() {
        return false;
    }

    @Override
    public boolean isTraceEnabled(Marker marker) {
        return false;
    }

    @Override
    public boolean isDebugEnabled() {
        return false;
    }

    @Override
    public boolean isDebugEnabled(Marker marker) {
        return false;
    }

    @Override
    public boolean isWarnEnabled(Marker marker) {
        return false;
    }

    @Override
    public boolean isInfoEnabled(Marker marker) {
        return false;
    }

    @Override
    public boolean isWarnEnabled() {
        return false;
    }

    @Override
    public boolean isErrorEnabled(Marker marker) {
        return false;
    }

    @Override
    public boolean isInfoEnabled() {
        return false;
    }

    @Override
    public boolean isErrorEnabled() {
        return false;
    }

    @Override
    public void trace(String s) {
        this.internalTrace(s);
    }

    @Override
    public void trace(String s, Object o) {
        this.internalTrace(s);
    }

    @Override
    public void trace(String s, Object o, Object o1) {
        this.internalTrace(s);
    }

    @Override
    public void trace(String s, Object... objects) {
        this.internalTrace(s);
    }

    @Override
    public void trace(String s, Throwable throwable) {
        this.internalTrace(s);
    }


    @Override
    public void trace(Marker marker, String s) {
        this.internalTrace(s);
    }

    @Override
    public void trace(Marker marker, String s, Object o) {
        this.internalTrace(s);
    }

    @Override
    public void trace(Marker marker, String s, Object o, Object o1) {
        this.internalTrace(s);
    }

    @Override
    public void trace(Marker marker, String s, Object... objects) {
        this.internalTrace(s);
    }

    @Override
    public void trace(Marker marker, String s, Throwable throwable) {
        this.internalTrace(s);
    }

    @Override
    public void debug(String s) {
        this.internalDebug(s);
    }

    @Override
    public void debug(String s, Object o) {
        this.internalDebug(s);
    }

    @Override
    public void debug(String s, Object o, Object o1) {
        this.internalDebug(s);
    }

    @Override
    public void debug(String s, Object... objects) {
        this.internalDebug(s);
    }

    @Override
    public void debug(String s, Throwable throwable) {
        this.internalDebug(s);
    }

    @Override
    public void debug(Marker marker, String s) {
        this.internalDebug(s);
    }

    @Override
    public void debug(Marker marker, String s, Object o) {
        this.internalDebug(s);
    }

    @Override
    public void debug(Marker marker, String s, Object o, Object o1) {
        this.internalDebug(s);
    }

    @Override
    public void debug(Marker marker, String s, Object... objects) {
        this.internalDebug(s);
    }

    @Override
    public void debug(Marker marker, String s, Throwable throwable) {
        this.internalDebug(s);
    }

    public void info(String s) {
        this.internalInfo(s);
    }

    @Override
    public void info(String s, Object o) {
        this.internalInfo(s);
    }

    @Override
    public void info(String s, Object o, Object o1) {
        this.internalInfo(s);
    }

    @Override
    public void info(String s, Object... objects) {
        this.internalInfo(s);
    }

    @Override
    public void info(String s, Throwable throwable) {
        this.internalInfo(s);
    }

    @Override
    public void info(Marker marker, String s) {
        this.internalInfo(s);
    }

    @Override
    public void info(Marker marker, String s, Object o) {
        this.internalInfo(s);
    }

    @Override
    public void info(Marker marker, String s, Object o, Object o1) {
        this.internalInfo(s);
    }

    @Override
    public void info(Marker marker, String s, Object... objects) {
        this.internalInfo(s);
    }

    @Override
    public void info(Marker marker, String s, Throwable throwable) {
        this.internalInfo(s);
    }

    public void error(String s) {
        this.internalError(s);
    }

    @Override
    public void error(String s, Object o) {
        this.internalError(s);
    }

    @Override
    public void error(String s, Object o, Object o1) {
        this.internalError(s);
    }

    @Override
    public void error(String s, Object... objects) {
        this.internalError(s);
    }

    @Override
    public void error(String s, Throwable throwable) {
        this.internalError(s);
    }

    @Override
    public void error(Marker marker, String s) {
        this.internalError(s);
    }

    @Override
    public void error(Marker marker, String s, Object o) {
        this.internalError(s);
    }

    @Override
    public void error(Marker marker, String s, Object o, Object o1) {
        this.internalError(s);
    }

    @Override
    public void error(Marker marker, String s, Object... objects) {
        this.internalError(s);
    }

    @Override
    public void error(Marker marker, String s, Throwable throwable) {
        this.internalError(s);
    }

    public void warn(String s) {
        this.internalWarn(s);
    }

    @Override
    public void warn(String s, Object o) {
        this.internalWarn(s);
    }

    @Override
    public void warn(String s, Object... objects) {
        this.internalWarn(s);
    }

    @Override
    public void warn(String s, Object o, Object o1) {
        this.internalWarn(s);
    }

    @Override
    public void warn(String s, Throwable throwable) {
        this.internalWarn(s);
    }

    @Override
    public void warn(Marker marker, String s) {
        this.internalWarn(s);
    }

    @Override
    public void warn(Marker marker, String s, Object o) {
        this.internalWarn(s);
    }

    @Override
    public void warn(Marker marker, String s, Object o, Object o1) {
        this.internalWarn(s);
    }

    @Override
    public void warn(Marker marker, String s, Object... objects) {
        this.internalWarn(s);
    }

    @Override
    public void warn(Marker marker, String s, Throwable throwable) {
        this.internalWarn(s);
    }

    private void internalDebug(String s) {
        System.out.println(s);
        this.debugs.add(s);
    }

    private void internalInfo(String msg) {
        System.out.println(msg);
        this.informations.add(msg);
    }

    private void internalTrace(String msg) {
        //??System.out.println(msg);
        this.traces.add(msg);
    }


    private void internalWarn(String msg) {
        System.err.println(msg);
        this.warns.add(msg);
    }

    private void internalError(String msg) {
        System.err.println(msg);
        this.errors.add(msg);
    }

次に、単体テストで、次の2つのいずれかを実行できます。

private ByteArrayOutputStream setupSimpleLog(Logger lgr) {
    ByteArrayOutputStream pipeOut = new ByteArrayOutputStream();
    PrintStream pipeIn = new PrintStream(pipeOut);
    System.setErr(pipeIn);
    return pipeOut;
}

private Logger getSimpleLog() {
    Logger lgr = new InMemoryUnitTestLogger();
    return lgr;
}


private void myTest() {


    Logger lgr = getSimpleLog();
    ByteArrayOutputStream pipeOut = this.setupSimpleLog(lgr);

    MyCoolClass testClass = new MyCoolClass(lgr);
    int myValue = 333;
    testClass.doSomething(myValue);

    String findMessage = String.format(MyCoolClass.PROCESS_STARTED, myValue);
    String output = new String(pipeOut.toByteArray());
    assertTrue(output.contains(findMessage));
}

または上記と同様ですが、カスタムロガーでキャストを実行します

private void myTest() {


    Logger lgr = getSimpleLog();
    MyCoolClass testClass = new MyCoolClass(lgr);
    int myValue = 333;
    testClass.doSomething(myValue);

    String findMessage = String.format(MyCoolClass.PROCESS_STARTED, myValue);
    InMemoryUnitTestLogger castLogger = (InMemoryUnitTestLogger)lgr;
    /* now check the exact subcollection for the message) */
    assertTrue(castLogger.getInfos().contains(findMessage));
}

細かいことを考慮してコードを理解してください。アイデアはそこにあります。コードをコンパイルしませんでした。

0
granadaCoder

@Zsoltと同様に、log4j AppenderをモックしてLoggerに設定し、Appender.doAppend()への呼び出しを確認できます。これにより、実際のコードを変更せずにテストできます。

0
Kevin Welker