こんにちは。テクマトリックス株式会社、奥村です。
今回は「JUnit パラメータ化テスト」について、具体例を用いて、通常のJUnit テストと比較しながら解説しようと思います。
目次
検証環境
OpenJDK | 11.0.17 |
Eclipse | 2022-09 (4.25.0) |
JUnit | 5 (Jupiter) |
結論
JUnit のパラメータ化テストを実施すべき場合
組み合わせのパターンが多いメソッドに対しては、「パラメータ化テスト」をした方が良いです。
パラメータ化テストコードの書き方
- テスト対象のメソッド
引数「i1」を処理し、戻り値「result」を返します。 - テストコード
引数「i1」と、処理後の期待値「expected」を記述したテストデータを呼び出して、
戻り値「result」と期待値「expected」が等しいか確認します。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
public class SampleTest {
@ParameterizedTest
@CsvFileSource(resources = "SampleTest_testCalc_parameters.csv", encoding = "UTF-8", numLinesToSkip = 1)
public void testCalc(int i1, int expected) throws Throwable {
Sample underTest = new Sample();
int result = underTest.calc(i1);
assertEquals(expected, result);
}
}
- テストデータ (SampleTest_testCalc_parameters.csv)
テストケースごとに、引数「i1」と、期待値「expected」を記述します。
ケース1 |
ケース2 |
ケース3 |
i1 | expected |
0 | 10 |
1 | 11 |
2 | -1 |
※ テストコードの解説
// アサート文のライブラリをインポート
import static org.junit.jupiter.api.Assertions.assertEquals;
// パラメータ化テスト、CSVのテストデータを取り込むためのライブラリをインポート
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
public class SampleTest {
// パラメータ化テストのアノテーション(テストメソッドであることを認識させる記述)
// 取り込むCSVを指定するためのアノテーション
@ParameterizedTest
@CsvFileSource(resources = "SampleTest_testCalc_parameters.csv", encoding = "UTF-8", numLinesToSkip = 1)
public void testCalc(int i1, int expected) throws Throwable {
// クラスのインスタンス化
Sample underTest = new Sample();
// メソッドの呼び出し
int result = underTest.calc(i1);
// アサート文(期待値と戻り値が等しいか確認する記述)
assertEquals(expected, result);
}
}
前提知識
1. JUnit とは
JUnitは、Java 言語を単体テストするためのテスティングフレームワーク です。
EclipseなどのIDEに標準搭載されているため、ご存じの方も多いのではないでしょうか。
テスト対象のメソッドごとにテストコードを作成し、実行するため、
- 実行環境に依存しない
- テストコードを残すことで記録になる
- 一度作ったテストコードを使いまわせる
という特徴があります。
2. パラメータ化テストとは
通常のJUnitテストコード(パラメータ化されていないもの)は、テストケースの個数分テストコードを記述します。
そのため、組み合わせのパターンが多いメソッドをテストする場合、記述が長くなることで難読化してしまいます。
そんなとき、便利なのが「パラメータ化テスト」です。
パラメータ化テストではテストデータを別ファイルに持たせ、共通する 1つのテストコードを使い回します。
これにより、組み合わせのパターンが多いメソッドでもテストコードの記述量が少なくて済みます。
3. パラメータ化テストのメリット
- 可読性を高く保てる
パラメータ化テストは、テストコードとテストデータを分離します。よって、テストデータがいくら増加したところでテストコードが長文化しないため、可読性が保たれます。
(JUnitは、「テストコードを記録として残す」「一度作ったテストコードを使います」という特徴から可読性は非常に重要な観点です。)
- 保守性が高い
保守性が高いテストコードとは、変更すべき箇所の特定が容易で、影響範囲が限定的であるということです。
パラメータ化テストはテストコードとテストデータの分離により、仕様変更が発生した際、どこを変更すべきかがとらえやすく、変更範囲も少なくなります。
単体テストの大幅な工数削減を実現できます。
通常のテストと比較してみた①(新規テスト)
具体的に「通常のテストコード」と「パラメータ化テストコード(テストデータ)」ではどのような違いがあるのでしょうか。
同じメソッドを対象に「通常のテストコード」と「パラメータ化テストコード(テストデータ)」をそれぞれ作成し、比較してみます。
テスト対象のメソッド
※ システム上、すべての引数には 0以上の整数が渡されるものとします。
public class Karaoke {
public int price(int status, int time, int age) {
int result = 0;
switch(status) {
case 0:// 会員
result = 200 * time;
break;
case 1:// 一般
if (age < 7) {
result = 100 * time;
} else {
result = 300 * time;
}
break;
default:
result = -1;
}
return result;
}
}
一人当たりのカラオケ料金を計算するメソッドです。
(利用者の属性別)1時間の料金 × 時間 で計算します。
引数
- 「status」利用者の属性を表します。
- 「0」会員
- 「1」一般(非会員)
- 「それ以外」例外処理を行う
- 「time」利用時間を表します。
- 「1」1時間利用
など
- 「1」1時間利用
- 「age」利用者の年齢を表します。
- 「7未満」一般(7歳未満)
- 「7以上」一般
テストケース
ステートメントカバレッジ(すべての命令を 1回以上実行する)が 100% になるような 4つのテストケースを作成します。
ケース1 |
ケース2 |
ケース3 |
ケース4 |
利用者の属性 | 利用時間 | 年齢 | 期待値 |
会員 | 5時間 | 25歳 | 1000円 |
一般 | 5時間 | 5歳 | 500円 |
一般 | 5時間 | 25歳 | 1500円 |
それ以外 | 5時間 | 25歳 | -1(例外) |
1. 通常のテストの場合
- テストコード
テストコード内のメソッドを実行し、戻り値「result」を取得します。
アサート文「assertEquals」にて、あらかじめ設定していた期待値「expected」と戻り値「result」が等しいか確認し、等しければテストがパスされます。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class KaraokeTest {
// ケース1
@Test
public void testPrice1() throws Throwable {
// Given
Karaoke underTest = new Karaoke();
// When
int status = 0;
int time = 5;
int age = 25;
int result = underTest.price(status, time, age);
// Then
assertEquals(1000, result);
}
// ケース2
@Test
public void testPrice2() throws Throwable {
// Given
Karaoke underTest = new Karaoke();
// When
int status = 1;
int time = 5;
int age = 5;
int result = underTest.price(status, time, age);
// Then
assertEquals(500, result);
}
// ケース3
@Test
public void testPrice3() throws Throwable {
// Given
Karaoke underTest = new Karaoke();
// When
int status = 1;
int time = 5;
int age = 25;
int result = underTest.price(status, time, age);
// Then
assertEquals(1500, result);
}
// ケース4
@Test
public void testPrice4() throws Throwable {
// Given
Karaoke underTest = new Karaoke();
// When
int status = 10;
int time = 5;
int age = 25;
int result = underTest.price(status, time, age);
// Then
assertEquals(-1, result);
}
}
2. パラメータ化テストの場合
パラメータ化テストでは、1つのテストコードと複数のテストケースを記述したテストデータファイルを作成します。
テストは、以下のようにケースごとに、その数だけ実行されます。
- テストデータ(KaraokeTest_testPrice_parameters.csv)のケース1をテストコード(KaraokeTest.java)で呼び出し、テストする
- ケース2をテストコードで呼び出し、テストする
…
- テストコード
通常のテスト同様、「アサート文を用いて期待値と戻り値を比較する」というテスト方法は変わりません。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
public class KaraokeTest {
@ParameterizedTest
@CsvFileSource(resources = "KaraokeTest_testPrice_parameters.csv", encoding = "UTF-8", numLinesToSkip = 1)
public void testPrice(int status, int time, int age, int expected) throws Throwable {
Karaoke underTest = new Karaoke();
int result = underTest.price(status, time, age);
assertEquals(expected, result);
}
}
- テストデータ (KaraokeTest_testPrice_parameters.csv)
1行ごとにテストケースとなっており、各変数に持たせる値が定義されています。
ケース1 |
ケース2 |
ケース3 |
ケース4 |
status | time | age | expected |
0 | 5 | 25 | 1000 |
1 | 5 | 5 | 500 |
1 | 5 | 25 | 1500 |
10 | 5 | 25 | -1 |
比較した結果
通常テストコードとパラメータ化テストコードを比較した時、記述量に大きな差があることが分かっていただけたのではないでしょうか。
今回は引数が 3つ、テストケース 4つのメソッドでしたが、テストコード行数に4倍以上の差があります。
(引数やテストケースがこれより増えた場合、さらに記述量に差が開きます。)
通常のテストコード | 70行 |
パラメータ化テストコード | 17行 |
このような組み合わせのパターンが多いメソッドのテストに対して、テストコード行数の少なく保て、それにより可読性が高いという点がパラメータ化テストの大きな魅力になります。
通常のテストと比較してみた②(仕様変更後のテスト)
続いて、保守性を確認してみます。
テスト対象のメソッドに仕様変更が起こった場合、各テストコードはどのように変更すべきでしょうか。
前章(通常のテストと比較してみた①)で作成した「通常のテストコード」と「パラメータ化テストコード(テストデータ)」を修正し、比較してみます。
テスト対象のメソッド
先ほどのカラオケ利用料金の計算に以下が追加されたメソッドです。
- 会員限定のクーポン(使用時に利用料金 20%引き)
- 利用者の属性「学生」(1時間 150円)
※ システム上、すべての引数に 0以上の整数が渡されるものとします。
public class Karaoke2 {
public int price(int status, int time, int discount, int age) {
int result = 0;
switch(status) {
case 0:// 会員
result = 200 * time;
if(discount == 1) {
result = result * 4 / 5;
}
break;
case 1:// 一般
if (age < 7) {
result = 100 * time;
} else {
result = 300 * time;
}
break;
case 2:// 学生
result = 150 * time;
break;
default:
result = -1;
}
return result;
}
引数(新たに追加されたもの)
- 「discount」利用者がクーポンを使用したかを表します。
- 「0」クーポンを使用しない
- 「1」クーポンを使用する
テストケース
以下を追加します。(追加部は赤文字で表示)
- 新しく追加された引数「discount」と、各ケースにおけるその値
- ステートメントカバレッジ(すべての命令を 1回以上実行する)が 100%になるように追加された2つのテストケース
ケース1 |
ケース2 |
ケース3 |
ケース4 |
ケース5 |
ケース6 |
利用者の属性 | 利用時間 | クーポン使用 | 年齢 | 期待値 |
会員 | 5時間 | 使用しない | 25歳 | 1000円 |
一般 | 5時間 | 使用しない | 5歳 | 500円 |
一般 | 5時間 | 使用しない | 25歳 | 1500円 |
それ以外 | 5時間 | 使用しない | 25歳 | -1 |
会員 | 5時間 | 使用する | 25歳 | 800円 |
学生 | 5時間 | 使用しない | 15歳 | 750円 |
1. 通常のテストの仕様変更
- テストコード
既存の 4テストケースそれぞれに以下を追加します。- 「discount」を初期化する処理を追加する(16行目)
- priceメソッドの呼び出しの引数に「discount」を追加する(18行目)
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class Karaoke2Test {
// ケース1
@Test
public void testPrice1() throws Throwable {
// Given
Karaoke2 underTest = new Karaoke2();
// When
int status = 0;
int time = 5;
int discount = 0;
int age = 25;
int result = underTest.price(status, time, discount, age);
// Then
assertEquals(1000, result);
}
…
さらに、新たに 2ケースを追加します。
…
// ケース5
@Test
public void testPrice5() throws Throwable {
// Given
Karaoke2 underTest = new Karaoke2();
// When
int status = 0;
int time = 5;
int discount = 1;
int age = 25;
int result = underTest.price(status, time, discount, age);
// Then
assertEquals(800, result);
}
// ケース6
@Test
public void testPrice6() throws Throwable {
// Given
Karaoke2 underTest = new Karaoke2();
// When
int status = 2;
int time = 5;
int discount = 0;
int age = 15;
int result = underTest.price(status, time, discount, age);
// Then
assertEquals(750, result);
}
}
2. パラメータ化テストの仕様変更
- テストコード
- testPriceメソッドの引数に「int discount」を追加する(10行目)
- priceメソッドの呼び出しの引数に「discount」を追加する(13行目)
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
public class Karaoke2Test {
@ParameterizedTest
@CsvFileSource(resources = "Karaoke2Test_testPrice_parameters.csv", encoding = "UTF-8", numLinesToSkip = 1)
public void testPrice(int status, int time, int discount, int age, int expected) throws Throwable {
Karaoke2 underTest = new Karaoke2();
int result = underTest.price(status, time, discount, age);
assertEquals(expected, result);
}
}
- テストデータ (Karaoke2Test_testPrice_parameters.csv)
テストケースに追加した要素をテストデータに反映させます。
ケース1 |
ケース2 |
ケース3 |
ケース4 |
ケース5 |
ケース6 |
※ 追加部は赤文字
status | time | discount | age | expected |
0 | 5 | 0 | 25 | 1000 |
1 | 5 | 0 | 5 | 500 |
1 | 5 | 0 | 25 | 1500 |
10 | 5 | 0 | 25 | -1 |
0 | 5 | 1 | 25 | 800 |
2 | 5 | 0 | 15 | 750 |
比較した結果
仕様変更の場合、パラメータ化テストコードの方が変更すべき箇所が見つけやすく、変更量が少ないことがわかります。
また、今回の仕様変更に伴い新たにテストケースを追加しましたが、それによってテストコードの記述量が増えていないことも確認していただけたのではないでしょうか。
番外編:テストコードを素早く生成する方法
ここまでパラメータ化テストについて解説してきましたが、いざ JUnit を使ってテストを実施しようとなった際に上手く導入できないことがあります。
例えば実装に時間がかかったり、JUnit テストコードの知識が必要になるなど、効率よくテストを実施することが難しいケースです。
このような場合、ツールを用いた対策が有効です。
一例として、「Parasoft Jtest」というツールの「テストコードテンプレートの自動生成」機能をご紹介します。
Java対応静的解析・単体テストツール Jtest について
Parasoft Jtest とは
Parasoft Jtest は、テスト工数の大幅削減とセキュアで高品質な Javaシステムの開発を強力にサポートする Java対応テストツールです。
以下のような機能があります。
- コード解析
- テスト自動化基盤
- 単体テスト
- アプリケーションカバレッジ計測
- 解析レポート
「JUnit テストコードテンプレートの自動生成」機能は「単体テスト」機能の一つです。
こちらの機能を使用していただくことで、画面上の操作のみで JUnit のテストコードテンプレートを作成してくれます。
これをベースにテストコードを作成することで、素早く実装することができます。
実は、今回用いた全ての JUnit テストコードは、「Parasoft Jtest」によって自動生成されたものをベースに作成しています。
使ってみて、私のような新人でも時間をかけることなく テストコードの実装ができることが分かりました。
「JUnit テストコードテンプレートの自動生成」機能を使ってみた
(以降より「Parasoft Jtest」は「Jtest」と表記します。)
- 検証環境
OpenJDL | 11.0.17 |
Eclipse | 2022-09 (4.25.0) |
JUnit | 5 (Jupiter) |
Parasoft Jtest Eclipse Plugin | 2022.2.0 |
- テスト対象のメソッド
「通常のテストコードと比較してみた①」で使用した Karaoke.java - 操作手順
画像は、Jtest のプラグインを導入したEclipseの画面です。この状態から操作します。
- 画面左部の Package Explorer から、
クラス(Karaoke.java)内のテストしたいメソッド(price)を選択します。 - 画面左下部の 単体テストアシスタント(Jtest専用ビュー)より「パラメータライズ」を押下します。
- テスト用のディレクトリに、テストコード、テストデータのテンプレートが作成されていることを確認してください。
※ 直前の手順で 「テスト スイートの作成」を押下することで、パラメータ化されていないテストコードテンプレートを作成することもできます。
作成されたテストコード、テストデータのテンプレート
- テストコード
テスト対象を解析し、以下を自動で記述してくれます。- クラスのインスタンス化
- メソッドの呼び出し
- アサ―ト文
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
/**
* Parasoft Jtest UTA: 次のテストクラス Karaoke
*
* @see jp.co.tmx.Karaoke
* @author okumura
*/
public class KaraokeTest {
/**
* Parasoft Jtest UTA: price(int, int, int) のテスト
*
* @see jp.co.tmx.Karaoke#price(int, int, int)
* @author okumura
*/
@ParameterizedTest
@CsvFileSource(resources = "KaraokeTest_testPrice_parameters.csv", encoding = "UTF-8", numLinesToSkip = 1)
public void testPrice(int status, int time, int age) throws Throwable {
Karaoke underTest = new Karaoke();
int result = underTest.price(status, time, age);
// assertEquals(expected, result);
}
}
- テストデータ (KaraokeTest_testPrice_parameters.csv)
テストコードと同じフォルダ内に自動でCSVを作成してくれます。
※ テストデータに関しては、あらかじめ設定した値が設定されます。ソースコードに合わせて自動生成されるわけではありません。
status | time | age |
0 | 0 | 0 |
1 | 1 | 1 |
-1 | -1 | -1 |
このように、Jtest は画面上の操作のみで素早く JUnit のテストコード(テストデータ)のテンプレートを作成することができます。
使うことで、
- テストコード作成の工数を大幅に削減する
- テストコードを書くことに慣れていない人がテストを実施する
などの効果が期待できます。
まとめ
以上、「JUnit パラメータ化テスト」について具体例を用いて解説しました。
活用していただくことで、組み合わせのパターンが多いメソッドのテストを行数の少ないテストコードで実現できます。
さらに、テストコードとテストデータが分離していることで仕様変更に対応しやすい、テストケース増加による影響が少ないなどのメリットも確認していただけたのではないでしょうか。
JUnit での単体テストを効率化にお役立てていただければ幸いです。
また、後半では JUnit のテストコードテンプレートを自動生成できる「Parasoft Jtest」についても紹介いたしました。
お使いいただくことで、単体テストの大幅な工数削減や、JUnit に不慣れな方がテストが実施できます。
無料の体験版もございますので、活用することを検討してみてはいかがでしょうか。