SE情報技術研究会’s blog

http://se-info-tech.connpass.com

2016-01-30 『JUnit実践入門』読書会(第8章)の振り返り

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

第8章 パラメータ化テスト

  • パラメータ化テストとは入力値と期待値をパラメータ化しテストする手法
  • ユニットテストではテストケースが多くなっても、テストケースごとの差異は入力値と期待値のみとなる場合が多い(入力値による正常系や異常系など)
  • 入力値と期待値をパラメータ化することでテストの見通しや入力値と期待値の追加や削除が容易になる
  • Theroiesテストランナーを使用することでパラメータ化テストが行える

テストデータの選択

  • 開発リソースも有限であるため、可能な限り少ないテストデータで効率良くテストすることが求められる
  • テストデータの選択方法はテストの品質に大きく影響を与える

どのくらいのテストデータが必要か?

同値クラスによるテストデータの選択

同値クラスとはテスト対象メソッドで同じ結果を返す入力値の集合

例:18歳以上かを判定するメソッドには次の2つの同値クラスがある

  • 18以上の値から1つ
  • 18未満の値から1つ

組み合わせによるテストデータの選択

  • 入力値が複数の値の組み合わせになる場合、組み合わせを考慮しないといけない
  • 全組み合わせを試すことは不可能なことがある

下記の基準より

「プログラムに混入する不具合のほとんどは単項目に関する不具合である」

テストデータの選択

  • すべての条件を満たす(成功)パターン
  • 入力値の各項目で対象の項目だけ失敗するパターン

どのテストデータを選択するか?

  • 同値クラスで分析しグループ分けする
  • 不具合は境界値付近で発生しやすいので境界値付近のデータを使用する (特に「以下」や「未満」など境界値を含むか含まないか)

コラム:妥当な値の範囲を制限する

  • 年齢など値に特化したオブジェクトを作成し入力可能な値を制限することでテストも効率的になる
  • プログラムのシンプルさとのトレードオフ

入力値と期待値のパラメータ化

テストしないといけないデータの組み合わせが多い場合、テストコードが長くなりすぎる

Theoriesテストランナーを使ったパラメータ化テストが有効

テストデータとテストメソッドを分割できる

Theories

  • パラメータ化テストをサポートするテストランナー
  • Enclosedテストランナーとの併用も可能
  • @Theoryでパラメータ化テストであることを宣言し、任意の引数(@DataPoint@DataPoints)が受け取れるようになる
  • @DataPointでフィールドをパラメータとして宣言する(1フィールド1パラメータの宣言しかできない)
  • @DataPointsでフィールドを複数のパラメータとして宣言する
@RunWith(Theories.class)
public class ParameterizedTest {
  
  @DataPoint
  public static int PARAM_1 = 1;
  @DataPoint
  public static int PARAM_2 = 2;
  
  public ParameterizedTest() {
    System.out.println("コンストラクタ");
  }
  
  @Theory
  public void 引数を受け取るテストケースTest(int param) {
    System.out.println("param=" + param);
  }
}

※ 本のサンプルを若干修正

実行結果

コンストラクタ
param=1
コンストラクタ
param=2

例:Enclosedテストランナーとの併用

@RunWith(Enclosed.class)
public class テストデータの構造化Test

  @RunWith(Theories.class)
  public static class パラメータ化Test1 {
    …略
  }

  @RunWith(Theories.class)
  public static class パラメータ化Test2 {
    …略
  }
  …略

複数のテストメソッドがある場合

  • @DataPointで宣言した型と@Theoryで宣言したテストメソッドの引数の型が同じものがパラメータ化テストの対象となる
  • このようなケースは直観的ではないのでEnclosedテストランナーで分割したほうがよい

複数の引数が定義されている場合

  • @DataPointで宣言されたパラメータのすべての組みあわせが実行される
  • 型が同じ場合は重複も含め全パターン実行される
  • そのような場合は引数と結果を持つフィクスチャオブジェクトを自作し、それをパラメータとして使う
  • フィクスチャオブジェクトはJUnitの機能ではなく自作のオブジェクト
  • 複数の引数が定義されているテストケースの場合、全組み合わせが実行されるので、戻り値が引数によって変わるものはテストできない

@DataPoints 複数のパラメータを定義するアノテーション

  • 1つのフィールドにフィクスチャオブジェクトなどの配列を定義し1レコードづつテストするためのパラメータ
  • テストデータと結果を外部リソースとして定義しやすくなる

組み合わせテスト

  • 複数の引数が定義されているテストケースで対象のパラメータ値の場合のみテストケースとして評価したい場合、assumeThatを使う(全組み合わせが実行されても問題無いようにする)

Assumeによるパラメータのフィルタリング

  • assumeThatメソッド(やassumeTrueメソッド)は検証結果が失敗でもテスト結果の失敗とせず、そのテストケースを終了する
  • assumeThatメソッドは検証失敗時にAssumptionViolateExceptionを発生させテスト結果を成功として処理する
  • import static org.junit.Assume.*;を宣言する
  • assumeThatメソッド@Theoryだけでなく@Testでも使える

assumeメソッドを使った例:

@DataPoints
public static final int[] VALUES = {1, 2, 3, 4, 5};

@Theory
public void 偶数のみ検証されるテスト(int value){
  assumeTrue(value %2 == 0);
  assertThat(value %2 == 0, is(true));
}

パラメータ化テストの問題

  • データの網羅性はJUnitが保証するわけではない(テスト開発者が自分で行う)
  • 失敗時の情報にパラメータの情報が少ない

※ 本には具体的な値がでないとあったがJUnit 4.12だと失敗時のパラメータの値の情報は出てるもよう

@Theory
public void 偶数は失敗するテスト(int value) {
  assertThat(value % 2 == 1, is(true));
}

実行結果

org.junit.experimental.theories.internal.ParameterizedAssertionError: 偶数は失敗するテスト("2" <from VALUES[1]>)
...略
Caused by: java.lang.AssertionError: 
Expected: is <true>
     but: was <false>
    ...略