こんにちは、くりりんです。
開発でユニットテストもやってねと任され、やり方を調べている中でMockito(モキート)という飲み物に出会いました。最初は飲み方がわかりませんでした…(笑)(最初に扱った時は使い方を理解できませんでした)。何回も飲むうちに飲みやすく感じるようになって、気が付いたらやみつきになっていましたね(何回も使っていたら便利すぎて手放せなくなりました)。
今回は、そんなMockitoの飲み方を解説していこうと思います。
くれぐれも飲み過ぎて倒れないようにしてくださいね!
Mockitoとは?
Mockitoは、Javaのユニットテストのために開発されたモックフレームワーク(mocking framework)です。テストでモックオブジェクトを直感的に操作できるのを目的として開発されています。
Mockitoの名の由来はもうわかると思いますがモヒートから来ています。
キレイでシンプルなAPIでモックを扱うテストコードを記述できること、また記述されたテストコードの可読性が高くわかりやすい検証エラーを生成することがMockitoの特徴です。それらの特徴とモヒートの爽やかなスッキリとした味わいの特徴をかけてMock + Mojito = Mockito(モック + モヒート = モキート)としたみたいですね。
モックオブジェクトとは?
モックオブジェクトはテスト対象(テストしたいクラス)から呼び出される依存先のオブジェクトに代わって使用されるテスト用のオブジェクトです。
モックオブジェクトは、テスト対象が呼び出す依存先と同じインターフェースを持ち外部からは期待した振る舞いで動いているように見えます(呼び出したメソッドが実装されているかのように見える)。しかし、実際は内部ロジック(内部のコード)が実装されていません。
その実装のかわりに、与えられたパラメータ(引数)に対してどのような結果で返すのか(返り値)を事前にモックオブジェクトに定義し、実際の振る舞いを真似ています。
ユニットテストで使われるMockitoのモックオブジェクトを理解するにはテストダブルとそれに含まれるスタブやモックを理解する必要があります。例を挙げてそれらを解説していきます。
例えば、テスト対象としてControllerクラスがあります。
そのControllerクラスがServiceクラスを使っていたとします。
つまり、ControllerクラスとServiceクラスが依存関係にあるということです。このServiceクラスのようなテスト対象の依存先を「依存コンポーネント」と呼びます。
以下のような感じです。
1 2 3 4 5 6 7 8 9 10 |
public class Controller { private Service service = new Service(); public String getContent(Integer id) { // Serviceクラスのメソッドに依存している部分 String content = service.getContentById(id); return content; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Service { private Map contents = new HashMap(); public Service() { genContents(); } // ControllerクラスのgetContentメソッドの中で使用 public String getContentById(Integer id) { String content; content = contents.get(id); return content; } public void genContents() { for(int i = 0; i < 10; i++) { contents.put(i, "Content" + i); } } } |
例にあるgetContentメソッドはserviceオブジェクトに依存しているため、テストが成功するかどうかはgetContentメソッドの実装とserviceオブジェクトの実装の両方に依存していることになります。
getContentメソッドのテストは、getContentメソッドの実装が正しい処理を示すかに注目すべきで、serviceオブジェクトの実装が正しい処理を示すかを検証する責任は持っていません。
つまり、そのテストはserviceオブジェクトの実装が正しい処理を示す前提で行いたいのです。
その前提があることによって、テストが失敗した時、serviceオブジェクトは正しい処理を示すとわかっているので、getContentメソッドの実装自体にテスト失敗の原因があることがわかります。
テストしたいものにより焦点を絞るために、依存コンポーネントの動作を代用品のオブジェクトに置き換える方法がテストの独立性を高めるために有効です。この代用品となるオブジェクトがモックオブジェクトです。
モックオブジェクトには、スタブやモックなどのオブジェクトが備わっています(厳密に言えば、モックの中にスタブが備わっている)。スタブやモックはオブジェクトですが、Mockitoではモックオブジェクトを介してスタブやモックの操作を行うためそれらを機能として捉えたほうがわかりやすいかもしれません。
テストを目的として本物のオブジェクトの動作を真似ているスタブやモックなどのオブジェクトの総称をテストダブルと呼びます。つまり、テスト対象の依存コンポーネントを置き換えている代用品は全てテストダブルということになります。このダブルは代役という意味で、映画などで危険なシーンを代わりに演じる役者のスタントダブル(Stunt double)から来ています。
テストダブルは5つの代役として、ダミー、フェイク、スタブ、モック、スパイを定義しています。
ここでは後の説明にでてくるスタブ、モック、スパイについて解説します。
スタブは、メソッドの実行に対して、事前に定義された振る舞い(引数、返り値)を提供するオブジェクトです。テスト対象から外部の依存性を排除し、テストがオブジェクトの実装の正しさだけに注目する目的で使われています。スタブはテストとは関係のない依存性を排除しただけなので、スタブだけではテストを行ったとは言えません。
モックは、メソッドの実行に対して、実行回数やパラメータの呼び出しを記録するオブジェクトです。テスト対象から依存コンポーネントの呼び出しを検証する目的で使われています。テスト対象から呼び出されたスタブの動作を記録し、その記録からメソッドの実行回数などを検証します。モックによる検証を含めてテストを行ったと言えます。
スパイも、メソッドの実行に対して、実行回数やパラメータの呼び出しを記録するオブジェクトです。モックと同様の役割ですが、基本的な定義として、モックはメソッドの実行中に検証するのに対して、スパイはメソッドの実行後に検証するという違いがあります。
しかし、Mockitoで扱う検証メソッドではメソッドの実行後に検証するので両者の違いはありませんが、Mockitoにおけるスパイはオブジェクトを部分的にモックする用途で使用されています。
Mockitoのモックオブジェクト、スタブ、モック、スパイについての解説ですので、それらの定義が他のテストダブルライブラリでは異なる可能性があるので確認が必要であることに注意してください。
様々な記事で”モックする”、”モック化”など書かれていますが、広義でのモックはスタブやスパイなどを含むので、そのモック化が何を指しているのかを考えると理解が進むと思います。
ここではモック化したオブジェクトをモックオブジェクトと言い、モックするということは元のオブジェクトのメソッドをスタブ化したメソッドを持つモックオブジェクトができるということです。
Mockitoの飲みやすさとは?
あるオブジェクトの代用品としてモックオブジェクトを生成したとします。
そのモックオブジェクトは、あるオブジェクトのインターフェースを真似ているので、そのオブジェクトのメソッド(を真似ているメソッド)などを呼び出すことができます。そして、そのメソッドの振る舞い(引数、返り値)を定義できます。
それをMockitoで書くと、
when(mockObject.method(“Say something”)).thenReturn(“I am Mockito!”);
英語として読んでみると「when mockObject.method(“Say something”) then return “I am Mockito!”」
日本語にすると「methodの引数に”Say something(なにか喋って)”と入れて呼んだ時、返り値は”I am Mockito!(私はモキートです!)”」と読めますよね。
これがMockitoはシンプルで飲みやすい(読みやすい)と言われている所以です。
どうです?飲みやすそうじゃないですか?え、まずそう?(笑)
大丈夫です、飲みやすいと思えるようにこれから操作方法を解説していきます。
モックオブジェクトの操作方法
テスティングフレームワークとしてJUnitを使うことを前提として解説していきます。
JUnitはモックを扱う仕組みが実装されていないため、Mockitoなどのモックフレームワークのライブラリからモック機能を取り入れています。
モックオブジェクトの作り方
モックオブジェクトの作り方は、2パターンあります。
- mockメソッドを使う方法
- @Mockを使う方法
1 2 3 4 5 6 7 8 |
// mockメソッドを使う方法 // mockメソッドによって既にモックオブジェクトが生成されている private Service service = mock(Service.class); // @Mockを使う方法 // このフィールドはモックオブジェクトとして使いますと指定しているだけ(あとで説明します) @Mock private Service service; |
Mockitoではモックオブジェクトを生成した時点で、生成元のオブジェクトが持つメソッドは全てスタブ化されます。そのスタブ化されたメソッドをスタブメソッドと呼びます。
mockメソッドで生成したモックオブジェクトの全てのスタブメソッドは内部ロジックを持ちません。
テスト対象へのモックオブジェクトの注入
@InjectMocksを付与することで、インジェクト先のオブジェクトを指定できます。
例ではControllerクラスがテスト対象ですので、Controllerクラスに@InjectMocksを付与します。
1 2 3 4 |
@Mock private Service service; @InjectMocks private Controller controller; |
モックの初期化
Mockitoアノテーション(@Mockや@InjectMocksや@Spyなど)が付与されているフィールドに対して初期化処理をする必要があります。Mockitoで言う初期化とは、モックオブジェクトを生成することです(アノテーションをつけただけで、mockメソッドは書いていません。ですので、初期化処理を書いて、実際にモックオブジェクトを作成します)。忘れるとテスト対象のクラスにインジェクトができませんのでご注意ください。
初期化するのにMockitoAnnotationsのinitMocksメソッドを使います。
初期化の記述は、@Before(JUnitのアノテーション)を付与したメソッドの中で行います。
@Beforeはメソッドに付与することで、各テストメソッドを実行する前に毎回実行するように指定できます。
テスト前にモックオブジェクトを生成しないと、もちろんテストでエラーします。
また、テストメソッドで操作したモックオブジェクトを毎回初期化する意味でも@Before付けています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class ControllerTest { @Mock private Service service; @InjectMocks private Controller controller; @Before private void initMocks() { // @Beforeが付与されているメソッドのなかにinitMocksメソッドを書くことで、各テストメソッドを実行する前に毎回実行できる MockitoAnnotations.initMocks(this); } } |
モックオブジェクトの操作
mockメソッドで生成したモックオブジェクトの中身を解説していきます。
モックオブジェクトのスタブメソッドは、全てnullを返すようにデフォルトで指定されています。
実際に以下のコードをJUnitで実行すればわかると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Test { @Mock private Service service; @Before public void initMocks() { MockitoAnnotations.initMocks(this); } @Test public void test() { // assertThatはJUnitの比較検証メソッドです // スタブ化されたserviceのgetContentByIdメソッドの引数にどの値をいれても、nullで返ってきます assertThat(service.getContentById(0), is(nullValue())); assertThat(service.getContentById(1), is(nullValue())); assertThat(service.getContentById(2), is(nullValue())); assertThat(service.getContentById(3), is(nullValue())); } } |
モックオブジェクトのスタブメソッドの振る舞いを定義するのにthenメソッド系(thenReturnメソッドやthenThrowメソッドなど)とdoメソッド系(doReturnメソッドやdoThrowメソッド)があります。
whenメソッドとthenReturnメソッドもしくはdoReturnメソッドを合わせてスタブメソッドの振る舞いを定義します。それぞれのメソッド系で構文が異なります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class ControllerTest { @Mock private List testMock; @Before public void initMocks() { MockitoAnnotations.initMocks(this); } @Test public void testThen() { // thenメソッド系 // when(モックオブジェクト.メソッド(任意の引数)): スタブを可能とするメソッドです。 // 特定のメソッドを呼び出して、その返り値に特定の値を入れる時に使います。 // thenReturn(返り値): スタブメソッドの返り値を指定するのに使います。 when(testMock.get(0)).thenReturn("Hi, Mockito"); assertThat(testMock.get(0), is("Hi, Mockito")); } @Test public void testDo() { // doメソッド系 // when(モックオブジェクト).メソッド(任意の引数) // doReturn(返り値): スタブメソッドの返り値を指定するのに使います。 // doメソッド系ではwhenメソッドより先に実行結果を定義するメソッドが来ます。 doReturn("Hi, Mockito").when(testMock).get(0); assertThat(testMock.get(0), is("Hi, Mockito")); } } |
どちらのメソッド系もできることは変わりませんが、doメソッド系にしかできないことがあります。それが以下の2点です。
- voidメソッドをスタブ化する場合
- @Spy、spyメソッドで生成したモックオブジェクトの場合
thenメソッド系にできて、doメソッド系にできないことはコンパイル時に引数の型をチェックしてくれることだけです。なので、基本的にdoメソッド系を使用してテストコードを書けばいいという意見が多い印象です。
オブジェクトを部分的にモック
最初の方にMockitoにおけるスパイはオブジェクトを部分的にモックする用途で使用すると言いました。
では、Mockitoのスパイはどのような時に使うのでしょうか?
スパイによるモックオブジェクト(厳密にはスパイオブジェクト)の作り方は、2パターンあります。
- spyメソッドを使う方法
- @Spyを使う方法
1 2 3 4 5 6 7 8 |
// spyメソッドを使う方法 // spyメソッドによって既にモックオブジェクトが生成されている private Controller controller = spy(Controller.class); // @Spyを使う方法 // @Mockの時と同様にinitMocksメソッドで初期化が必要 @Mock private Service service; |
spyメソッドや@Spyで生成したモックオブジェクトは、mockメソッドや@Mockで生成したモックオブジェクトと同様に元のオブジェクトのメソッドを全てスタブ化しています。
異なる点として、spyメソッドや@Spyで生成したモックオブジェクトのスタブメソッドは、元のオブジェクトのメソッドの実装をそのまま真似ています。
つまり、spyメソッドや@Spyで生成したモックオブジェクトの全てのスタブメソッドは、元のメソッドの内部ロジックを持ちます。
doメソッド系で内部ロジックを持っているスタブメソッドの振る舞いを上書きすることで、見かけ上一部のメソッドだけの振る舞いをモックしているように見えます。
そのため、スパイは部分的にモックする用途で使用できるということになります。
例えば、テスト対象のメソッドの中でテスト対象の内部にある別のメソッドを使っている場合に、spyメソッドを使って部分的にモックします。
そうすることで、 テスト対象内の別のメソッドにも依存せずにテストを行うことができます。
以下のコードを見れば理解が深まると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Controller { private Service service = new Service(); public String getContent(Integer id) { String content = service.getContentById(id); // 同じクラス内の別のメソッドに依存している部分 content = editContent(content); return content; } public String editContent(String content) { String editContent = "Edited" + content; return editContent; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public class ControllerTest { @Mock private Service service; @Spy @InjectMocks private Controller controller; @Before public void initMocks() { MockitoAnnotations.initMocks(this); } @Test public void test_getContent() { Integer id = 1; // idに1を入れた時、"Contetn1"が返ってくるようにServiceモックオブジェクトのスタブ化されたgetContentByIdメソッドを定義 when(service.getContentById(id)).thenReturn("Content1"); // Controllerモックオブジェクトのスタブ化されたeditContentメソッドだけを再定義 doReturn("EditedContent1").when(controller).editContent("Content1"); // Controllerモックオブジェクトの他のメソッドはスタブ化されているが、再定義しない限り元の内部ロジックを持ったまま // 期待している返り値 String expected = "EditedContent1"; // 実際の返り値 String actual = controller.getContent(id); // 期待している返り値と実際の返り値を比較検証 assertThat(actual, equalTo(expected)); } } |
上の例では、テスト対象内の別のメソッドに依存せずにテストを行えることを示しましたが、あるメソッドがDB接続など内部ロジックがそのままでないと機能しない場合にもspyメソッドは有効です。
繰り返しになりますがspyメソッドでモックオブジェクトを生成すれば、スタブメソッドは元のメソッドの内部ロジックをそのまま持てるのです。
メソッド実行に対する呼び出し回数や引数の検証
最後にverifyメソッドについて解説していきます。
verifyは、モックオブジェクトから呼び出されたメソッドが指定した引数で指定した回数だけ呼ばれたかどうかを検証します。テスト対象の実装に依存コンポーネントが呼ばれている部分をテストする時に使われます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public class ControllerTest { @Mock private Service service; @Spy @InjectMocks private Controller controller; @Before public void initMocks() { MockitoAnnotations.initMocks(this); } @Test public void test_getContent() { Integer id = 1; when(service.getContentById(id)).thenReturn("Content1"); doReturn("EditedContent1").when(controller).editContent("Content1"); // 期待している返り値 String expected = "EditedContent1"; // 実際の返り値 String actual = controller.getContent(id); // 指定した引数で指定した回数だけ依存コンポーネントのメソッド(モックオブジェクトのスタブメソッド)が呼ばれたかどうかの検証 verify(service, times(1)).getContentById(id); verify(controller, times(1)).editContent("Content1"); // 期待している返り値と実際の返り値を比較検証 assertThat(actual, equalTo(expected)); } } |
上記のコードでは、service.getContentByIdメソッドとcontroller.editContentメソッドの呼び出され方をテストしています。
controller.getContentメソッドから、service.getContentByIdメソッドとcontroller.editContentメソッドの引数の値および呼び出し回数をverifyメソッドで検証できます。もしいずれかが指定した値や回数が異なっていたら、JUnitが失敗を示します。
まとめ
- Mockito: モックオブジェクトを直感的に操作できるフレームワーク(Javaのライブラリの一つ)
- モックオブジェクト: テスト対象(テストしたいクラス)から呼び出される依存先のオブジェクトに代わって使用されるテスト用のオブジェクト
- スタブ: 事前に定義された振る舞い(引数、返り値)を提供するオブジェクト
- モック: 実行回数やパラメータの呼び出しを記録するオブジェクト
- スパイ: 実行回数やパラメータの呼び出しを記録するオブジェクト
- モックオブジェクトの生成(スタブメソッドの内部ロジックなし): mockメソッドもしくは@Mockの使用
- モックオブジェクトの生成(スタブメソッドの内部ロジックあり): spyメソッドもしくは@Spyの使用
- テスト対象への注入: @InjectMocksの使用
- スタブメソッドの定義:
- when(モックオブジェクト.メソッド).thenReturn(返り値)
- doReturn(返り値).when(モックオブジェクト).メソッド(任意の引数)
- 呼び出し回数や引数の検証: verifyメソッドの使用
モキートモキートと打っていたらモヒートが飲みたくなってきました(笑)
夏に飲むモヒート最高ですよね!
参考文献
渡辺修司(2012)『JUnit実践入門 ── 体系的に学ぶユニットテストの技法』技術評論社
PHPUnitのモックオブジェクトの使い方を仕組みから理解する
モックオブジェクト(もっくおぶじぇくと)
スタブとモックの違い
スタブとモックの基礎
自動テストのスタブ・スパイ・モックの違い
What’s the difference between a mock & stub?
Mockito Mock vs. Spy in Spring Boot Tests
Mocks Aren’t Stubs
mockito入門1 概要
Getting Started with Mockito
落ち着いて聞いてほしい、@Mockでモックを初期化する方法は3つある。
Junitライブラリ「Mockito」の@Mockと@InjectMocksの使い方
Class MockitoAnnotations
Unit Test Spring MVC Rest Service: MockMVC, JUnit, Mockito