背景
現在、担当中の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
などを実行してみると、、、。
添付のように 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
Point1. 複数のExtentionを指定することができる
JUnit4では Runnerは 1つのみ指定可能、複数指定不可 である一方、Junit5 Extentionからは 複数指定が可能となり、複数のExtentionをTest classへ追加することができるようになった (そしてこれは便利 とも)。
Point2. MockitoAnnotations#initMocks(Object)
を呼ばすとも、@Mock
の自動初期化が可能
各 @Mock
をテストケースごとに登録したStub挙動をリセットさせる必要がある。通常は、MockitoAnnotations#initMocks(Object)
メソッドを @BeforeEach
内などで実行する必要がある。
が、MockitoExtension
を利用すると、暗黙的に実行してくれる。
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かつ読みやすくなる
**ちなみに、この辺、自分は結構きちんと理解しにくかったので、[別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>
注意
mockito-core
と同じで、もし Spring test frameworkを利用している場合は、デフォルトで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
などを実行してみると、、、。
添付のように 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
ライブラリを別途追加しなければならない- ただし、上記 注意 にも記載した通り、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 での実装例の記載しておく。
親クラス SampleBaseTestCase
で MockitoAnnotations.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
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などが発生した時のトラブルシューティング/デバッグが楽
上記を踏まえて、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
Point1. 非同期テスト実行に強い
方法1. MockitoExteption
と 方法2. MockitoAnnotations.initMocks
では、共に以下の共通点がある。
-
@Mock
アノテーションを使い、 インスタンス変数としてMockオブジェクトを定義する- つまり、各テストケースがマルチスレッドでインスタンス変数として定義されたMockオブジェクトを共有し、使い回すことになったらどうするのだろう。。。
- JUnitは基本デフォルトでは、スレッドセーフ、同期実行であるものの、もし複数テストケースを(ビルド時間の軽減などのために)非同期で実行したらどうなるのだろう??
But don't forget that:
Good automated tests should be independent, isolated and reproducible, making them ideal candidates for being run concurrently.
なので、気になって調べてみたところ、以下のLINKが役立った。
- https://stackoverflow.com/questions/59651296/are-java-mockito-mocked-method-invocations-thread-safe
- https://github.com/mockito/mockito/wiki/FAQ#is-mockito-thread-safe
まとめると
- 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されるかもしれない
- そのような実装では、以下の問題が発生する可能性がある
例えば、テストケース内のローカル変数としてmockを定義すれば、そのMock objectはスレッドセーフとなり、安全にテストを実行することができる。 ただ、当然、例の通り、コードがrepeatable/冗長化してしまうことがわかるが。。
スレッドセーフにしたテスト実装例:
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 & 方法2 では
-
方法1. MockitoExtention
と比べて、Stubのverifyが正しく行われているかチェックまでしてくれない
会社のプロジェクトではどれを使うか。。。
とても悩むところではあるが、うちの会社の事情を考えると、(IT系スタートアアップとか、IT設備やCI/CDに力入れて潤沢にマシンリソースが使える会社は違うんだろうなぁ。。。と思いつつ。。。 )
- Build CI/CDで Jenkinsを使っているものの、割り当てられてメモリやマシンリソースが少ない
- 可能な限りスレッドを発生させるなと言われ、mockserverも各テストケースごとではなく、テストクラスごとに起動させるよう言われた (そうでないと、
java.lang.OutOfMemoryError: unable to create new native thread.
が頻発しちゃった) - なので、(Java/Spring boot projectに関しては)テストを非同期で行うよう拡張することは今現在、あまり考えられにくい。。
- 可能な限りスレッドを発生させるなと言われ、mockserverも各テストケースごとではなく、テストクラスごとに起動させるよう言われた (そうでないと、
上記事情から考えると、
- テストは同期/シングルスレッド/シーケンシャルに実行する前提で進める
- すると、
方法3. Mockito.mock
はテストコードの冗長化を促進するだけであまりメリットはない - とすると、
方法1. MockitoExtention
を利用し、かつ github issue とソースコードコメントなどに、非同期環境での実装は対応していないとドキュメントする方法が一番良いのではないか?- 非同期に対応していない、ということは絶対にどこかへ記録しておいた方が良いはず