【単体テスト設計】どのようにしてテストコードを書くのか?

 テストコードは重要なものです。対象のコードの品質を担保してくれるばかりでなく、自動テストによって改修時のバグ発生を未然に防いだり、リグレッションテストの手助けにもなるでしょう。

 反面、テストコードの作成には、それなりの工数が掛かることも周知のとおりですから、工数をかけたくないプロジェクトでは後回しにされてしまいがちです。

テストコードとは

 メソッドなどの実行結果が適切かどうかをコード上で試験するものです。以下に例を挙げてみましょう。

 例は2つの引数を合計する単純なコードです。

public int sum(int a, int b) {
    return a + b;
}

 これに対してテストコードを書いてみます。jUnitのメソッドを使ってみましょう。

public void testSum() {
    int result = sum(1,2);
    assertEquals(result, 3);
}

 assertEqualsは、第一引数と第二引数が同一であればテスト成功とみなします。

 この例では、変数resultに入っている数値は1 + 2 = 3なので、第二引数の3と同一であるとみなされ、テストが成功します。

テストの意図

 コードを見れば動作が明確に分かるものをどうしてテストしなければならないのでしょうか?

 これは、検算と同じで、仕様に対しプログラムが間違いなく合っているかどうかを確かめているのです。

 「何を言っている、書かれたプログラムが一番正しいではないか」と考えても無理はありませんが、以下のケースをご覧ください。

 小数同士の差を計算するメソッドを定義してみます。

public double subtract(double a, double b) {
    return a - b;
}

 どのような動きをするでしょうか。確かめてみましょう。

subtract(1, 0.9);       // -> 0.09999999999999998

 さて、プログラマが期待する値は、1 - 0.9 = 0.1のはずですが、実際に出た答えは、微小ながら誤差が出ています。

 これは、有名な浮動小数点の丸め誤差です。コードを見てもすぐには気付けないのではないでしょうか?

 誤差としては小さいものですが、これが金融関係のシステムだったりすると、最も誤差が出てはいけないところに誤差が発生することとなり、多大な損害を生むこととなるでしょう。

 テストコードでは、こういったプログラマとプログラムの認識齟齬をはじめ、単なるバグや仕様外の事象などの、あらゆる異常を検出してくれる手助けとなります。

テスト設計

 テストは、考えられる入力に対し、正常な値が出力されているかを調べるものですが、値の取決めや範囲など、闇雲に決めていては正確性を欠きます。

 かといって、考えられる入力を全て試そうと思うと、莫大な時間がかかります。100までの数字を2つ受け取るメソッドをテストするだけでも1万通りのテストが必要ですから、現実的ではありません。

 本項では、そういった観点から、テスト設計技法をいくつかご紹介します。

ブラックボックステスト

 ブラックボックステストとは、プログラムのコードは無視し、仕様から与えるべき値を取り決める技法です。

同値分割

 同値分割では、その仕様に応じて、各範囲から値をひとつ抽出し、テスト用の値とする手法です。

 例を挙げてみましょう。

 仕様

  • メソッドは、年齢と基礎料金を入力する。
  • 年齢が6歳未満の場合は、基礎料金の0%を出力する。
  • 年齢が6歳以上で、かつ13歳未満の場合は、基礎料金の50%を出力する。
  • 年齢が13歳以上で、かつ18歳未満の場合は、基礎料金の80%を出力する。
  • 年齢が18歳以上で、かつ60歳未満の場合は、基礎料金の100%を出力する。
  • 年齢が60歳以上の場合は、基礎料金の90%を出力する。
  • 小数点は切り捨てる。

 この場合だと、以下の5つの値が必要です。

  • N < 6
  • 6 <= N < 13
  • 13 <= N < 18
  • 18 <= N < 60
  • 60 <= N

 5つの各範囲より、任意の数字を抽出します。

  • 3
  • 9
  • 15
  • 30
  • 65

 これで、仕様に沿った形で全てのケースをテストすることができますから、効率的ですね。

境界値分析

 プログラムを書く上で、最もバグが発生しやすい値が境界値です。先ほどの仕様に沿ってプログラムを書いてみます。

public int calculatePrice(int basePrice, int age) {
    if (age <= 6) {
        return 0;
    }

    if (age <= 13) {
        return basePrice * 0.5;
    }

    // ...
}

 明白なバグがありますね。しかし、同値分割ではこれが顕在化せず、見過ごされてしまいます。

 そうなっては大変ですので、同値分割に加えて、さらに境界値をテストしようというのが境界値分析法です。

 境界値分析では、同値分割で得られたグループの境界をそれぞれテストします。先ほどの仕様で言えば、以下のようになります。

  • 5 ( 「6未満」の境界値 )
  • 6 ( 「6以上」の境界値 )
  • 12 ( 「13未満」の境界値 )
  • 13 ( 「13以上」の境界値 )
  • 17 ( 「18未満」の境界値 )
  • 18 ( 「18以上」の境界値 )
  • 59 ( 「60未満」の境界値 )
  • 60 ( 「60以上」の境界値 )

 一気に数が増えました。しかし、これによって境界値に関する殆どのバグが発見できそうです。

