Anker PowerCor
旅行には必須の大容量モバイルバッテリー!
【最新機種】GoPro hero11 Black
最新機種でVlogの思い出を撮影しよう!
[ノースフェイス] THE NORTH FACE メンズ アウター マウンテンライトジャケット
防水暴風で耐久性抜群なので旅行で大活躍です!
ペヤング ソースやきそば 120g×18個
とりあえず保存食として買っておけば間違いなし!
Bauhutte ( バウヒュッテ ) 昇降式 L字デスク ブラック BHD-670H-BK
メインデスクの横に置くのにぴったりなおしゃれな可動式ラック!
BANDAI SPIRITS ULTIMAGEAR 遊戯王 千年パズル 1/1スケール
もう一人の僕を呼び覚ませ!!
MOFT X 【新型 ミニマム版】 iPhone対応 スマホスタンド
Amazon一番人気のスマホスタンド!カード類も収納出来てかさ張らないのでオススメです!
スポンサーリンク
ユニットテストでモック化してみよう
ユニットテストはなるべく書こう
開発を行う上でユニットテストはとても大切です。
実装と並行してユニットテストを組んでおけば、簡単なテストの自動化や保守性・潜在的バグを洗い出せるので出来る限りやっておきたいですね。
今回はユニットテストを行う際に一部のクラスをモック化するためのライブラリ「Mockito」を使ってみようと思います!
モックとは?
モックという言葉を初めて聞く方もいるかもしれません。
モックとは、クラスの動作を意図的に設定し、擬似的シミュレートするためのオブジェクトの事を指します。
例えば、Service
層とRepository
層がある場合、Repository
層のユニットテストでは実際にDBにアクセスして動作確認を行うテストを記述しますが、Service
層のユニットテストを記述する際は、Repository
クラスの動きはRepository
層のユニットテストで担保している前提で行うことが多いので、実際にDBや外部システムへのリクエストは行いたくない場合があると思います。
その際にRepository
クラスをモック化する事で、実際にはDBや外部システムへのリクエストは発生していないがService
層からすると実際にデータを取得出来るので問題なくService
層のテストを行うことが可能です。
Java製の定番ライブラリ「Mockito」
今回はJunitを使う際にお馴染みのMockitoを使って簡単なモック化の動きについてご紹介しようと思います♪
Tasty mocking framework for unit tests in JavaMockito framework site
使い方
準備
前提
今回は以下の環境で動かします。
- Gradle 5.6.4
- OpenJDK 11.0.2
- Lombok 1.18.12
- Junit 4.12
- Mockito 2.28.2
使用クラス
Model層
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package sample.model; import lombok.Builder; import lombok.Data; @Data @Builder public class PersonModel { public static final String NAME_PREFIX = "Mr."; private Integer id; private String name; private Integer age; } |
Repository層
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package sample.repository; import java.util.Optional; import sample.model.PersonModel; public class PersonRepository { public Optional<PersonModel> findBy(Integer id) { // DBアクセスや外部APIアクセスetc... // 今回は暫定的にダミーデータを生成 return Optional.ofNullable(PersonModel.builder().id(id).name(String.format("%s%s", PersonModel.NAME_PREFIX, id)) .age(id * 10).build()); } } |
Service層
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package sample.service; import sample.model.PersonModel; import sample.repository.PersonRepository; public class PersonService { private PersonRepository repository;// 本来はAutoWiredやInjectionをするのが正しいが今回は割愛 public PersonService(PersonRepository repository) { this.repository = repository; } public PersonModel findBy(Integer id) { return repository.findBy(id).orElseThrow(() -> new InternalError()); } } |
Gradleに依存関係の追加
まずは今回使用するライブラリをbuild.gradle
に追記しましょう。
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 |
... (略) ... repositories { jcenter() } dependencies { // https://mvnrepository.com/artifact/org.projectlombok/lombok compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.12' annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.12' // This dependency is used by the application. implementation 'com.google.guava:guava:28.0-jre' // Use JUnit test framework testImplementation 'junit:junit:4.12' // Use Mockit testImplementation "org.mockito:mockito-core:2.+" } ... (略) ... |
whenメソッドによるMock作成用のHelperクラスの用意
Mockオブジェクトを生成する方法はstatic
メソッドによる生成とアノテーションによる生成の2種類あります。
まずは前者の方法による生成のためのヘルパークラスを用意しました。
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package sample.mock; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import java.util.Optional; import sample.model.PersonModel; import sample.repository.PersonRepository; public class PersonRepositoryMockHelper { public static MockBuilder mockBuilder() { return new MockBuilder(); } public static SpyBuilder spyBuilder() { return new SpyBuilder(); } public static class MockBuilder { private PersonRepository target = mock(PersonRepository.class); public PersonRepository build() { return target; } public MockBuilder findBy(Optional<PersonModel> data) { when(target.findBy(any())).thenReturn(data); return this; } } public static class SpyBuilder { private PersonRepository target = spy(PersonRepository.class); public PersonRepository build() { return target; } public SpyBuilder findBy(Optional<PersonModel> data) { // when(target.findBy(any())).thenReturn(data); これだとエラーになる doReturn(data).when(target).findBy(any()); return this; } } } |
このクラスは本質的な部分ではない為簡易的に作っているので、よしなに各自カスタマイズしてください!
Service層のテスト
mock化しない
では、まずはRepository
層のfindBy
メソッドをmock化せずにそのままテストしてみましょう。
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 34 35 36 |
package sample.service; import static org.junit.Assert.assertThat; import java.util.Optional; import static org.hamcrest.Matcher.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNot.*; import org.junit.Test; import sample.mock.PersonRepositoryMockHelper; import sample.model.PersonModel; import sample.repository.PersonRepository; public class PersonServiceTest { private PersonService target; @Test public void そのまま() { // SetUp this.target = new PersonService(new PersonRepository()); // 本来はAutoWiredやInjectionをするのが正しいが今回は割愛 // When Integer ID = 12345; PersonModel expected = this.target.findBy(ID); // Than assertThat(expected.getId(), is(ID)); assertThat(expected.getName(), is(String.format("%s%s", PersonModel.NAME_PREFIX, ID))); assertThat(expected.getAge(), is(ID * 10)); } } |
こちらは問題なく動いています。
Mock化
では、まずはRepository
層のfindBy
メソッドをmock化してみましょう。
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 34 35 36 37 38 39 40 |
package sample.service; import static org.junit.Assert.assertThat; import java.util.Optional; import static org.hamcrest.Matcher.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNot.*; import org.junit.Test; import sample.mock.PersonRepositoryMockHelper; import sample.model.PersonModel; import sample.repository.PersonRepository; public class PersonServiceTest { private PersonService target; @Test public void mock化() { // SetUp Integer ID = 12345; String NAME = "Ms." + ID; Integer AGE = ID * 100; PersonModel PERSON = PersonModel.builder().id(ID).name(NAME).age(AGE).build(); this.target = new PersonService(PersonRepositoryMockHelper.mockBuilder().findBy(Optional.of(PERSON)).build()); // findByメソッドをモック化し戻り値を明示的セット // When PersonModel expected = this.target.findBy(ID); // Than assertThat(expected.getId(), is(ID)); assertThat(expected.getName(), is(not(String.format("%s%s", PersonModel.NAME_PREFIX, ID)))); assertThat(expected.getAge(), is(not(ID * 10))); } } |
このようにfindBy
メソッドを呼び出した際の戻り値を呼び出し側でセット可能なので、実際に定義されたPersonRepository
のfindBy
メソッドは呼ばれないのでDBアクセスや外部APIアクセスを無効化した上でService
クラスを動かす事が出来ます。
テストも正常に行えていますね♪
mock化した際には戻り値をセットする必要がある
対象クラスをmock化すると、全ての実装済メソッドが内部ロジックを持たないインターフェースのようなメソッドに置き換わっているため、実行時に期待通りの動きをしてくれません。
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 |
package sample.service; import static org.junit.Assert.assertThat; import java.util.Optional; import static org.hamcrest.Matcher.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNot.*; import org.junit.Test; import sample.mock.PersonRepositoryMockHelper; import sample.model.PersonModel; import sample.repository.PersonRepository; public class PersonServiceTest { private PersonService target; @Test public void mock化_未指定() { // SetUp Integer ID = 12345; String NAME = "Ms." + ID; Integer AGE = ID * 100; PersonModel PERSON = PersonModel.builder().id(ID).name(NAME).age(AGE).build(); this.target = new PersonService(PersonRepositoryMockHelper.mockBuilder().build());// mock化したのに戻り値を指定しない場合は実行時にnullなりnullableな値が返ってくるので注意 } } |
なので、呼び出すメソッドの戻り値を明示的に指定する必要がある点を気をつけましょう。
Spy化
MockitoにはMock化のほかにSpyという機能もあります。
Spyとは、対象クラスの実装メソッドをそのままにしながら、任意のメソッドのみをMock化することが出来ます。
一部のメソッドはそのままのロジックで動かしつつ一部のメソッドのみ制御したい場合にSpyを使う感じですね。
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
package sample.service; import static org.junit.Assert.assertThat; import java.util.Optional; import static org.hamcrest.Matcher.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNot.*; import org.junit.Test; import sample.mock.PersonRepositoryMockHelper; import sample.model.PersonModel; import sample.repository.PersonRepository; public class PersonServiceTest { private PersonService target; @Test public void spy化_未指定() { // SetUp Integer ID = 12345; String NAME = "Ms." + ID; Integer AGE = ID * 100; PersonModel PERSON = PersonModel.builder().id(ID).name(NAME).age(AGE).build(); this.target = new PersonService(PersonRepositoryMockHelper.spyBuilder().build()); // Mockと違い、戻り値を指定しない場合は実装済みのロジックが動く // When PersonModel expected = this.target.findBy(ID); // Than assertThat(expected.getId(), is(ID)); assertThat(expected.getName(), is(String.format("%s%s", PersonModel.NAME_PREFIX, ID))); assertThat(expected.getAge(), is(ID * 10)); } @Test public void spy化() { // SetUp Integer ID = 12345; String NAME = "Ms." + ID; Integer AGE = ID * 100; PersonModel PERSON = PersonModel.builder().id(ID).name(NAME).age(AGE).build(); this.target = new PersonService(PersonRepositoryMockHelper.spyBuilder().findBy(Optional.of(PERSON)).build());// 戻り値を指定した場合は実装済みのロジックが動かずに指定した値を返す // When PersonModel expected = this.target.findBy(ID); // Than assertThat(expected.getId(), is(ID)); assertThat(expected.getName(), is(not(String.format("%s%s", PersonModel.NAME_PREFIX, ID)))); assertThat(expected.getAge(), is(not(ID * 10))); } } |
Spyも正常に動きましたね♪
アノテーションによる生成方法
次にアノテーションを使ったMockオブジェクト及びSpyオブジェクトの生成方法を試してみましょう。
Mockの生成
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package sample.service; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import java.util.Optional; import static org.hamcrest.Matcher.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNot.*; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import sample.mock.PersonRepositoryMockHelper; import sample.model.PersonModel; import sample.repository.PersonRepository; public class PersonServiceTestWithAnnotaion { private PersonService target; @Mock PersonRepository mockRepository; // Mockとして使うオブジェクトを明示(この時点では何もMock化されていない) @Before public void initMocks() { MockitoAnnotations.initMocks(this);// @Mockが付与されたクラスをMock化する(この時点ではメソッドのStub化はされていない) } @Test public void mock化() { // SetUp Integer ID = 12345; String NAME = "Ms." + ID; Integer AGE = ID * 100; PersonModel PERSON = PersonModel.builder().id(ID).name(NAME).age(AGE).build(); this.target = new PersonService(mockRepository); when(mockRepository.findBy(any())).thenReturn(Optional.of(PERSON));// ここでセットアップ // When PersonModel expected = this.target.findBy(ID); // Than assertThat(expected.getId(), is(ID)); assertThat(expected.getName(), is(not(String.format("%s%s", PersonModel.NAME_PREFIX, ID)))); assertThat(expected.getAge(), is(not(ID * 10))); } } |
アノテーションで実装した方がスッキリかつTestケース内でStubの内容がはっきり分かるので良いかもしれませんね!
Mockの生成
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
package sample.service; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; import java.util.Optional; import static org.hamcrest.Matcher.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsNot.*; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import sample.mock.PersonRepositoryMockHelper; import sample.model.PersonModel; import sample.repository.PersonRepository; public class PersonServiceTestWithAnnotaion { private PersonService target; // @Mock PersonRepository mockRepository; // Mockとして使うオブジェクトを明示(この時点では何もMock化されていない) @Spy PersonRepository spyRepository; @Before public void initMocks() { MockitoAnnotations.initMocks(this);// @Mockが付与されたクラスをMock化する(この時点ではメソッドのStub化はされていない) } // @Test public void mock化() { // SetUp Integer ID = 12345; String NAME = "Ms." + ID; Integer AGE = ID * 100; PersonModel PERSON = PersonModel.builder().id(ID).name(NAME).age(AGE).build(); this.target = new PersonService(mockRepository); when(mockRepository.findBy(any())).thenReturn(Optional.of(PERSON));// ここでセットアップ // When PersonModel expected = this.target.findBy(ID); // Than assertThat(expected.getId(), is(ID)); assertThat(expected.getName(), is(not(String.format("%s%s", PersonModel.NAME_PREFIX, ID)))); assertThat(expected.getAge(), is(not(ID * 10))); } @Test public void spy化_そのまま() { // SetUp Integer ID = 12345; this.target = new PersonService(spyRepository); // When PersonModel expected = this.target.findBy(ID); // Than assertThat(expected.getId(), is(ID)); assertThat(expected.getName(), is(String.format("%s%s", PersonModel.NAME_PREFIX, ID))); assertThat(expected.getAge(), is(ID * 10)); } @Test public void spy化() { // SetUp Integer ID = 12345; String NAME = "Ms." + ID; Integer AGE = ID * 100; PersonModel PERSON = PersonModel.builder().id(ID).name(NAME).age(AGE).build(); this.target = new PersonService(spyRepository); doReturn(Optional.of(PERSON)).when(spyRepository).findBy(any()); // When PersonModel expected = this.target.findBy(ID); // Than assertThat(expected.getId(), is(ID)); assertThat(expected.getName(), is(not(String.format("%s%s", PersonModel.NAME_PREFIX, ID)))); assertThat(expected.getAge(), is(not(ID * 10))); } } |
Spyも同様に簡単に生成出来ましたね♪
終わりに
以上のようにレイヤー単位でのユニットテストを実装するのであれば、必ず必要となってくるMock化をご紹介しました。
JUnitでモック化するのであれば、覚えていて損はないライブラリなので、皆さんもぜひ試してみてください♪