0
0

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 4

JUnit5の@ParameterizedTestにメソッド参照を渡す

Posted at

はじめに

ひとりJUnitアドベントカレンダー4日目の記事です。
2日の記事3日の記事に続いて本日もJUnit5の@ParameterizedTestです。

@ParameterizedTest + @MethodSource

一つのテストメソッドに対して引数を渡すことで、
複数の条件でテストを実行できるのが@ParameterizedTestです。
その際の引数の指定をメソッドで行うのが@MethodSourceで、以下のように記述します。

  @ParameterizedTest
  @MethodSource("hogeProvider")
  void test(User user, String expected) {
    hogeService.execute(user);
  }

  static Stream<Arguments> hogeProvider() {
    User fooBarUser = new User(true, true);
    User fooUser = new User(true, false);
    return Stream.of(arguments(fooBarUser, "expected1"), arguments(fooUser, "expected2"));
  }

  @AllArgsConstructor
  public class User {
    boolean isFoo;
    boolean isBar;
  }

このように書くことで、
1回目のテストではisFooisBartrueUserと文字列expected1
2回目のテストではisFooのみがtrueUserと文字列expected2が、
それぞれテストに渡されます。

メソッド参照を渡したくなるとき

以下のような(まあ滅茶苦茶な)クラス構成および処理があったとします。
そもそもこんな作りにするなって話ですが、
他システムからAPI経由で渡される構造がこんなんだったとか、まあないことはないでしょう。

  @Getter
  @Setter
  public class Data {
    ChildData1 child1 = new ChildData1();
    ChildData2 child2 = new ChildData2();
    ChildData3 child3 = new ChildData3();
  }

  @Getter
  @Setter
  public class ChildData1 {
    boolean isFoo;
  }

  @Getter
  @Setter
  public class ChildData2 {
    boolean isBar;
  }

  @Getter
  @Setter
  public class ChildData3 {
    boolean isBaz;
  }

  public class HogeService {
    public boolean execute(Data data) {
      return data.getChild1().isFoo() || data.getChild2().isBar() || data.getChild3().isBaz();
    }
  }

この時、HogeService#execute()のテストとして考えられるケースは
分岐網羅を考えると以下の8パターンになるかと思います。
image.png

これを@ParameterizedTestで実現したのが以下です。

  @ParameterizedTest
  @MethodSource("hogeProvider")
  void test(boolean isFoo, boolean isBar, boolean isBaz) {
    Data data = new Data();
    data.getChild1().setFoo(isFoo);
    data.getChild2().setBar(isBar);
    data.getChild3().setBaz(isBaz);
    hogeService.execute(data);
  }

  static Stream<Arguments> hogeProvider() {
    return Stream.of(
            arguments(true, true, true), 
            arguments(true, true, false), 
            arguments(true, false, true),
            arguments(true, false, false), 
            arguments(false, true, true),
            arguments(false, true, false), 
            arguments(false, false, true),
            arguments(false, false, false)
        );
  }

一方で、このような書き方もできます。

  @ParameterizedTest
  @MethodSource("mogeProvider")
  void test(Data data) {
    hogeService.execute(data);
  }

  static Stream<Arguments> mogeProvider() {
    Data basicData = new Data();
    Data fooData = new Data();
    fooData.getChild1().setFoo(true);
    Data barData = new Data();
    barData.getChild2().setBar(true);
    Data bazData = new Data();
    bazData.getChild3().setBaz(true);
    Data fooBarData = new Data();
    fooBarData.getChild1().setFoo(true);
    fooBarData.getChild2().setBar(true);
    Data fooBazData = new Data();
    fooBazData.getChild1().setFoo(true);
    fooBazData.getChild2().setBar(true);
    Data barBazData = new Data();
    barBazData.getChild2().setBar(true);
    barBazData.getChild3().setBaz(true);
    Data fooBarBazData = new Data();
    fooBarBazData.getChild1().setFoo(true);
    fooBarBazData.getChild2().setBar(true);
    fooBarBazData.getChild3().setBaz(true);

    return Stream.of(
            arguments(basicData),
            arguments(fooData),
            arguments(barData),
            arguments(bazData),
            arguments(fooBarData),
            arguments(fooBazData),
            arguments(barBazData),
            arguments(fooBarBazData)
    );
  }

最初の書き方だと、どの引数がどのフラグを更新するかがパッと見わかりませんでしたが
この書き方だと、変数名から条件を読み解くことがある程度可能となります。
ただ、事前準備としてDataのインスタンスをせっせと作る処理が煩雑ですね。

そこで、完成したインスタンスではなくメソッド参照を渡す方法で考えてみます。

  @ParameterizedTest
  @MethodSource("hogeProvider")
  void test(List<Consumer<Data>> conditions) {
    Data data = new Data();
    conditions.forEach(c -> c.accept(data));
    hogeService.execute(data);
  }

  static Stream<Arguments> hogeProvider() {
    Consumer<Data> fooCondition = (Data d) -> d.getChild1().setFoo(true);
    Consumer<Data> barCondition = (Data d) -> d.getChild2().setBar(true);
    Consumer<Data> bazCondition = (Data d) -> d.getChild3().setBaz(true);

    return Stream.of(arguments(Collections.emptyList()),
        arguments(Collections.singletonList(fooCondition)),
        arguments(Collections.singletonList(barCondition)),
        arguments(Collections.singletonList(bazCondition)),
        arguments(Arrays.asList(fooCondition, barCondition)),
        arguments(Arrays.asList(fooCondition, bazCondition)),
        arguments(Arrays.asList(barCondition, bazCondition)),
        arguments(Arrays.asList(fooCondition, barCondition, bazCondition))
    );
  }

まあまあすっきりしました。
テスト数分インスタンスを生成しなくてよくなりましたし、
初案とは異なり、どのような条件になるかはhogeProviderを見るだけで判断できるため、
多少は視認性が上がったのではないかと思います。

単体テストは分岐網羅のために条件の掛け合わせをしたい場面が少なからずあるはずなので、
タイミングは限られるものの、もっとパラメータが多い場合や設定する値が複雑な場合など、
選択肢の一つとしてこのような指定方法も理解しておくとどこかで役に立つやもしれません。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?