web-dev-qa-db-ja.com

ロガーのメッセージで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を取得する方法は明確ではありませんが、それが可能であると仮定してください。)

164
Jon

これらの(驚くほど)迅速かつ有用な回答に感謝します。彼らは私のソリューションのために私を正しい道に導いた。

コードベースは私がこれを使いたいと思っていて、Java.util.loggingをロガーのメカニズムとして使用しており、それらのコードをlog4jやロガーのインターフェース/ファサードに完全に変更するのに十分な気がしません。しかし、これらの提案に基づいて、私はj.u.l.handler拡張機能を「ハッキング」しました。

短い要約が続きます。 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の準備では、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-test実行の場合)不要なロギングは発生しません。テスト対象のコードでこのロガーを使用するために必要なことをすべて行い、テストを実行してassertEqualityを実行します。

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

(もちろん、この作業の大部分を@Beforeメソッドに移動し、その他のさまざまな改善を行いますが、それによってこのプレゼンテーションが煩雑になります。)

32
Jon

これも数回必要です。必要に応じて調整したい小さなサンプルを以下にまとめました。基本的に、独自のAppenderを作成し、必要なロガーに追加します。すべてを収集する場合は、ルートロガーを開始するのに適していますが、必要に応じてより具体的なものを使用できます。完了したら、アペンダーを削除することを忘れないでください。そうしないと、メモリリークが発生する可能性があります。以下はテスト内で行いましたが、必要に応じてsetUpまたは@BeforetearDownまたは@Afterの方が適している場合があります。

また、以下の実装は、メモリ内のList内のすべてを収集します。大量にログを記録する場合は、退屈なエントリを削除するフィルタを追加するか、ディスク上の一時ファイルにログを書き込むことを検討してください(ヒント:LoggingEventSerializableです。ログメッセージがあれば、イベントオブジェクトをシリアル化します。)

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);
    }
}
132
Ronald Blaschke

シンプルで効率的な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());
    }
}

JUnitアサーションは、リスト要素の特定のプロパティをアサートするのにあまり適していません。
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));
31
davidxxx

事実上、依存クラスの副作用をテストしています。単体テストの場合、検証する必要があるのは

logger.info()

正しいパラメーターで呼び出されました。したがって、モックフレームワークを使用してロガーをエミュレートすると、独自のクラスの動作をテストできます。

16
djna

別のオプションは、アペンダーをモックし、このアペンダーにメッセージが記録されたかどうかを確認することです。 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);
    }
}
14
Marcin

ロッキングは一般にプライベートな静的ファイナルであるため、モッキングはオプションですが、モックロガーを設定することは簡単ではなく、テスト対象のクラスを変更する必要があります。

カスタムAppender(またはそれが呼び出されるもの)を作成し、テスト専用の構成ファイルまたはランタイム(ある意味では、ロギングフレームワークに依存)を介して登録できます。そして、そのアペンダーを取得して(構成ファイルで宣言されている場合は静的に、ランタイムにプラグインしている場合は現在の参照によって)、その内容を確認できます。

10
Bozho

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

たぶんもっと賢い方法でハンクレストを使用することができますが、私はこれを残しました。

6
slim

Log4j2の場合、AppenderSkeletonが利用できなくなったため、ソリューションはわずかに異なります。さらに、複数のログメッセージでMutableLogEventが再利用されるため、複数のログメッセージが予想される場合、Mockitoまたは同様のライブラリを使用してArgumentCaptorでAppenderを作成することはできません。 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());
    }
}
4
joseph

これが私がログバックのためにしたことです。

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(), "...");
4
kfox

他の人から述べたように、モックフレームワークを使用できます。これを機能させるには、クラス内のロガーを公開する必要があります(ただし、パブリックセッターを作成する代わりにプライベートにパッケージ化することをお勧めします)。

他の解決策は、手作業で偽のロガーを作成することです。偽のロガー(より多くのフィクスチャコード)を記述する必要がありますが、この場合、モックフレームワークから保存されたコードに対するテストの読みやすさを向上させることをお勧めします。

