SpringBoot (JUnit 5 + Mockito) でユニットテストを書くときのお話。
当記事の内容は初歩的なものであり、Spring経験者には物足りないと思われるのでご注意を。
今回テストしたいクラス
HogeServiceというクラスがあるとしよう。
@Service // ←これを書くとSpringが当クラスをBeanとして扱ってくれる
@RequiredArgsConstructor // ←これでコンストラクタを自動生成
public class HogeService {
private final One one;
private final Two two;
private final Three three;
public String doSomething() {
// ここに one, two, three を使った何らかの処理
return "Yes";
}
}
上記クラスにはコンストラクタが書かれていないように見えるが、代わりに @RequiredArgsConstructor が付いている。
これはLombokというライブラリの機能のひとつで、あとでコンストラクタを自動生成してくれる。
下記のようなコンストラクタが目に見えないけど存在している、と思っておこう。
public HogeService(One one, Two two, Three three) {
this.one = one;
this.two = two;
this.three = three;
}
HogeServiceのインスタンス作成をするには one, two, three の3つの引数が必要というわけである。
言い換えると、one, two, three の3つの "依存性" の "注入" が必要というわけである。
なんだか、「必要とする性質」のことを "依存性" と呼ぶのは分かるけど、「必要とする部品」のことを "依存性" と呼ぶのは違和感がある。でも "Dependency" を直訳したら "依存性" だから別にいいよね、と個人的には思うようにしている。
テストクラス
HogeServiceをテストするためにHogeServiceTestを作ることを考えよう。
@ExtendWith(MockitoExtension.class) // ←これはMockitoを使うのに必須
class HogeServiceTest {
// テスト対象
private HogeService hogeService;
// 以下略!
}
ここから先、当記事ではテスト対象であるHogeServiceのインスタンスを作成する方法を3つ紹介していく。
インスタンス作成といえばnew文
hogeService = new HogeService(one, two, three);
が馴染み深いかもしれないが、先に言っておくと当記事でnew文を使うのは「方法2」だけである。
方法1 @InjectMocks を使う
@ExtendWith(MockitoExtension.class) // ←これはMockitoを使うのに必須
class HogeServiceTest {
// テスト対象
@InjectMocks // ←これでMockitoがインスタンス作成をしてくれる
private HogeService hogeService;
@Mock
private One one;
@Spy
private Two two;
@Spy
private Three three;
// 以下略!
}
テスト対象のhogeServiceに @InjectMocks を付与している。
これはMockitoというライブラリの機能のひとつで、インスタンス作成を代行してくれるので、自分でnew文を書かなくてよい。
このとき必要な one, two, three は、@Mock や @Spy が付与されているものが自動的に注入に使われる。
上記コードでは、oneはモック、他はスパイにしてみた。
モックやスパイは、
// one の getColor が呼ばれたら "white" を返す
doReturn("white").when(one).getColor();
// two の getColor が呼ばれたら "black" を返す
doReturn("black").when(two).getColor();
みたいな感じで、後からメソッドの振る舞いを上書き設定可能。
振る舞いを何も設定していないスパイ(three)は、実質、いつもどおりの「実インスタンス」として振る舞ってくれる。
@InjectMocks の注意点
下記のようにoneとtwoだけ書いてthreeを書き忘れた場合、厄介なことになる。
@ExtendWith(MockitoExtension.class) // ←これはMockitoを使うのに必須
class HogeServiceTest {
// テスト対象
@InjectMocks // ←これでMockitoがインスタンス作成をしてくれる
private HogeService hogeService;
@Mock
private One one;
@Spy
private Two two;
// 以下略!
}
この場合、インスタンス作成をしようとしたMockitoが「注入するthreeが無いぞ」と困ってしまい、
threeがnullになっているhogeServiceが作成されるかもしれないし、
インスタンス作成に失敗してhogeService自体がnullになるかもしれない。
(どうなるかは状況によって異なる)
開発者にとってツライのは、書き忘れてもコンパイル自体は通ってしまうこと。
Mockitoの公式Javadocにも "If any of the following strategy fail, then Mockito won't report failure(失敗しても報告しませんよ)" や "you will have to provide dependencies yourself.(注入するものはご自身で揃えてね)" とある。
書き忘れたままテストを動かしてしまい、突然発生したぬるぽに困惑した人もいる。そう、私である。
Geminiに「Spring開発の業界全体で @InjectMocks の評価ってどうなの」と質問したら「あまり好まれていません」とのこと。
それが本当かどうかは分からないが、私も好んでいないし、私の職場のコーディング規約も「禁止はしないけど推奨しない」という姿勢を取っていたりする。
方法2 手動でnew文を書く
@InjectMocks なんて使わずにやっぱり自分でnew文を書こうじゃないか、という人はこちら。
@ExtendWith(MockitoExtension.class) // ←これはMockitoを使うのに必須
class HogeServiceTest {
// テスト対象
private HogeService hogeService;
@Mock
private One one;
@Spy
private Two two = new Two();
private Three three = new Three(); // threeはただのフィールド
@BeforeEach // ←これによりsetUp()は各テスト実行前に毎回呼ばれる
void setUp() {
// new文を自分で書いてインスタンス作成
hogeService = new HogeService(one, two, three);
}
// 以下略!
}
@BeforeEach を付けたメソッドは各テストが実行される直前に毎回実行されるので、その中でhogeServiceを手動でnewするようにしている。
手動でnew文を書くことの最大のメリットは、「何を注入するのか」をパッと書けること、そしてコードを見た人もパッと分かることだと思う。
twoのスパイを作る部分は、上記コードでは private Two two = new Two(); と明示的にnew文を書いている。
もし private Two two; とだけ書いた場合、親切なMockitoがデフォルトコンストラクタ(引数なし中身なしのコンストラクタ)を使ってtwoをnewする作業を代行してくれるので、それでもいいときはそうしよう。
上記コードでは、threeにアノテーションを付けていない。よってthreeはモックでもないしスパイでもない。いつもどおりの実インスタンスである。
そもそもスパイが欲しくなるのは
- デフォルトだと本物のロジックが動いてほしい
- でも一部のメソッドについては振る舞いを設定したい
- (さきほど少し触れたが
doReturn("black").when(two).getColor();のような文で設定可能)
- (さきほど少し触れたが
- 呼び出し回数を verify で検証したい
- (verifyはMockitoの機能のひとつ)
といったことをやりたい場面である。こうしたことをやる予定がないならスパイ化の必要はない。
方法3 @Autowired を使う
Springでは様々な部品をBeanと呼ぶ。
Springの中では ApplicationContext というコンテナが動いていて、Beanの生成(インスタンス化)や管理などを自動でやってくれる。
変数に @Autowired を付けておくとSpring起動時にコンテナからBeanのインスタンスが届く。
このとき、そのインスタンスが必要とする "依存性" も一緒にコンテナから届く。
というわけでインスタンスがお届けされた状態でテストが実行されるため、new文を自分で書かなくてよい。
@SpringBootTest // ←これを書くとSpringが起動する
class HogeServiceTest {
// テスト対象
@Autowired // ←これを書くことでコンテナからインスタンスを取得
private HogeService hogeService;
@MockBean
private One one; // コンテナ内のOneはモックに差し替え
@SpyBean
private Two two; // コンテナ内のTwoはスパイに差し替え
// 以下略!
}
OneやTwoに @MockBean や @SpyBean が付いているが、こうするとコンテナには本物のOneやTwoの代わりに「モック化されたOne」「スパイ化されたTwo」が保管される。
すると @Autowired で作成されるhogeServiceには、その「モック化されたOne」「スパイ化されたTwo」が注入される仕組みである。
また、上記コードではThreeの @MockBean や @SpyBean を書いていない。(書き忘れじゃないよ)
つまりコンテナには本来のThreeの実インスタンスが保管されているので、hogeServiceにはそれが注入されることになる。
で、この方法3だが、方法1や方法2と違ってSpringを起動させることになるので、もはやユニットテストではなく結合テストだと捉えたほうがよい。
Springを起動させると src/main/java にある全てのBean登録対象クラス(つまり @Component @Service @Repository 等のアノテーションが付いているクラス)を洗い出してひとつひとつコンテナに保管するという処理が走り出すため、テスト実行もやたら遅くなる。
方法3を選ぶのは、Springの機能が絡むテストをやるときだけにしておこう。
まとめ!
「テストクラスにおいてテスト対象のインスタンスをどうやって準備するか?」をテーマに、当記事では私が知っている方法を3つご紹介した。
- 方法1
@InjectMocksを使う-
@Mock@Spyの付いた変数が自動注入されて便利だが、書き忘れに注意
-
- 方法2 手動でnew文を書く
- 好きなものをコンストラクタの引数に渡せるのでシンプルイズベスト
- 方法3
@Autowiredを使う- コンテナで保管されているインスタンスが届いた状態でテスト可能
今回はサービスクラスを例にとったが、サービスクラスではないクラスのテストにも多少応用が利くはず。
おまけの余談
ひとつの変数に @InjectMocks と @Autowired の「同時付け」も一応可能である。
同時付けをしてもコンパイルエラーにならないので、そのままテスト実行もできてしまう。
よく分かっていないのだが、「Springがコンテナから持ってきたインスタンスが既にあるので、Mockitoはそのインスタンスに @Mock @Spy の付いた変数を注入しようとする」のだろうか?
同時付けは本来の使い方から逸脱していると思われるので、良い子の皆さんはマネしないように。
(もしかしたら私が知らないだけで、世の中には「同時付け」をうまく活用したテストテクニックが存在するのかもしれないが…)