投稿日:
    								
    									2020年8月21日    								
    								最終更新日:
    								
    									
    								
    							    						
【JUnit】Java製のユニットテスト向けフレームワーク「Mockito」でMockとSpyを使いこなそう【簡単スタブ化】
YouTubeも見てね♪
ねこじゃすり
猫を魅了する魔法の装備品!
【最新機種】GoPro hero11 Black
最新機種でVlogの思い出を撮影しよう!
ペヤング ソースやきそば 120g×18個
とりあえず保存食として買っておけば間違いなし!
レッドブル エナジードリンク 250ml×24本
翼を授けよう!
ドラゴンクエスト メタリックモンスターズギャラリー メタルキング
みんな大好き経験値の塊をデスクに常備しておこう!
BANDAI SPIRITS ULTIMAGEAR 遊戯王 千年パズル 1/1スケール
もう一人の僕を呼び覚ませ!!
サンディスク microSD 128GB
スマホからSwitchまで使える大容量MicroSDカード!
ユニットテストでモック化してみよう
ユニットテストはなるべく書こう
開発を行う上でユニットテストはとても大切です。
実装と並行してユニットテストを組んでおけば、簡単なテストの自動化や保守性・潜在的バグを洗い出せるので出来る限りやっておきたいですね。
今回はユニットテストを行う際に一部のクラスをモック化するためのライブラリ「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でモック化するのであれば、覚えていて損はないライブラリなので、皆さんもぜひ試してみてください♪





