はじめに
ひとり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回目のテストではisFoo
とisBar
がtrue
のUser
と文字列expected1
、
2回目のテストではisFoo
のみがtrue
のUser
と文字列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パターンになるかと思います。
これを@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
を見るだけで判断できるため、
多少は視認性が上がったのではないかと思います。
単体テストは分岐網羅のために条件の掛け合わせをしたい場面が少なからずあるはずなので、
タイミングは限られるものの、もっとパラメータが多い場合や設定する値が複雑な場合など、
選択肢の一つとしてこのような指定方法も理解しておくとどこかで役に立つやもしれません。