はじめに
ひとりJUnitアドベントカレンダー3日目の記事です。
12/2分の記事からのある意味続きです。
@EnumSourceってなんだっけ
@EnumSourceの使い方は上記の記事で説明していますが、改めて簡単に触れます。
まず、以下のようなロジックがあるとします。
public class HogeService {
public boolean isDiscountTarget(UserRank userRank) {
return userRank.getScore() > 1;
}
}
@AllArgsConstructor
@Getter
public enum UserRank {
BRONZE(1),
SILVER(2),
GOLD(3);
private int score;
}
UserRankの変数として割引有無を持たせるべきとか、そういう設計のツッコミは置いといて
このようなenumを元に処理の分岐を行っている実装に対しては、
JUnit5の@ParameterizedTest+@EnumSourceを使うと便利だよ、という話です。
@ParameterizedTest
@EnumSource(value = UserRank.class, names = {"SILVER", "GOLD"})
void 割引がある場合(UserRank userRank) {
assertThat(hogeService.isDiscountTarget(userRank), is(true));
}
@ParameterizedTest
@EnumSource(value = UserRank.class, names = {"BRONZE"})
void 割引がない場合(UserRank userRank) {
assertThat(hogeService.isDiscountTarget(userRank), is(false));
}
@EnumSource(value = UserRank.class, names = {"SILVER", "GOLD"})
テストに対して付与しているこちらのアノテーションは、
UserRankというenumの、SILVER,GOLDの定義を指定する書き方です。
下のテストはBRONZEのみを指定しているので、一定義のみが引数として渡されます。
もう一工夫したい
今回の本題です。
前提として、@EnumSourceはいろいろな指定の仕方ができて、
例えば@EnumSource(value = UserRank.class)というように、
namesを設定しない場合はUserRankの全ての定義を引数として渡すことができます。
ただ、今回のテスト対象であるhogeService.isDiscountTarget()もそうですが、
基本的には定義によって期待結果が異なるからこそテストしているはずなので、
enumの全定義を引数として渡すというのは使用方法としてあまり現実的ではありません。
一方で、先ほどの定義に加えてさらにmodeを設定し、
@EnumSource(value = UserRank.class, names = {"BRONZE"}, mode = EnumSource.Mode.EXCLUDE)というような指定をすると
UserRankの定義のうち、BRONZE以外の全てを引数として渡すことができます。
さて、話を元に戻します。
今回の例として挙げているUserRankというenumですが、
テスト対象の処理であるhogeService.isDiscountTarget()以外でも広く使われており、
あるタイミングで、別の機能追加の事情で以下のように定義が増えたとします。
@AllArgsConstructor
@Getter
public enum UserRank {
IRON(0),
BRONZE(1),
SILVER(2),
GOLD(3),
DIAMOND(4);
private int score;
}
今まではBRONZE, SILVER, GOLDの3種類のみだった定義が、
IRONとDIAMONDが増えて5種類になっています。
この状態で、冒頭のテストを実行するとどのような結果となるでしょうか。
@ParameterizedTest
@EnumSource(value = UserRank.class, names = {"SILVER", "GOLD"})
void 割引がある場合(UserRank userRank) {
assertThat(hogeService.isDiscountTarget(userRank), is(true));
}
@ParameterizedTest
@EnumSource(value = UserRank.class, names = {"BRONZE"})
void 割引がない場合(UserRank userRank) {
assertThat(hogeService.isDiscountTarget(userRank), is(false));
}
2種類のテストはどちらもnamesで実行対象定義を指定しているため、
新たに追加された定義はテスト対象とならず、実行結果も変わらず以下の通りです。
それでは、以下の記述だとどうでしょうか。
@ParameterizedTest
@EnumSource(value = UserRank.class, names = {"BRONZE"}, mode = EnumSource.Mode.EXCLUDE)
void 割引がある場合(UserRank userRank) {
assertThat(hogeService.isDiscountTarget(userRank), is(true));
}
@ParameterizedTest
@EnumSource(value = UserRank.class, names = {"BRONZE"})
void 割引がない場合(UserRank userRank) {
assertThat(hogeService.isDiscountTarget(userRank), is(false));
}
上のテストの指定を「SILVERかGOLD」ではなく「BRONZE以外」としています。
UserRankの定義が増えた状態で、上記のテストを実行した結果は以下です。
はい、テストが失敗しました。
冒頭で書いたテストはそれぞれ「BRONZE」「SILVER, GOLD」を対象にするものでした。
しかし今回の記述は「BRONZE」と「BRONZE以外」を対象としているため、
新たに追加された定義も自動的にテスト対象となり、処理の不備を検出することができます。
まとめ
enumの定義を増やす・変更する際は、使用されている箇所を全て見直すべきなのは当然ですが
そうは言っても人の為すことですから、見落としや誤解はあり得ます。
@EnumSourceを使う場合は、テスト作成時に存在する定義のみを指定するのではなく
上記のような指定を行い、追加があっても自動的に全定義をテストできるような形にしておくと
変更による影響を正しく検出することができるのではないかと思います。

