0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ひとりJUnitAdvent Calendar 2022

Day 3

JUnit5の@ParameterizedTest + @EnumSourceで変更に強いテストを作る

Posted at

はじめに

ひとり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のみを指定しているので、一定義のみが引数として渡されます。

実行結果は以下の通りです。
image.png

もう一工夫したい

今回の本題です。

前提として、@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種類のみだった定義が、
IRONDIAMONDが増えて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で実行対象定義を指定しているため、
新たに追加された定義はテスト対象とならず、実行結果も変わらず以下の通りです。

image.png

それでは、以下の記述だとどうでしょうか。

  @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));
  }

上のテストの指定を「SILVERGOLD」ではなく「BRONZE以外」としています。
UserRankの定義が増えた状態で、上記のテストを実行した結果は以下です。

image.png

はい、テストが失敗しました。

冒頭で書いたテストはそれぞれ「BRONZE」「SILVER, GOLD」を対象にするものでした。
しかし今回の記述は「BRONZE」と「BRONZE以外」を対象としているため、
新たに追加された定義も自動的にテスト対象となり、処理の不備を検出することができます。

まとめ

enumの定義を増やす・変更する際は、使用されている箇所を全て見直すべきなのは当然ですが
そうは言っても人の為すことですから、見落としや誤解はあり得ます。

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?