web-dev-qa-db-ja.com

PowerMockおよびMockitoを使用したLoggerおよびLoggerFactoryのモック

私はモックアウトしたい次のロガーを持っていますが、コンテンツではなくログエントリが呼び出されていることを検証します。

_private static Logger logger = 
        LoggerFactory.getLogger(GoodbyeController.class);
_

LoggerFactory.getLogger()に使用されるクラスをモックしたいのですが、その方法を見つけることができませんでした。これは私がこれまでに終わったものです:

_@Before
public void performBeforeEachTest() {
    PowerMockito.mockStatic(LoggerFactory.class);
    when(LoggerFactory.getLogger(GoodbyeController.class)).
        thenReturn(loggerMock);

    when(loggerMock.isDebugEnabled()).thenReturn(true);
    doNothing().when(loggerMock).error(any(String.class));

    ...
}
_

私が知りたいのですが:

  1. 静的LoggerFactory.getLogger()をモックして、任意のクラスで動作することはできますか?
  2. _@Before_でwhen(loggerMock.isDebugEnabled()).thenReturn(true);のみを実行しているように見えるため、メソッドごとに特性を変更できないようです。これを回避する方法はありますか?

調査結果の編集:

私はすでにこれを試したが、うまくいかないと思った:

_ when(LoggerFactory.getLogger(any(Class.class))).thenReturn(loggerMock);
_

しかし、うまくいきましたので、ありがとうございます。

しかし、私は無数のバリエーションを試しました:

_when(loggerMock.isDebugEnabled()).thenReturn(true);
_

LoggerMockに_@Before_以外の動作を変更させることはできませんが、これはCoburturaでのみ発生します。 Cloverの場合、カバレッジは100%を示しますが、いずれにしても問題があります。

この単純なクラスがあります:

_public ExampleService{
    private static final Logger logger =
            LoggerFactory.getLogger(ExampleService.class);

    public String getMessage() {        
    if(logger.isDebugEnabled()){
        logger.debug("isDebugEnabled");
        logger.debug("isDebugEnabled");
    }
    return "Hello world!";
    }
    ...
}
_

次に、このテストがあります:

_@RunWith(PowerMockRunner.class)
@PrepareForTest({LoggerFactory.class})
public class ExampleServiceTests {

    @Mock
    private Logger loggerMock;
    private ExampleServiceservice = new ExampleService();

    @Before
    public void performBeforeEachTest() {
        PowerMockito.mockStatic(LoggerFactory.class);
        when(LoggerFactory.getLogger(any(Class.class))).
            thenReturn(loggerMock);

        //PowerMockito.verifyStatic(); // fails
    }

    @Test
    public void testIsDebugEnabled_True() throws Exception {
        when(loggerMock.isDebugEnabled()).thenReturn(true);
        doNothing().when(loggerMock).debug(any(String.class));

        assertThat(service.getMessage(), is("Hello null: 0"));
        //verify(loggerMock, atLeast(1)).isDebugEnabled(); // fails
    }

    @Test
    public void testIsDebugEnabled_False() throws Exception {
        when(loggerMock.isDebugEnabled()).thenReturn(false);
        doNothing().when(loggerMock).debug(any(String.class));

        assertThat(service.getMessage(), is("Hello null: 0"));
        //verify(loggerMock, atLeast(1)).isDebugEnabled(); // fails
    }
}
_

クローバーでは、if(logger.isDebugEnabled()){ブロックの100%カバレッジを示しています。しかし、loggerMockを検証しようとすると:

_verify(loggerMock, atLeast(1)).isDebugEnabled();
_

インタラクションはゼロです。 PowerMockito.verifyStatic()も試しました。 _@Before_にありますが、インタラクションもありません。

これは、Coberturaがif(logger.isDebugEnabled()){を100%完全ではなく、Cloverが示すように見えるのは奇妙に思えますが、両方とも検証が失敗することに同意します。

53
Mick Knutson

@Mick、静的フィールドの所有者も準備してみてください。例:

@PrepareForTest({GoodbyeController.class, LoggerFactory.class})

EDIT1:ちょっとした例を作りました。まずコントローラー:

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

public class Controller {
    Logger logger = LoggerFactory.getLogger(Controller.class);

    public void log() { logger.warn("yup"); }
}

次にテスト:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Controller.class, LoggerFactory.class})
public class ControllerTest {

    @Test
    public void name() throws Exception {
        mockStatic(LoggerFactory.class);
        Logger logger = mock(Logger.class);
        when(LoggerFactory.getLogger(any(Class.class))).thenReturn(logger);

        new Controller().log();

        verify(logger).warn(anyString());
    }
}

インポートに注意してください!クラスパスの注目すべきライブラリ:Mockito、PowerMock、JUnit、logback-core、logback-clasic、slf4j


EDIT2:よくある質問のように思えるので、これらのログメッセージがそれほど重要な場合であり、テストする必要がある、つまりシステムの機能/ビジネス部分であるということを指摘したいと思います- そして、これらのログが機能であることを明確にする実際の依存関係を導入することは、システム全体の設計において非常に優れているでしょう、ロガーの標準および技術クラスの静的コードに依存する代わりに。

この問題については、ReporterreportIncorrectUseOfYAndZForActionXなどのメソッドを持つreportProgressStartedForActionXクラスのようなものを作成することをお勧めします。これには、コードを読むすべての人がこの機能を表示できるという利点があります。ただし、テストの実施、この特定の機能の実装の詳細の変更にも役立ちます。

したがって、PowerMockのような静的なモックツールは必要ありません。 私の意見では、静的コードは問題ない可能性がありますが、テストで静的動作を検証またはモックする必要があるとすぐに、明確な依存関係をリファクタリングして導入する必要があります。

