3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Web/まとめ] JUnit5 で Mockitoを利用する方法

Last updated at Posted at 2021-06-20

背景

現在、担当中のjava moduleで Spring 2.3系から Spring2.5へmigrationを行なっている。
その際に、test周り、特にmockitoの移行で若干ハマったのでメモ。

特に、以下の英語LINKがよくまとまっていたので、この内容を元に要約してみる。
https://mincong.io/2020/04/19/mockito-junit5/

なお、自分のMockito + Spring boot test frameworkの知識補充のために記載しているので、WEB Pageの丸翻訳というわけではなく、自分なりの理解や補足を加筆している。 なので、LINKの中で初めて知ること、わからないメソッドなどは適宜調べて自分なりのオリジナルまとめとなっている。

Junit5で Mockitoを利用する方法

以下の3つの方法があり、各方法での実装例や方法だけではなく、メリデメについてもリストしていく。

  • MockitoExtension
  • MockitoAnnotations#initMocks
  • Mockito#mock

実装の前に、、、

Mockitoそのものを利用するために、以下のDependencyをプロジェクト/モジュールの pom.xml などに追加する必要がある。

追加例 (versionは適宜最新のものにUpdateすること):

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>2.28.2</version>
  <scope>test</scope>
</dependency>
注意

もし、Spring bootのstarter moduleを利用している場合、特に spring-boot-starter-test が test dependencyとして追加されている場合は、上記モジュールはすでに依存関係に追加されているので、明示的に追加しなくてもビルドパスに追加される。

例: pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

上記dependencyが追加されている状態で、mvn dependency:treeなどを実行してみると、、、。

image.png

添付のように spring-boot-starter-test からの依存として、mockito-coreがモジュールのテストライブラリとして自動的に追加されていることがわかる。

なので、Spring boot testを利用している場合は、明示的にインポートを行う必要はない!

方法1. MockitoExtension

概要

  • JUnit5で新規追加されたExtention APIを用い、Mockito用のExtentionを @ExtendWith を使い、組み込む
  • @Mock アノテーションを使い、Mockオブジェクトを定義する

実装例

@ExtendWith(MockitoExtension.class)
class BookReaderAnnotationWithExtensionTest {

  @Mock private Book mockedBook;

  private BookReader reader;

  @BeforeEach
  void setUp() {
    reader = new BookReader(mockedBook);
  }

  @Test
  void testGetContent() {
    Mockito.when(mockedBook.getContent()).thenReturn("Mockito");
    assertEquals("Mockito", reader.getContent());
  }
}

(補足) JUnit5 Extention API & JUnit4 Runnerからの移行

JUnit4では、Mockito(特に @Mockアノテーション)を利用する場合には、Runnerなどの拡張実装を利用する必要があった。 が、JUnit5からはExtention APIを利用することになるため、@Extention を代わりに利用する必要がある。

JUnit4のRunnerに関しては、以下のLINKを読んでざっと理解した。
https://qiita.com/YutaKase6/items/1f3ca15900b5146351de

  • Mockitoの @Mock アノテーションを利用するためには MockitoJUnitRunner を指定しなければならない

また、Junit4 Runner から Junit5 Extentionへの移行に関しては、以下のLINKで概要を理解。
https://www.m3tech.blog/entry/2018/12/20/junit5

テストの拡張機能
JUnit4ではテストの事前処理や事後処理などを汎用的に実装するための機能として、@RunWithや@Ruleなどのテスト拡張機能がありました。 JUnit5では、これらのテスト拡張機能が廃止され、@ExtendWithに置き換わっています。

@RunWithは、1クラスに1つしか指定できなかったため、複数の拡張を同時に使用することができませんでした。そのため、Springのための@RunWith(SpringRunner.class)を使うと@RunWith(Parameterized.class)が使えないなど、便利な機能を上手く活かしずらいことがありました。 @ExtendWithは同じクラスに複数指定することができ、この手の問題が解消されました。

また、同英語記事内では、上記引用と同じポイントについても述べている (MockitoExtensionを利用した場合の特徴 => 複数のExtentionを指定することができる を参考)。

MockitoExtensionを利用した場合の特徴 & Point :thumbsup:

Point1. 複数のExtentionを指定することができる

JUnit4では Runnerは 1つのみ指定可能、複数指定不可 である一方、Junit5 Extentionからは 複数指定が可能となり、複数のExtentionをTest classへ追加することができるようになった (そしてこれは便利 :smile:とも)。

Point2. MockitoAnnotations#initMocks(Object) を呼ばすとも、@Mock の自動初期化が可能