私はこのようなことをします:

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

ワオ。なぜこんなに大変だったのかわかりません。 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
Dagmar

私に関しては、JUnitMockitoとともに使用することでテストを簡素化できます。次の解決策を提案します。

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

それが、異なるメッセージ量のテストに対してNice flexibilityがある理由です

3

アペンダーのモックは、ログ行のキャプチャに役立ちます。サンプルを見つける: 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
nishant

Jmockit(1.21)を使用して、この簡単なテストを作成できました。テストでは、特定のERRORメッセージが1回だけ呼び出されることを確認します。

@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
Yarix

私がやりたいのは、文字列がログに記録されたことを確認することです(非常に脆い正確なログステートメントを確認するのではなく)、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);
0
Cheefachi

Java.util.logging.Loggerを使用している場合、この記事は非常に役立つかもしれません。新しいハンドラーを作成し、ログ出力にアサーションを作成します。 http://octodecillion.com/blog/jmockit-test-logging/

0
Mehdi Karamosly

以下のコードを使用してください。私は、春の統合テストに同じコードを使用しており、ログにログバックを使用しています。メソッド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
SUMIT

テストしようとしている可能性のあるものが2つあります。

  • プログラムのオペレーターに関心のあるイベントがある場合、プログラムは適切なロギング操作を実行します。これにより、そのイベントをオペレーターに通知できます。
  • 私のプログラムがロギング操作を実行すると、生成されるログメッセージに正しいテキストが含まれますか。

これらの2つのことは実際には異なるものであるため、別々にテストできます。ただし、2番目(メッセージのテキスト)のテストは非常に問題があるため、まったく実行しないことをお勧めします。メッセージテキストのテストは、最終的に、1つのテキスト文字列(予想されるメッセージテキスト)がロギングコードで使用されるテキスト文字列と同じか、または簡単に派生できることを確認することで構成されます。

  • これらのテストはプログラムロジックをまったくテストしません。1つのリソース(文字列)が別のリソースと同等であることをテストするだけです。
  • テストは脆弱です。ログメッセージのフォーマットを微調整しても、テストは中断されます。
  • テストは、ロギングインターフェースの国際化(翻訳)と互換性がありません。テストでは、可能なメッセージテキストは1つだけであるため、人間の言語は1つだけであると想定しています。

テキストロギングインターフェイスを直接呼び出すプログラムコード(おそらくいくつかのビジネスロジックを実装する)を使用すると、設計が不十分になります(ただし、残念ながら非常に使い古されています)。ビジネスロジックを担当するコードは、ロギングポリシーとログメッセージのテキストも決定します。ビジネスロジックとユーザーインターフェイスコードを組み合わせます(はい、ログメッセージはプログラムのユーザーインターフェイスの一部です)。それらは別のものでなければなりません。

したがって、ビジネスロジックがログメッセージのテキストを直接生成しないことをお勧めします。代わりに、ロギングオブジェクトに委任します。

  • ロギングオブジェクトのクラスは、適切な内部APIを提供する必要があります。これは、ビジネスオブジェクトが、テキスト文字列ではなく、ドメインモデルのオブジェクトを使用して発生したイベントを表現するために使用できます。
  • ロギングクラスの実装は、これらのドメインオブジェクトのテキスト表現を生成し、イベントの適切なテキスト記述をレンダリングし、そのテキストメッセージを低レベルのロギングフレームワーク(JUL、log4j、slf4jなど)に転送します。
  • ビジネスロジックは、ロガークラスの内部APIの正しいメソッドを呼び出し、正しいドメインオブジェクトを渡して、発生した実際のイベントを記述することのみを担当します。
  • 具体的なログクラスimplementsおよびinterface。ビジネスロジックで使用できる内部APIを記述します。
  • ビジネスロジックを実装し、ロギングを実行する必要があるクラスには、委任するロギングオブジェクトへの参照があります。参照のクラスは抽象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
Raedwald

言及する価値のあるもう1つのアイデアは、古いトピックですが、ロガーを挿入する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
GregD

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
0
robbo