258

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

4

31 に答える 31

157

私もこれを数回必要としました。以下に、ニーズに合わせて調整したい小さなサンプルをまとめました。基本的に、独自に作成して、必要なAppenderロガーに追加します。すべてを収集したい場合は、ルートロガーから始めるのが良いでしょうが、必要に応じて、より具体的なものを使用することもできます。完了したら、アペンダーを削除することを忘れないでください。そうしないと、メモリリークが発生する可能性があります。以下で私はテスト内でそれを行いましたが、あなたのニーズに応じて、setUpまたは@BeforeおよびtearDownまたはより良い場所かもしれません。@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);
    }
}
于 2009-12-01T19:20:40.917 に答える
44

これらの(驚くべきことに)迅速で役立つ回答に感謝します。彼らは私の解決策のために私を正しい道に導いてくれました。

コードベースは私がこれを使いたいと思っていて、そのロガーメカニズムとして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の準備では、を作成し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メソッドに移し、その他のさまざまな改善を行うことになりますが、それではこのプレゼンテーションが煩雑になります。)

于 2009-12-02T18:14:51.373 に答える
21

事実上、依存クラスの副作用をテストしています。単体テストでは、次のことを確認するだけで済みます

logger.info()

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

于 2009-12-01T17:33:44.370 に答える
12

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

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

于 2009-12-01T17:58:07.430 に答える
12

@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 をもっとスマートな方法で使用できるようにすることもできますが、私はそのままにしました。

于 2015-11-27T15:47:16.773 に答える
5

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

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(), "...");
于 2013-04-03T05:39:15.627 に答える
5

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

もう 1 つの解決策は、手動で偽のロガーを作成することです。偽のロガー (追加のフィクスチャ コード) を作成する必要がありますが、この場合は、モック フレームワークから保存されたコードに対するテストの読みやすさを向上させたいと考えています。

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

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());
    }
}
于 2009-12-01T19:04:46.543 に答える
4

Log4J 2.x では、パブリック インターフェイスに メソッドとメソッドorg.apache.logging.log4j.Loggerが含まれていないことに注意してください。setAppender()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);
于 2019-12-10T19:11:40.237 に答える
3

私に関しては、 with を使用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)
        );
    }
}

そのため、さまざまなメッセージ量のテストに柔軟に対応できます

于 2017-04-19T18:00:11.483 に答える
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.    
}
于 2016-02-10T10:20:20.437 に答える
0

Appender をモックすると、ログ行をキャプチャするのに役立ちます。サンプルを探す: 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")));
}
于 2016-12-12T10:01:19.780 に答える
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();
}
于 2015-08-06T08:45:57.473 に答える