ロガーのメッセージで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が望むロガーにそれを追加します。すべてを収集したい場合は、ルートロガーから始めるのが適切ですが、必要に応じてより具体的なものを使用できます。完了したらアペンダーを削除することを忘れないでください。そうしないと、メモリリークが発生する可能性があります。以下では、テスト内でそれを行いましたが、setUpまたは@BeforeおよびtearDownまたはまたは@After、ニーズに応じて、より良い場所かもしれません。

また、以下の実装Listはメモリ内のすべてを収集します。大量のログを記録している場合は、退屈なエントリを削除したり、ディスク上の一時ファイルにログを書き込んだりするためにフィルターを追加することを検討することをお勧めします(ヒント:LoggingEventです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 2013年

1
:あなたがプラグインを作成する必要があるとして、Log4jの2.xでは、もう少しこれを見てい、畳み込むstackoverflow.com/questions/24205093/...
paranza

1
これをありがとう。ただし、LogBackを使用している場合はListAppender<ILoggingEvent>、独自のカスタムアペンダーを作成する代わりにを使用できます。
sinujohn 2017年

2
しかし、これはslf4jでは機能しません!どうすればそれで動作するように変更できますか?
Shilan

3
@sd Loggerto org.apache.logging.log4j.core.Logger(インターフェースの実装クラス)をキャストすると、setAppender()/removeAppender()再びアクセスできるようになります。
David Moles

59

これがシンプルで効率的な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
        // 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としてのマッチャー/アサーションライブラリは、そのために適切に表示されます。

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

@Gilterasわかりません。エラーを記録しても、テストが失敗することはありません。あなたは何を説明しますか?
davidxxx 2018

また、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-testの実行では)不要なロギングが発生しなくなります。テスト対象のコードがこのロガーを使用するために必要なことは何でも行い、テストを実行してassertEqualityを実行します。

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

(もちろん、この作業の大部分を@Beforeメソッドに移動して、他のさまざまな改良を加えますが、それではこのプレゼンテーションが乱雑になります。)


16

別のオプションは、アペンダーをモックして、メッセージがこのアペンダーに記録されたかどうかを確認することです。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
ほとんどのロガーが定義されているプラ​​イベート静的最終フィールドをどのようにモック化しましたか?Powermockito?楽しい..持っている
ステファノ・L

ステファノ:ファイナルフィールドが何らかの形で初期化されました。本物ではなく、モックを注入するさまざまなアプローチを見てきました。おそらく、そもそもテストしやすさのためにある程度の設計が必要です。blog.codecentric.de/en/2011/11/...
djna

Mehdiが言ったように、おそらく適切なハンドラーを使用するだけで十分かもしれません
djna

11

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

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


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でアペンダーを作成しても、複数のログメッセージで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メソッドが定義されている場所がわかりません?!?
バイオインフォマティクス

getAppenderはch.qos.logback.classic.Loggerのメソッドです
kfox

4

Junit 5(Jupiter)の場合、SpringのOutputCaptureExtensionは非常に便利です。Spring Boot 2.2以降で利用可能で、spring-boot-testで利用可能アーティファクトでます。

例(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
ほぼ同じコードブロックを繰り返さないようにするには、Log4j2でほぼ1to1が機能することを追加します。インポートを "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.LoggersetAppender()およびが含まれていないことに注意してください。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

言及する価値のあるもう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

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

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

以下のコードを使用してください。ロギングにlog backを使用している私のスプリング統合テストに同じコードを使用しています。メソッド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

テストしようとしていることが2つあります。

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

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

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

プログラムコード(ビジネスロジックを実装している可能性があります)からテキストロギングインターフェイスを直接呼び出すのは設計が悪いことに注意してください(残念ながら非常に一般的です)。ビジネスロジックを担当するコードは、いくつかのログポリシーとログメッセージのテキストも決定します。ビジネスロジックとユーザーインターフェイスコードを組み合わせます(そうです、ログメッセージはプログラムのユーザーインターフェイスの一部です)。それらは別々にすべきです。

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

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

私がやりたいのは、(あまりにも壊れやすい正確なログステートメントを確認するのではなく)いくつかの文字列がログに記録されていることを確認することです。

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.loggingSystem.setErr(new PrintStream(buffer));stderrにログを記録するため、を使用しましたが)でこれを試しましたが、機能しません(バッファーが空のままです)。私がSystem.err.println("foo")直接使用する場合、それは機能します。したがって、ロギングシステムは、それから取得する出力ストリームの独自の参照を保持していると想定しますSystem.err。したがって、への呼び出し System.setErr(..)は、ログシステムの初期化後に発生するため、ログ出力に影響を与えません。
ホイジュイ

0

私はlog4jについて同様の質問に回答しましたhow-can-i-test-with-junit-that-a-warning-was-logged-with-log4

これはより新しく、Log4j2(2.11.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());の行でエラーが発生しました。エラーはorg.apache.logging.log4j.spi.LoggerContextShutdownEnabledのorg.apache.logging.log4j.spi.LoggerContextShutdownEnabledクラスファイルにアクセスできない
カルロスパルマ

コードを確認し、いくつかの小さな変更を加えましたが、うまくいきました。依存関係をチェックし、すべてのインポートが正しいことを確認することをお勧めします
Haim Raman

こんにちはハイム。私は最終的にlogbackソリューションを実装しました...しかし、あなたが正しいと思います。それを実装するには、別の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.