無効値

 無効値を類推してテストをするようなケースが必要になるかもしれません。例えば・・・。

public int sum(int a, int b) {
    return a + b;
}

 これに対して、intの最大値と1を与えてみるとどうなるでしょう。

sum(2147483647, 1);     // -> -2147483648

 明らかに変な値が出力されます。

 本来であればバリデータなどで排除されるべきですが、特段そういった処理が無い場合は、仕様に応じて確認しておきましょう。

ホワイトボックステスト

 ホワイトボックス手法では、主にコードに着目して試験を行います。実際のプログラムに応じて効率的なテストが可能な反面、プログラマ自身の誤解や、あるべき分岐が無いなど、ソースコード上でのミスを発見できません。

 そのため、ブラックボックステストを主軸として、補助的な観点(テストカバレッジの向上)で実施すべきテストです。通常は、「複合条件網羅」を利用します。

 試しに、以下のコードをホワイトボックス観点でテストしてみましょう。

public boolean isValid(int a, int b) {
    boolean result = false;
    if (a > 1 || b > 10) {
        result = true;
    }
    return result;
}

命令網羅

 すべての命令を実行するようにテストを記述します。

aに与える値 bに与える値 結果
5 15 true

 この1つのテストデータで、全ての命令を実行できることから、このテストは命令網羅を満たしていると言えます。

分岐網羅

 すべての分岐方向を実行するようにテストを記述します。

aに与える値 bに与える値 a結果 b結果 結果
5 15 true true true
0 0 false false false

 これですべての分岐が実行されますので、分岐網羅を満たしたと言えます。

条件網羅

 すべての真偽が1回以上出現するようにテストします。ただし、分岐方向は問いません。

aに与える値 bに与える値 a結果 b結果 結果
0 15 false true true
5 0 true false true

 falseの条件が実行されていませんが、条件網羅は満たしていると言えます。

判定条件 / 条件網羅

 上記、分岐網羅と条件網羅を組み合わせてテストします。

aに与える値 bに与える値 a結果 b結果 結果
0 0 false false false
0 15 false true true
5 0 true false true

 双方trueの場合のテストが実行されていませんが、判定条件/条件網羅の基準は満たしています。

複合条件網羅

 条件に対し、全ての組み合わせをテストします。現在主流の考え方です。

aに与える値 bに与える値 a結果 b結果
5 15 true true
0 15 false true
5 0 true false
0 0 false false

 すべての組み合わせをテストできているので、複合条件網羅を満たしています。

単体テスト(ユニットテスト)に対するテスト設計の考え方

 ブラックボックステストとホワイトボックステストは、いずれも重要な観点です。

 実際のテスト設計では、同値分割・境界値分析を組み合わせたブラックボックステストに、複合条件網羅のホワイトボックステストを組み合わせてテストケースを作成するのが望ましいでしょう。

テストコーディング

 基本的には、テスティングフレームワークを利用して、テスト設計のとおりに書きます。
 例として、ホワイトボックステストの複合条件網羅を参考にテストメソッドを作成してみましょう。

public void testIsValid() {
    assertTrue(isValid(5, 15));
    assertTrue(isValid(0, 15));
    assertTrue(isValid(5, 0));
    assertFalse(isValid(0, 0));
}

 これで、エラーが起きた場合は対象クラスに異常があることがわかります。

 各言語やテストフレームワークでコードに多少の違いはありますが、基本的には入力値に対して期待される値が出力されているかを検査するにすぎませんから、大きな違いはないはずです。

まとめ

 テストコードは、テストコードそのものよりもテスト設計に多くの時間を割きます。これは本体のコーディングも同じことですが、設計がしっかりしていないと、検出できるはずの異常を見逃したりして、成果物の品質を著しく落とすことになってしまいます。

 まず設計をしっかりと行い、精度の高いテストコードを書くようにしていきましょう。

コメント

  1. 匿名 より:

    テストコードの元は、プログラム設計書ですか?

    • tam-tam より:

       ウォーターフォールでは、V字モデルと呼ばれるモデルがありました。

      * 要件定義ー受入試験(システム結合テスト)
      * 基本設計ー結合試験
      * 詳細設計ー単体試験

       このように、設計書と各種試験が一対一で紐付けられた指標があるため、テストはソースコードによってではなく、設計書によって書かれることが多かったのです。

       つまり、仰るとおり、テストコードの元はプログラム設計書ということで間違いありません。

       しかし、最近ではアジャイルの流行によって、必ずしもV字モデルを基準としたやり方を行うわけではなく、色々なやり方が考えられてきています。

       例えば、テストファーストという考え方では、いきなりテストコードを書いてから、プロダクトのコードを書きます。場合によっては詳細設計書も起こさずに、テストコードを詳細設計書替わりにすることもあります。

       どれが正解とか、間違いとかではなく、プロジェクトによって向き不向きもあるので、まずは「テストを何故やるのか?(テストによって何を保証したいのか)」を考えてみると良いかもしれません。

       テストファーストやテストドリブンなどの考え方については、色々と勉強になることもあるので、興味がおありでしたら、是非調べてみてください!