はじめに
ひとり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
を使う場合は、テスト作成時に存在する定義のみを指定するのではなく
上記のような指定を行い、追加があっても自動的に全定義をテストできるような形にしておくと
変更による影響を正しく検出することができるのではないかと思います。