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