最近、JUnit でユニットテストを若手と一緒に書く機会がありました。
JUnit を利用したユニットテストは、JUnit4 のころから書くことはあったので、とくに初めて、というわけではないのですが、若手と「どんな風にユニットテストを書くか」ということ話すなかで、自分の中に一定の方針があることに気がつきました。
一度、それをまとめてみるのも悪くないかな、と考えて、ここに記載しておきます。
以降に記載されているのは「※個人の感想」であって、絶対的な指針でないことを、あらかじめ断っておきます。
ユニットテストを書く動機
えらそうに言うほどユニットテストを書いているわけではないのですが、自分がテストを書きたい・書かなきゃと思う動機としては、以下の様なものがあるようです。
動作が正しいことを確認したい
まあ、これは普通にテストの目的ですね。
自信をもってリリースしたい
これも一般的な動機かもしれないですね。
テストを書いて、通したことで、リリース時の安心感を得たい、と。
リファクタリングしたい
先日、 Java17 がリリースされましたね。
新しもの好きは、古くさい書き方をされているコードを見たら、ガンガン新しい書き方に変えたくなるものです。
レコードとか使ってDTOをシンプルに記述したり、switch式使って分岐を格好良くしたり、したいじゃないですか。
そんなとき、(適切な)テストがあれば、安心してリファクタリングすることができますね。
古いシステムにはテストが無かったりするわけなので、「仕方ない、テスト書くか」ってなります。
クラスの仕様・責務を明確にしたい
複雑な分岐をしている、責務が多いクラス・メソッドがある場合に、その内容を理解するためにテストを書きたくなります。
資料に書き出して整理する、でも良いのですが、ユニットテストとして書き起こせば、仕様も理解できるしテストも実施できるしで一石二鳥です。
既にテストが合ったとしても、新規に書き起こします。すると、既存のテストと比較して、自分のテストで足りなかったところや、逆に既存のテストの穴なんかを見つけることができたりします。
開発時にやっていること
上記の様な動機があって、ユニットテストを書くことになるのですが、テスト対象となるシステムの開発を行っているとき、ユニットテストを書きやすいようにやっていることがありました。
新規にクラスやメソッドを書くとき、私は以下の様なことを意識的・無意識的に実施しているようです。
実装を書きながらテストを書く
テストファーストでテスト駆動開発していくメリットに異論は無いのですが、最初にテストを書いて…というやり方を私はしていません。
最初に小さな実装を書いて、そのテストを書いて、また実装を進めて、実装に併せてテストを書いて…というやり方が多いです。
こうすることで、実装をマメに確認できますし、たまに、テストの穴を見つけることもあります。
テストしたいメソッド・変数は、 package private
にする
私の場合、新規にクラスを書くときは大抵スコープ宣言なしで書いて、package private
にします。
これは何故かというと、「テストクラスで参照したいから」です。
テストしたいのにテスト出来ない、という事態をあらかじめ回避しています。
もし、システム上 private
にする必要があれば、そのとき付ければいいや、ぐらいの心持ちでいます。
メソッドの責務は極小化する
テストが複雑になることは避けたいので、メソッドの責務は極小化してシンプルになるようにしています。
感覚的には、if
文による分岐がネストしない程度にしています。
ユニットテスト作成時にやっていること
JUnit5
+ Mockito
を使う
いまどき JUnit4
使っている人はいないと思いますが、新たにユニットテストを書くのであれば JUnit5
を使います。またMockito
も導入します。
JUnit5
を導入するのは nested tests
と parameterized tests
が使いたいからです。
Mockito
は、依存先クラスに併せてテストケースを作るのが苦痛なので、モックオブジェクトで依存関係を意識せずにテストできるようにするために導入します。
SpringBoot なら spring-boot-starter-test
に JUnit5 / Mockito が含まれているので、導入は楽ちんですね。
nested tests
で状態を表現する
nested test
が使いたいので JUnit5
を導入する、という話をしましたが、私は、nested tests
を使ってテストケースの状態を表現して書きます。
以下の様な感じです。
class HogeTest {
Hoge sut = new Hoge();
@DisplayName("Hoge の fuga が A のとき")
@Nested
class OnFugaIsA {
@BeforeEach
void setup() {
sut.fuga = "A";
}
@DisplayName("execute()がtrueを返すこと")
@Test
void executeReturnTrue() {
assertTrue(sut.execute());
}
}
@DisplayName("Hoge の fuga が B のとき")
@Nested
class OnFugaIsB {
@BeforeEach
void setup() {
sut.fuga = "B";
}
@DisplayName("execute()がfalseを返すこと")
@Test
void executeReturnTrue() {
assertFalse(sut.execute());
}
}
}
こう書くことで、ネストしたテストケースでも見通しが良くなります。
また、DisplayName
を工夫して書くと、テスト結果が
Hoge の fuga が A のとき
execute()がtrueを返すこと
Hoge の fuga が B のとき
execute()がfalseを返すこと
と表示されて、結果の見通してもよくなります。
Mockito を使って、テスト対象の責務にだけ注目する
複雑な構造になると、あるメソッドが別のメソッドを呼び出していたりします。
こうしたメソッドのテストを真面目にやろうとすると、別のメソッドのことも考えてテストを書かなければならなくなりますが、さすがにそれではテストを書くのに苦労するので、Mockitoを使います。
以下の様な感じです。
class methodTest {
@Spy
Hoge sut = new Hoge();
@DisplayTest("execute() は、fuga()を引数 A で呼び出していること")
@Test
void executeCallFugaWithA() {
sut.execute();
verify(sut).fuga("A");
}
}
こうすることで、Hoge#execute()
の責務である、メソッド fuga()
を引数 A で呼び出すことだけに注目してテストできます。
もちろん、メソッド fuga()
の責務は、別のテストメソッドでテストすることになります。
さいごに
ざっと、私がJUnitでユニットテストを書くときに考えていることを記載してみました。
今回、ユニットテストについて若手と話す機会を得ることで、自分がなんとなくやっていたユニットテストの書き方について整理ができたと感じています。いろいろと質問をしてくれた若手の方には感謝しかありません。
今回記載事は、もしかして、当たり前のことかもしれませんし、世間の常識とはまるで違っているかも知れません。
異論は受け付けますので、コメントを頂ければ幸いです。