63
Brice

パーティーにやや遅れました-私は似たようなことをしていて、いくつかのポインタが必要でしたが、ここで終わりました。クレジットなし-Briceからすべてのコードを取得しましたが、Cengizが取得した「ゼロインタラクション」を取得しました。

AMD Joseph Lustが提供したjheriksからのガイダンスを使用して、私はその理由を知っていると思います。その後、実際のロガーはモックではなく、jhriksが示唆したように実際のクラスの初期化でした...

通常、テスト対象のオブジェクトに対してこれを行い、各テストで新しいオブジェクトを取得します。フィールドをローカルに移動し、テストでフィールドを更新すると、問題なく実行されました。ただし、2番目のテストを試した場合、それは私のテストの模擬ではなく、最初のテストの模擬であり、相互作用はゼロになりました。

モックの作成を@BeforeClassに入れると、テスト対象のオブジェクトのロガーは常にモックになりますが、これに関する問題については以下のメモを参照してください...

テスト対象のクラス

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

public class MyClassWithSomeLogging  {

    private static final Logger LOG = LoggerFactory.getLogger(MyClassWithSomeLogging.class);

    public void doStuff(boolean b) {
        if(b) {
            LOG.info("true");
        } else {
            LOG.info("false");
        }

    }
}

テスト

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.mockito.Mockito.*;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.*;
import static org.powermock.api.mockito.PowerMockito.when;


@RunWith(PowerMockRunner.class)
@PrepareForTest({LoggerFactory.class})
public class MyClassWithSomeLoggingTest {

    private static Logger mockLOG;

    @BeforeClass
    public static void setup() {
        mockStatic(LoggerFactory.class);
        mockLOG = mock(Logger.class);
        when(LoggerFactory.getLogger(any(Class.class))).thenReturn(mockLOG);
    }

    @Test
    public void testIt() {
        MyClassWithSomeLogging myClassWithSomeLogging = new MyClassWithSomeLogging();
        myClassWithSomeLogging.doStuff(true);

        verify(mockLOG, times(1)).info("true");
    }

    @Test
    public void testIt2() {
        MyClassWithSomeLogging myClassWithSomeLogging = new MyClassWithSomeLogging();
        myClassWithSomeLogging.doStuff(false);

        verify(mockLOG, times(1)).info("false");
    }

    @AfterClass
    public static void verifyStatic() {
        verify(mockLOG, times(1)).info("true");
        verify(mockLOG, times(1)).info("false");
        verify(mockLOG, times(2)).info(anyString());
    }
}

同じ期待のテストが2つある場合、@ AfterClassで検証を行う必要がありました。2番目のテストのように各テストでtimes(1)ではなく、静的での呼び出しが積み重ねられるためです-verify(mockLOG, times(2)).info("true");これを2回呼び出すと、そこに言って失敗します。これはかわいいパンツですが、呼び出しをクリアする方法が見つかりませんでした。私は誰かがこれを回避する方法を考えることができるかどうか知りたいです...

15
user1276925

最初の質問に答えるには、次のように簡単に置き換える必要があります。

   when(LoggerFactory.getLogger(GoodbyeController.class)).thenReturn(loggerMock);

   when(LoggerFactory.getLogger(any(Class.class))).thenReturn(loggerMock);

2番目の質問(およびおそらく1番目の質問での不可解な動作)に関して、問題はロガーが静的であるということです。そう、

private static Logger logger = LoggerFactory.getLogger(GoodbyeController.class);

classが初期化されたときに実行され、objectがインスタンス化。これはほぼ同時に行われる場合があるため、大丈夫ですが、それを保証するのは困難です。そのため、モックを返すようにLoggerFactory.getLoggerを設定しましたが、モックが設定されるまでにロガー変数が実際のLoggerオブジェクトですでに設定されている場合があります。

ReflectionTestUtils (静的フィールドで機能するかどうかはわかりません)のようなものを使用してロガーを明示的に設定するか、静的フィールドからインスタンスフィールドに変更できます。いずれにしても、LoggerFactory.getLoggerをモックする必要はありません。これは、Loggerのモックインスタンスを直接インジェクトするからです。

5
jhericks

Mockito.reset(mockLog)を使用して呼び出しをリセットできると思います。すべてのテストの前にこれを呼び出す必要があります。したがって、@ Beforeの内部が適切な場所になります。

2
Markus Wendl

明示的な注入を使用します。他のアプローチでは、たとえば、同じJVMでテストを並行して実行することはできません。

静的なログバインダーのようにクラスローダー全体を使用するパターンや、環境を乱すことはlogback.XMLのように考えられますが、テストに関しては破綻します。

私が言及した並列化されたテストを検討するか、または構成がAPI Bの背後に隠されているコンポーネントAのロギングをインターセプトしたいケースを検討してください。 ILoggerFactory.getLoggerでこのアセンブリにシームがないためロガーを注入する場合。

そして、単体テストについてのすべてではありません。統合テストでログを出力したい場合があります。時々しない。統合テストのロギングの一部を選択的に抑制したい場合があります。たとえば、CIコンソールが混乱して混乱するような予期されるエラーの場合です。メインラインの上部からILoggerFactoryを注入する場合(または使用する可能性のあるdiフレームワーク)はすべて簡単です

そう...

推奨どおりレポーターを挿入するか、ILoggerFactoryを挿入するパターンを採用します。ロガーではなく明示的なILoggerFactoryインジェクションにより、多くのアクセス/インターセプトパターンと並列化をサポートできます。

0
johnlon