@Mock をテストケースごとに登録したStub挙動をリセットさせる必要がある。通常は、MockitoAnnotations#initMocks(Object) メソッドを @BeforeEach 内などで実行する必要がある。
が、MockitoExtensionを利用すると、暗黙的に実行してくれる。

:warning: MockitoAnnotations.initMocks(this)については、当ページのMock作成の方法2でも再度説明する

Point3. 実装とは関係のない余計なStub定義がないか自動で検知をしてくれる

詳細は Mockito#validateMockitoUsage()のJavaDoc を参照せよとのこと。

どういうことかというと、上記の実装例で見てみると、

もし以下のテストロジックで、 実際にテストするメソッド reader.getContent() 内で  Mockito.when で定義した mockedBook.getContent() 実は全然呼ばれていなかったとする

  @Test
  void testGetContent() {
    Mockito.when(mockedBook.getContent()).thenReturn("Mockito");
    assertEquals("Mockito", reader.getContent());
    verify(mockedBook, times(1)).getContent();

  }

すると、mockitoは、以下のようなエラーをthrowしてくれる。

-> at …testGetContent(BookReaderAnnotationWithExtensionTest.java:38))
Please remove unnecessary stubbings or use ‘lenient’ strictness. More info: javadoc for UnnecessaryStubbingException class.”

そうすることで各テストケース内で必要最低限のStub実装のみが定義されていることが保証され、テストコードがneatかつ読みやすくなる :tada:

**ちなみに、この辺、自分は結構きちんと理解しにくかったので、[別Quita Page](https://qiita.com/northernbird/items/667fa37319c3cc0d085a) にサンプル実装例をつけてまとめた。**

詳細は、以下のLINKも参照した。
https://qiita.com/kazuki43zoo/items/8762927ed182878eb58c
(lenient モードに設定するなどして、このエラーをinactiveにすることができるらしい)

Point4. 利用のために、別途ライブラリを追加でビルドパスへ追加する必要がある

以下のDependencyを pom.xml などへ追加する必要がある。

追加例(versionは適宜最新のものにUpdateすること):

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>2.28.2</version>
  <scope>test</scope>
</dependency>
:warning:注意:warning:

mockito-core と同じで、もし Spring test frameworkを利用している場合は、デフォルトでDependencyとして上記モジュールは追加されているので、何もしなくてもよし:exclamation:

もし、Spring bootのstarter moduleを利用している場合、特に spring-boot-starter-test が test dependencyとして追加されている場合は、上記モジュールはすでに依存関係に追加されているので、明示的に追加しなくてもビルドパスに追加される。

例: pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

上記dependencyが追加されている状態で、mvn dependency:treeなどを実行してみると、、、。
image.png

添付のように spring-boot-starter-test からの依存として、mockito-coreがモジュールのテストライブラリとして自動的に追加されていることがわかる。

なので、Spring boot testを利用している場合は、明示的にインポートを行う必要はない!

Junit5 Extention APIでのPros&Consまとめ

Pros
  • mockの手動初期化が必要ない (MockitoAnnotations#initMocks(Object) がいらない)
  • Stubのverifyが正しく行われているかチェックしてくれる
  • @Mock アノテーションを mock変数に追加させることで、どの変数がmockなのかがとてもわかりやすくなる
  • Mockを簡単に作成し、簡単に利用することができる
  • @ExtendWith が複数指定可能なので、他の任意のExtentionも併用することができるため、便利
Cons
  • org.mockito:mockito-junit-jupiter ライブラリを別途追加しなければならない
    • ただし、上記 :warning:注意:warning: にも記載した通り、Spring boot frameworkを使っている場合はその心配をする必要はない (自動的に依存ライブラリとして透過的に組み込まれるため)

方法2. MockitoAnnotations.initMocks

概要

  • @MockアノテーションでMockオブジェクトを作成する
  • 各テストケース実行前に MockitoAnnotations#initMocks(Object)メソッドを手動で実行し、Mock objectの初期化を行う
  • 上記 方法1. MockitoExtention で説明した通り、手動でinitMocksをコールするため、 MockitoExtentionは使わない

実装例

// you don't need: @ExtendWith(MockitoExtension.class)
class BookReaderAnnotationWithSetupTest {

  private BookReader reader;

  @Mock private Book mockedBook;

  @BeforeEach
  void setUp() {
    MockitoAnnotations.initMocks(this);
    reader = new BookReader(mockedBook);
  }

  @Test
  void testPrintContent() {
    mockedBook.printContent();
    Mockito.verify(mockedBook).printContent();
  }

  @Test
  void testGetContent() {
    Mockito.when(mockedBook.getContent()).thenReturn("Mockito");
    assertEquals("Mockito", reader.getContent());
  }
}

ちなみに、MockitoのJavaDoc での実装例の記載しておく。

親クラス SampleBaseTestCaseMockitoAnnotations.initMocks(this) が毎テストケース実行前にコールされていることがわかる。

public class ArticleManagerTest extends SampleBaseTestCase {

       @Mock private ArticleCalculator calculator;
       @Mock(name = "database") private ArticleDatabase dbMock;
       @Mock(answer = RETURNS_MOCKS) private UserProvider userProvider;
       @Mock(extraInterfaces = {Queue.class, Observer.class}) private  articleMonitor;

       private ArticleManager manager;

       @Before public void setup() {
           manager = new ArticleManager(userProvider, database, calculator, articleMonitor);
       }
   }

   public class SampleBaseTestCase {

       @Before public void initMocks() {
           MockitoAnnotations.initMocks(this);
       }
   }

MockitoAnnotations.initMocks を利用した場合の特徴 & Point :thumbsup:

Point1. @Mockを利用することでコードが読みやすくなる + Mock Objectの初期化が簡単

上記initMocksに関するJavaDocの以下テキストより、、

MockitoAnnotations.initMocks(this); initializes fields annotated with Mockito annotations.
Allows shorthand creation of objects required for testing.
Minimizes repetitive mock creation code.
Makes the test class more readable.
Makes the verification error easier to read because field name is used to identify the mock.

以下のメリットがあることがわかる。

  • Mockオブジェクトが簡単に初期化&生成できる
  • Mock作成の手動コードを省略することができる ( initMocks(this)が全てやってくれるので)
  • テストコードが読みやすい
    -@Mockがマーキングの様な役目を果たしてくれるので、開発者は一見でどのオブジェクトがMockなのかがすぐにわかる (そうではないと、どこかで mock()みたいなメソッドをコールしているか探して判別しなければならず、面倒)
  • Mockがクラスのインスタンス変数として定義されているので、mockのverification errorなどが発生した時のトラブルシューティング/デバッグが楽
**ちなみに `方法1. MockitoExtention` では、すでに上記メリットを兼ね備えた上に、`initMocks`メソッド自体を手動で呼ぶ必要がなくなるので、`方法1. MockitoExtention`の方がさらに洗練された実装となっていることがわかる**

上記を踏まえて、Pros & Consをまとめてみると、、

Pros
  • @Mock アノテーションを mock変数に追加させることで、どの変数がmockなのかがとてもわかりやすくなる
  • Mockを簡単に作成し、簡単に利用することができる
Cons
  • 方法1. MockitoExtention と比べて、Stubのverifyが正しく行われているかチェックまでしてくれない

方法3. Mockito.mock

概要

  • Extention APIや Mockito frameworkに頼らず、全て手動でコードする
  • Mockito#mock(Class<T> classToMock) メソッドを任意のコードで呼ぶことで mock objectを作成する
  • サッと紹介されて一番面倒臭そう + お勧めではないな実装には見えるが、テストが非同期で実行される場合は、このオプションは有用だと思う

実装例

class BookReaderClassicMockTest {

  private BookReader reader;
  private Book mockedBook;

  @BeforeEach
  void setUp() {
    mockedBook = Mockito.mock(Book.class);
    reader = new BookReader(mockedBook);
  }

  @Test
  void testPrintContent() {
    mockedBook.printContent();
    Mockito.verify(mockedBook).printContent();
  }

  @Test
  void testGetContent() {
    Mockito.when(mockedBook.getContent()).thenReturn("Mockito");
    assertEquals("Mockito", reader.getContent());
  }
}

Mockito.mock を利用した場合の特徴 & Point :thumbsup:

Point1. 非同期テスト実行に強い

方法1. MockitoExteption方法2. MockitoAnnotations.initMocks では、共に以下の共通点がある。

  • @Mockアノテーションを使い、 インスタンス変数としてMockオブジェクトを定義する
    • つまり、各テストケースがマルチスレッドでインスタンス変数として定義されたMockオブジェクトを共有し、使い回すことになったらどうするのだろう。。。 :disappointed_relieved:
  • JUnitは基本デフォルトでは、スレッドセーフ、同期実行であるものの、もし複数テストケースを(ビルド時間の軽減などのために)非同期で実行したらどうなるのだろう??
**ちなみに、私は、テストはいつでも非同期実行に対応すべきよう実装するべきものと考えている。** 例えば、JSにおけるJESTなどは非同期実行がデフォルトである、またこの[stackoverflow link](https://stackoverflow.com/questions/7267790/does-junit-execute-test-cases-sequentiallyl)の意見に大大大賛成 :thumbsup: である)

But don't forget that:
Good automated tests should be independent, isolated and reproducible, making them ideal candidates for being run concurrently.

なので、気になって調べてみたところ、以下のLINKが役立った。

まとめると

  • Mockitoで作成されたMock Objectが概してMulti threadに問題なく対応できるように実装されている
  • が、複数Threadが一つのMock Objectを共有し、使いまわしており、さらにそのMock objectに対して verifyをかけて、各Thread内で検証を行おうとする場合は、正しく動作しない
    • そのような実装では、以下の問題が発生する可能性がある
      • そもそもテストとして正しい実装方法ではない (Single Threadに対して、特定のMock Objectが作成されるべきであり、複数のThreadでシェアされるべきものではない)
      • 複数のスレッドから共有されたMock Objectが各スレッドごとに変わるようなMock挙動を定義され、それに対してassertionを行うような場合、Mockは期待された挙動をせず、ランダムな挙動をするかもしれない
      • WrongTypeOfReturnValue みたいなExceptionがthrowされるかもしれない
**なので、私の懸念はドンピシャで、`方法1. MockitoExtention` & `方法2. MockitoAnnotations.initMocks` はマルチスレッドで並列実行されるテストの場合には、正しく動作することができないはず。** **そして、その時の救世主となるのが、この方法での実装である!!**

例えば、テストケース内のローカル変数としてmockを定義すれば、そのMock objectはスレッドセーフとなり、安全にテストを実行することができる。 ただ、当然、例の通り、コードがrepeatable/冗長化してしまうことがわかるが。。 :sweat:

スレッドセーフにしたテスト実装例:

class BookReaderClassicMockThreadSafeTest {

  @Test
  void testPrintContent() {
    Book mockedBook = Mockito.mock(Book.class);
    BookReader reader = new BookReader(mockedBook);
    mockedBook.printContent();
    Mockito.verify(mockedBook).printContent();
  }

  @Test
  void testGetContent() {
    Book mockedBook = Mockito.mock(Book.class);
    BookReader reader = new BookReader(mockedBook);
    Mockito.when(mockedBook.getContent()).thenReturn("Mockito");
    assertEquals("Mockito", reader.getContent());
  }
}

上記を踏まえて、Pros & Consをまとめてみると、、

Pros
  • Mock Object作成をフレームワークに依存しないので、Mock Objectのコントロールをより実装者が管理できる
    • 例えば、上記に示したようにスレッドセーフにするため常にローカル変数で定義するとか。。
  • Mock Objectをインスタンス変数以外として好きなように定義する
    • 例えば、上記に示したようにスレッドセーフにするため常にローカル変数で定義するとか。。
Cons
  • コードが冗長になる
  • Mock Objectをクリーンかつ読みやすく定義できない
    • 方法1 & 方法2 では @Mock アノテーションを使い Mock Objectとしてマーキングもできた
  • 方法1. MockitoExtention と比べて、Stubのverifyが正しく行われているかチェックまでしてくれない

会社のプロジェクトではどれを使うか。。。 :thinking:

とても悩むところではあるが、うちの会社の事情を考えると、(IT系スタートアアップとか、IT設備やCI/CDに力入れて潤沢にマシンリソースが使える会社は違うんだろうなぁ。。。と思いつつ。。。 :sweat_smile: )

  • Build CI/CDで Jenkinsを使っているものの、割り当てられてメモリやマシンリソースが少ない :disappointed:
    • 可能な限りスレッドを発生させるなと言われ、mockserverも各テストケースごとではなく、テストクラスごとに起動させるよう言われた (そうでないと、 java.lang.OutOfMemoryError: unable to create new native thread. が頻発しちゃった)
    • なので、(Java/Spring boot projectに関しては)テストを非同期で行うよう拡張することは今現在、あまり考えられにくい。。 :disappointed:

上記事情から考えると、

  • テストは同期/シングルスレッド/シーケンシャルに実行する前提で進める
  • すると、 方法3. Mockito.mock はテストコードの冗長化を促進するだけであまりメリットはない
  • とすると、方法1. MockitoExtentionを利用し、かつ github issue とソースコードコメントなどに、非同期環境での実装は対応していないとドキュメントする方法が一番良いのではないか?
    • 非同期に対応していない、ということは絶対にどこかへ記録しておいた方が良いはず
3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?