【Java】【Junit5】@Parametarizedによるパラメータ化テスト
本記事の概要
Junit5より@ParametarizedTestを使用することでテストメソッド単位のパラメータ化テストを簡単に実装できるようになりました。テストメソッドへのパラメータもCSVファイルやヘルパーメソッドなど様々な方法でテストデータを提供できるようになっています。
本記事ではパラメータ化テストの概要とJunit5での実装方法について特に@Parameterized, @CsvSource, @MethodSourceの使い方を紹介します。
パラメータ化テストとは
テストケースで使用するデータをテストケースに引数として渡す手法をパラメータ化テスト(Parameterized Test)と呼びます。複数のテストケースでテストのロジックが共通でテストデータのみが異なる場合、パラメータ化テストにすることで重複コードを削減することができます。
例として以下のCustomerクラス内のfirstNameとlastNameを半角スペースで結合した文字列を返すgetFullNameメソッドのテストを考えます。実際のシステムとしては色々と突っ込みどころがありますが、例なので許容お願いします。
// importは省略
public record Customer(String firstName, String lastName) {
public Customer {
// firstName, lastNameに対するバリデーションを行う
}
public String getFullName() {
if (StringUtils.isEmpty(firstName) && StringUtils.isEmpty(lastName)) {
return "";
}
if (StringUtils.isEmpty(firstName)) {
return lastName;
}
if (StringUtils.isEmpty(lastName)) {
return firstName;
}
return String.join(" ", firstName, lastName);
}
}
getFullNameメソッドの仕様は以下のとおりです。
- firstNameとlastNameがともに空文字ではない場合、firstName, lastNameの順に半角スペースで結合した文字列を返す。
- firstNameが空文字場合、lastNameを返す。
- lastNameが空文字場合、firstNameを返す。
また、firstName, lastNameの値として以下を前提条件とします。
- firstName, lastNameともに値中のnull、スペースは許容しない。
- firstName, lastNameともに空文字は許容する。
getFullNameメソッドのテストケースのパターンを表にすると以下のようになります。
firstName | lastName | getFullNameの返り値 |
空文字 | 空文字 | 空文字 |
“hoge" | 空文字 | “hoge" |
空文字 | “foo" | “foo" |
“hoge" | “foo" | “hoge foo" |
このテストパターンをパラメータ化テストを使用せずに実装した例が以下のテストコードです。
// importは省略
public class CustomerTest {
@Test
public void firstNameとlastNameがともに空文字の場合はgetFullNameは空文字を返す() {
var customer = new Customer("", "");
assertThat(customer.getFullName()).isEqualTo("");
}
@Test
public void lastNameが空文字の場合はgetFullNameはfirstNameを返す() {
var customer = new Customer("hoge", "");
assertThat(customer.getFullName()).isEqualTo("hoge");
}
@Test
public void firstNameが空文字の場合はgetFullNameはlastNameを返す() {
var customer = new Customer("", "foo");
assertThat(customer1.getFullName()).isEqualTo("foo");
}
@Test
public void firstNameとlastNameがともに空文字でない場合はgetFullNameはfirstNameとlastNameを半角スペースで結合して返す() {
var customer = new Customer("hoge", "foo");
assertThat(customer.getFullName()).isEqualTo("hoge foo");
}
}
データパターンが増えた場合にテストコードが長くなるため、テストの可読性、保守性が低下してしまいます。1つのテストメソッドの内に複数のデータパターンのアサーションを記述する方法も考えられますが、これはアサーションルーレットと呼ばれるアンチパターンとなります。
パラメータ化テストにすると以下のようなテストコードとなります。コード量が削減されたことと、テストデータがアノテーション内に分離しておりテストの可読性、保守性が向上しています。
// importは省略
public class CustomerTest {
@ParameterizedTest
@CsvSource({
"'', '', ''",
"hoge, '', hoge",
"'', foo, foo",
"hoge, foo, hoge foo"
})
public void fistNameとlastNameのデータパターンに対するgetFullNameのテスト(String firstName, String lastName, String fullName) {
var customer = new Customer(firstName, lastName);
assertThat(customer.getFullName()).isEqualTo(fullName);
}
}
各アノテーションについてはこの後詳しく説明します。
@ParametarizedTestを使ったパラメータ化テスト
@CsvSourceの使い方
@CsvSourceは下記の例のようにアノテーション内にCSV形式でパラメータを記述します。テストメソッドの各引数とCSVのカラムが並び順に対応します。テストメソッドのn番目の引数にはCSVのn番目のカラムの値が代入されます。
@CsvSource内には文字列でデータを記述するため、テストメソッドの引数へはキャストが発生します。キャストで対応できない型変換が必要な場合は後述の@MethodSourceを使う必要があります。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class PointTableTest {
@ParameterizedTest
@CsvSource({
"0, 0",
"999, 0",
"1000, 100",
"1999, 100",
"2000, 200",
"2001, 200",
})
public void pointTable1Test(int paymentAmount, int expectedPoint) {
var pointTable = new PointTable1();
assertThat(pointTable.calculatePointsOnPurchase(paymentAmount)).isEqualTo(expectedPoint);
}
@ParameterizedTest
@CsvSource({
"0, 0",
"999, 0",
"1000, 100",
"1999, 100",
"2000, 200",
"2001, 200",
})
public void pointTable2Test(int paymentAmount, int expectedPoint) {
var pointTable = new PointTable2();
assertThat(pointTable.calculatePointsOnPurchase(paymentAmount)).isEqualTo(expectedPoint);
}
}
@CsvFileSourceの使い方
@CsvFileSourceを使うことでテストデータを記述したCSVファイルをパラメータのデータソースとすることができます。Spring BootでのresourcesディレクトリをルートディレクトリとしてデータソースCSVファイルを指定するにはアノテーションのresources に"/TestData.csv" のように記述します。
@CsvFileSourceも@CsvFileSourceと同様にテストメソッドの各引数とCSVのカラムが並び順に対応します。テストメソッドのn番目の引数にはCSVのn番目のカラムの値が代入されます。
また、CSVファイル内では文字列でデータを記述するため、テストメソッドの引数へはキャストが発生します。キャストで対応できない型変換が必要な場合は後述の@MethodSourceを使う必要があります。
// importは省略
public class CustomerTest {
@CsvFileSource(resources= "/TestData.csv")
@ParameterizedTest
public void test_get_full_name(String firstName, String lastName, int age, String expected) {
var customer1 = new Customer(firstName, lastName, age);
assertThat(customer1.getFullName()).isEqualTo(expected);
}
}
この例でのTestData.csvは以下のように作成しました。
"hoge", "foo", "18", "hoge foo"
"HOGE", "FOO", "18", "HOGE FOO"
@MethodSourceの使い方
@MethodSourceを使うことで独自に定義したメソッドをパラメータのデータソースとすることができます。
@ParametarizedTestを付与したテストメソッドのパラメータのデータソースとしたいメソッド名を@MethodSourceの引数に指定します。
データソースとなるメソッドは返り値の型がStream<Arguments>として作成します。Stream中の1つのAurgumentsインスタンスが1つのテストケースのパラメータを表しています。
テストメソッドの引数が複数あり、プリミティブ、String型以外の型の場合は@MethodSourceを使うとよいと思います。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.math.BigDecimal;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
class PointRateTableTest {
@ParameterizedTest
@MethodSource("dataProvider")
public void bronze_rank_test(CustomerRank customerRank, BigDecimal purchaseAmount, BigDecimal expected) {
var pointTable = new PointRateTable();
var rate1 = pointTable.getPointRateByCustomerRankAndPurchaseAmount(customerRank, purchaseAmount);
assertThat(rate1).isEqualTo(expected);
}
static Stream<Arguments> dataProvider() {
return Stream.of(
arguments(CustomerRank.BRONZE, new BigDecimal("1"), new BigDecimal("0.01")),
arguments(CustomerRank.BRONZE, new BigDecimal("999"), new BigDecimal("0.01")),
arguments(CustomerRank.BRONZE, new BigDecimal("1000"), new BigDecimal("0.05")),
arguments(CustomerRank.BRONZE, new BigDecimal("9999"), new BigDecimal("0.05")),
arguments(CustomerRank.BRONZE, new BigDecimal("10000"), new BigDecimal("0.1")),
arguments(CustomerRank.SILVER, new BigDecimal("1"), new BigDecimal("0.02")),
arguments(CustomerRank.SILVER, new BigDecimal("999"), new BigDecimal("0.02")),
arguments(CustomerRank.SILVER, new BigDecimal("1000"), new BigDecimal("0.06")),
arguments(CustomerRank.SILVER, new BigDecimal("9999"), new BigDecimal("0.06")),
arguments(CustomerRank.SILVER, new BigDecimal("10000"), new BigDecimal("0.11"))
);
}
}