Featured image of post 【JUnit】Java製のユニットテスト向けフレームワーク「Mockito」でMockとSpyを使いこなそう【簡単スタブ化】

【JUnit】Java製のユニットテスト向けフレームワーク「Mockito」でMockとSpyを使いこなそう【簡単スタブ化】

3091文字

ユニットテストでモック化してみよう

ユニットテストはなるべく書こう

開発を行う上でユニットテストはとても大切です。

実装と並行してユニットテストを組んでおけば、簡単なテストの自動化や保守性・潜在的バグを洗い出せるので出来る限りやっておきたいですね。

今回はユニットテストを行う際に一部のクラスをモック化するためのライブラリ「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層

src/main/java/sample/model/PersonModel.java
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層

src/main/java/sample/repository/PersonRepository.java
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層

src/main/javasample/service/PersonService/java
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に追記しましょう。

[adsense-responsive]

build.gradle
...
()
...

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種類あります。

まずは前者の方法による生成のためのヘルパークラスを用意しました。

src/test/java/sample/mock/PersonRepositoryMockHelper.java
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化せずにそのままテストしてみましょう。

src/test/java/sample/service/PersonServiceTest.java
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化してみましょう。

a
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メソッドを呼び出した際の戻り値を呼び出し側でセット可能なので、実際に定義されたPersonRepositoryfindByメソッドは呼ばれないのでDBアクセスや外部APIアクセスを無効化した上でServiceクラスを動かす事が出来ます。

テストも正常に行えていますね♪

mock化した際には戻り値をセットする必要がある

対象クラスをmock化すると、全ての実装済メソッドが内部ロジックを持たないインターフェースのようなメソッドに置き換わっているため、実行時に期待通りの動きをしてくれません。

src/test/java/sample/service/PersonServiceTest.java
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を使う感じですね。

src/test/java/sample/service/PersonServiceTest.java
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の生成

[adsense-responsive]

src/test/java/sample/service/PersonServiceTest.java
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の生成

src/test/java/sample/service/PersonServiceTest.java
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でモック化するのであれば、覚えていて損はないライブラリなので、皆さんもぜひ試してみてください♪