若くない何かの悩み

何かをdisっているときは、たいていツンデレですので大目に見てやってください。

テスト技法「同値分割」を信頼していいのかわからなくなった

これまで同値分割を信頼できる手法だと信じてきました。最近になってどうして同値分割が信頼できる方法なのかその理由を私が説明できないことに気づきました。この原因は2つあります:

  1. 同値分割の分割の基準が不明確であること
  2. 後述するいくつかの仮定を満たさない場合、ある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいといえる根拠に乏しいこと

この2つから、不明確な基準の同値分割はその信頼性の説明ができないこと、同値テストは後述するいくつかの仮定が満たされたときのみ有効な手段でありいずれかの仮定が満たされない場合はさして信頼できないことが導かれます。

この記事ではこの結論に至るまでの過程について詳しく説明していきます。なお誤りのご指摘は大歓迎です。ぜひ皆さんで議論しましょう。

同値分割とは

後述する複数の文献の同値分割の説明に共通しているのは以下の2点です:

  1. 入力空間を分割する(分割された空間のことを同値パーティションまたは同値クラスまたはドメインという)
  2. ある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいとする

▶︎ 11文献それぞれの同値分割の説明(クリックして開く)

これらの文献の同値分割の説明を確認してみましょう:

テスト対象が同じ振る舞いをすると仮定できる入力や出力などの値の集合や範囲を 「同値パーティション (同値クラスとも呼ぶ)」 としてまとめ, 同値パーティション内の代表値のみをテストすることによりテストの数を削減する技法である。

── ソフトウェア品質知識体系ガイド (第3版) ―SQuBOK Guide V3― (p.202).


同値分割法では、まずシステムやソフトウェアの入力データを同じ処理が行われるグループに分割します。グループ内のデータはシステムやソフトウェアで同じ処理が行われるため、同じもの(これを同値と言います)とみなします。つまり、グループ内の代表的なデータを 1 つ選び、そのデータを使ってテストすれば、同じグループの他のデータもテストしたものとみなせるからです。

── ソフトウェアテスト教科書 JSTQB Foundation 第3版


(中略)プログラムの入力領域を有限の数の同値のクラスに分割する試みをすべきであることを意味する.というのは,それぞれのクラスの代表的な値をテストすることは,他の値のすべてをテストするのと同等であると考えられるからである(もちろん絶対的にそうだとはいえないが).

── ソフトウェア・テストの技法 (p.53)


  1. 同値分割

入力空間をテストに対して同値な部分空間に分割して,各部分から1つのテストケースを選ぶもの.テストに対して同値とは,1 つのテストデータで実行しエラーがなければ,同じ類の他のデータに対してもエラーがないことが保証されるというものである.

── ソフトウェア工学の基礎 改訂新版 (p.235)


ドメインテスト:ドメインとは,(数学的には)ある関数について変数がとりうるすべての値の集合のことをいう。ドメインテストでは,まず関数と変数を分類する。変数には入力用のものと出力用のものがある。(数学的には入力と出力は区別すべきであるが,テストに関してはどちらも同じような分析をするので,ここでは特に変数を入力,出力と分けて考えない。)各々の変数について,とりうる値を同値クラスに分割して,(境界値などの)各クラスの代表値を数個選択する。このテスト技法は,ある変数について,数個の代表値を上手く選び出すことができれば,そのクラス内のすべての値でテストしなくとも発見できるはずのバグをほとんど見つけ出すことができるという仮定のもとに立っている。

── ソフトウェアテスト293の鉄則 (p.104) *1


同値クラスは,モジュールで同等に処理されるデータ、あるいは同じ結果を返すデータの集合を意味します。クラスの中のどのデータも、テストという観点からは、他のデータと「同じ値」と見なせるわけです。もっと細かく言うと、同値クラスには以下のような条件があります。

  • 同値クラス内のあるテストケースで欠陥が検出された場合には、同じ同値クラスの他のすべてのテストケースで同じ欠陥が検出される
  • 同値クラス内のあるテストケースで欠陥が検出されない場合には、同じ同値クラスの他のどのようなテストケースでも欠陥は検出されない

── はじめて学ぶソフトウェアテストのテスト技法 (p.47) *2


「同値分割」とは、入力領域を「同値クラス」という部分集合に分割し、その部分集合に入る入力値を等価とみなす作業です。

── 知識ゼロから学ぶ ソフトウェアテスト


ドメインテストやパステストのようなパーティションテスト方式の基本的な考えは,入力空間を等価なクラスに分割することである。同値クラスに分割できれば,同値クラス中の任意のものを1つ選んでテストすると全部をテストしたのと同じ効果がある。

── ソフトウェアテスト技法 (p.337)


同値分割テストとは、同値パーティション(同じ動作をする条件の集まり)ごとにテストを行うテスト技法です。同値パーティションには、

  • 同じ処理結果となる「入力値」の集まり
  • 同じ処理結果となる「時間」の集まり
  • 同じ入力値から処理される「出力結果」の集まり

などがあります。同値パーティションは条件の境目によって区切られ、集まりを形成しています。

── ソフトウェアテストの教科書


同値分割法というのは、入力される可能性のあるデータすべてテストするのはたいへんなので入力をグルーピングしてそれぞれのグループから代表となる値を選びそれだけをテストする方法です。「それだけ」と書きましたが、ある観点でグループ分けして、そこから代表値を選ぶわけですから「網羅」するテストになります。

── ソフトウェアテスト技法ドリル (p.19)


二つのテストを実行して同じ結果を期待するとき、二つは 同値 であるという。同じグループのテストが,以下を満たしていれば同値クラスを形成する。

  • 同じ機能をテストする。
  • 一つのテストで障害が見つかれば,残りのテストでも見つかると予想できる。
  • 一つのテストで障害が見つからなければ,残りのテストでも見つからないと予想できる。

── 基本から学ぶソフトウェアテスト (p.123)

この記事の論点は 1 の分割基準は不明確で、かつ 2 はなぜそういえるのかがよくわからないということです。1 が十分明確でない限りそもそも手法の信頼性について議論ができないですし、2 がもし成り立たないとすると代表値のテストは同数のサンプルのモンキーテスト*3で代えられるということになります*4

まず 1 の入力空間の分割がどのような基準でおこなわれるかをみてみましょう。

入力空間の分割の基準

前述の文献の分割の基準は一致しておらず、文献によっては一般的な基準を定義せず例示のみで済ませていることもあります。解釈に解釈を重ねた結果、一般的な基準を定義しているものについては大雑把には4つのグループに分類できそうです*5

  • 基準グループA: 関数的な仕様の出力を無効*6な入力と有効な入力に分ける
  • 基準グループB: 想定される実装の制御フローのパスの一致を同値関係とする(この基準は シンボリック実行 で生成される空間とよく似ています)
  • 基準グループC: 仕様のデータフローグラフのパスの一致を同値関係とする
  • 基準グループD: 関数的な仕様の出力の一致を同値関係とする
文献名 属する基準グループ
ソフトウェア品質知識体系ガイド (第3版) ―SQuBOK Guide V3― A, C, D のいずれか(不明確)*7
ソフトウェアテスト教科書 JSTQB Foundation 第3版 A, C, D のいずれか(不明確)*8
ソフトウェア・テストの技法 A および B の両方(不明確)*9
ソフトウェア工学の基礎 改訂新版 A(不明確)*10
はじめて学ぶソフトウェアテストのテスト技法 B(不明確)*11
ソフトウェアテスト技法 C
知識ゼロから学ぶ ソフトウェアテスト C(不明確)*12
ソフトウェアをカイゼンする50のアイデア A および D の両方(不明確)*13
ソフトウェアテスト技法練習帳 A(不明確) *14
ソフトウェアテストの教科書 D
ソフトウェアテスト技法ドリル A(不明確) *15
基本から学ぶソフトウェアテスト A および D(不明確) *16

それぞれの定義の確認

FizzBuzz ゲームと自然数列上のソートで同値分割の例を確認することでご自身の理解がどの定義に属するかを確認してみてください。

同値分割の例1

FizzBuzz ゲームを解く仕様を扱ってみます。FizzBuzz ゲームを解く仕様は次の入出力の表で表現できます:

入力 出力
... ...
-1 未定義
0 未定義
1 Num 1
2 Num 2
3 Fizz
4 Num 4
5 Buzz
6 Fizz
7 Num 7
... ...
15 FizzBuzz
... ...

▶︎ 仕様の厳密な表現(クリックして開く)

この表は次の述語 fizzbuzz_table を満たす入力 x と出力 y の関係としても表現できます:

fizzbuzz_pre x ≡ x > 0

fizzbuzz_post x y ≡
    (¬x dvd 3 ∧ ¬x dvd 5 ∧ y = Num x) ∨
    (x dvd 3 ∧ ¬x dvd 5 ∧ y = Fizz) ∨
    (¬x dvd 3 ∧ x dvd 5 ∧ y = Buzz) ∨
    (x dvd 3 ∧ x dvd 5 ∧ y = FizzBuzz)

fizzbuzz_table ≡ {(x, y) |x y. fizzbuzz_pre x ∧ fizzbuzz_post x y}

x dvd yxy で割り切れれば真それ以外は偽です。¬NOT (否定)AND(連言)IMP(含意) です。{... |x y. ...}集合の内包的表記です。このように数学的な表現を使えば表のように ... といった推測を必要とする記法を用いずに表現できます。

このとき事前条件 fizzbuzz_pre x が真となるようなすべての x について実装に x を入力して y が出力されたとき、どんな xy でも fizzbuzz_post x y が真であれば実装は仕様を満たしたといえます*17。これがテストで検証すべきことです。

この仕様を前述の基準で同値分割した結果を次表に示します。:

基準グループ 同値パーティション
基準グループA {1, 2, 3, ...}
{..., -2, -1, 0}
基準グループB {..., -1, 1, 2, ...}
{..., -3, 3, 6, ... }
{..., -5, 5, 10, ...}
{..., -15, 0, 15, ...}
基準グループC {..., -2, -1, 0}
{1, 2, 4, ...}
{3, 6, 9, ...}
{5, 10, 20, ...}
{15, 30, 45, ...}
基準グループD {3, 6, 9, ...}
{5, 10, 20, ...}
{15, 30, 45, ...}
{1}
{2}
{4}
...

▶︎ より厳密な表(クリックして開く)

基準グループ 同値パーティション
基準グループA {x. 0 < x}
{x. x ≦ 0}
基準グループB {x. ¬x dvd 3 ∧ ¬x dvd 5}
{x. x dvd 3 ∧ ¬x dvd 5}
{x. ¬x dvd 3 ∧ x dvd 5}
{x. x dvd 3 ∧ x dvd 5}
基準グループC {x. x ≦ 0}
{x. x > 0 ∧ ¬x dvd 3 ∧ ¬x dvd 5}
{x. x > 0 ∧ x dvd 3 ∧ ¬x dvd 5}
{x. x > 0 ∧ ¬x dvd 3 ∧ x dvd 5}
{x. x > 0 ∧ x dvd 3 ∧ x dvd 5}
基準グループD {x. x > 0 ∧ x dvd 3 ∧ ¬x dvd 5}
{x. x > 0 ∧ ¬x dvd 3 ∧ x dvd 5}
{x. x > 0 ∧ x dvd 3 ∧ x dvd 5}
{1} {2} {4} ...

基準グループA: 関数的な仕様の出力を無効な入力と有効な入力に分ける

有効な入力を事前条件を満たすこととすれば {x. x > 0} の有効同値パーティションと {x. x ≦ 0} の無効同値パーティションに分けられそうです。

基準グループB: 想定される実装の制御フローのパスの一致を同値関係とする

FizzBuzz ゲームを解くプログラムは典型的な次のような関数であると想像できます*18

function fizzbuzz(x) {
  return x % 3 === 0
    ? x % 5 === 0
      ? "FizzBuzz"
      : "Fizz"
    : x % 5 === 0
      ? "Buzz"
      : `Num ${i}`;
}

制御フロー上のパスの数は x % 3 === 0 の真偽と x % 5 === 0 の真偽の組み合わせ、すなわち 4 通りです。つまり次のような同値パーティションに分割されます:

  1. {..., -1, 1, 2, ...}
  2. {..., -3, 3, 6, ... }
  3. {..., -5, 5, 10, ...}
  4. {..., -15, 0, 15, ...}

▶︎ 厳密な同値パーティションの表現(クリックして開く)

  1. {x. ¬x dvd 3 ∧ ¬x dvd 5}
  2. {x. x dvd 3 ∧ ¬x dvd 5}
  3. {x. ¬x dvd 3 ∧ x dvd 5}
  4. {x. x dvd 3 ∧ x dvd 5}

基準グループC: 仕様のデータフローグラフのパスの一致を同値関係とする

今回の仕様はデータフローグラフで表現されておらずこの基準グループでの同値分割はできません。そこで次のように等価で実行可能な関数として仕様が定義されていたとします*19

function fizzbuzz_spec(i) {
  if (i <= 0) return null;

  return i % 3 === 0
    ? i % 5 === 0
      ? "FizzBuzz"
      : "Fizz"
    : i % 5 === 0
      ? "Buzz"
      : `Num ${i}`;
}

この仕様のデータフロー上のパスは次の 5 通りの入力で定義できます:

  1. {..., -2, -1, 0}
  2. {1, 2, 4, ...}
  3. {3, 6, 9, ...}
  4. {5, 10, 20, ...}
  5. {15, 30, 45, ...}

▶︎ 厳密な同値パーティションの表現(クリックして開く)

  1. {x. x ≦ 0}
  2. {x. x > 0 ∧ ¬x dvd 3 ∧ ¬x dvd 5}
  3. {x. x > 0 ∧ x dvd 3 ∧ ¬x dvd 5}
  4. {x. x > 0 ∧ ¬x dvd 3 ∧ x dvd 5}
  5. {x. x > 0 ∧ x dvd 3 ∧ x dvd 5}

この 5 つの入力の集合がそれぞれ同値パーティションとなります。

基準グループD: 関数的な仕様の出力の一致を同値関係とする

出力の値の一致による同値関係で定義される同値パーティションは無数にあります:

  1. {3, 6, 9, ...}(Fizz の同値パーティション)
  2. {5, 10, 20, ...}(Buzz の同値パーティション)
  3. {15, 30, 45, ...}(FizzBuzz の同値パーティション)
  4. {1}Num 1 の同値パーティション)
  5. {2}Num 2 の同値パーティション)
  6. {4}Num 4 の同値パーティション)
  7. ... (Num x の同値パーティションが続く)

▶︎ 厳密な同値パーティションの表現(クリックして開く)

  1. {x. x > 0 ∧ x dvd 3 ∧ ¬x dvd 5} (Fizz の同値パーティション)
  2. {x. x > 0 ∧ ¬x dvd 3 ∧ x dvd 5} (Buzz の同値パーティション)
  3. {x. x > 0 ∧ x dvd 3 ∧ x dvd 5}(FizzBuzz の同値パーティション)
  4. {1}Num 1 の同値パーティション)
  5. {2}Num 2 の同値パーティション)
  6. {4}Num 4 の同値パーティション)
  7. ...(x > 0 ∧ ¬x dvd 3 ∧ ¬x dvd 5 を満たす x からなる singleton が続く)

同値分割の例2

次に自然数上のソートを扱ってみましょう。自然数の列をソートするプログラムの仕様は次の入出力の表で表現できます:

入力 出力
[] []
[0] [0]
[1] [1]
[2] [2]
... ...
[0, 0] [0, 0]
[0, 1] [0, 1]
[0, 2] [0, 2]
... ...
[1, 0] [0, 1]
[1, 1] [1, 1]
[1, 2] [1, 2]
[1, 3] [1, 3]
... ...
[2, 0] [0, 2]
[2, 1] [1, 2]
[2, 2] [2, 2]
[2, 3] [2, 3]
... ...

▶︎ 仕様の厳密な表現(クリックして開く)

この表は次の述語 sort_table xs ys を満たす入力 xs と出力 ys の関係としても表現できます。mset はリストから多重集合への関数です。すなわち mset xs = mset ys とは順番は変わってもよいがリストに含まれる要素が変わらないときのみに真となる論理式です。∀i < length ys. 1 ≤ i ⟶ ys ! (i - 1) ≤ ys ! i は列の要素が昇順に並んでいるときのみに真となる論理式です。つまり sort_specysxs を昇順に並び替えたもののときのみ真となりそれ以外は偽となります:

sort_pre xs ≡ True

sort_post xs ys ≡ mset xs = mset ys ∧ (∀i < length ys. 1 ≤ i ⟶ ys ! (i - 1) ≤ ys ! i)

sort_table xs ys ≡ {(x, y) |x y. sort_pre xs ∧ sort_post xs ys}

このとき事前条件 sort_pre xs が真となるようなすべての xs について実装に xs を入力して ys が出力されたとき、どんな xsys でも事後条件 sort_post xs ys が真であれば実装は仕様を満たしたといえます。

この仕様を前述の基準で同値分割した結果を次表に示します:

基準グループ 同値パーティション
基準グループA 自然数上のすべての配列からなる集合
基準グループB プログラムが分割ソートなら
{[], [0], [1], [2], ...}
{[0, 0], [0, 1], [0, 2], ...}
{[1, 0], [2, 0], [2, 1], ... }
...
クイックソートなら
...
基準グループC (仮定しているモデルに合わないので適用外)
基準グループD {[]}
{[0]}
{[1]}
...
{[0, 0]}
{[0, 1]}
{[0, 2]}
...
基準グループA: 関数的な仕様の出力を無効な入力と有効な入力に分ける

事前条件を満たさない自然数の列はありませんのですべて有効な入力です。したがって分割はできずただ1つの同値パーティションがあります。

基準グループB: 想定される実装の制御フローのパスの一致を同値関係とする

自然数上のソートを実現するアルゴリズムは複数ありますからどのアルゴリズムで実装されたプログラムを想定すればいいのか明らかでありません。仮にここでは挿入ソートによる次の単純な実装を想定したとして話を進めましょう:

function sort(arr) {
  loop1: for (let i = 1; i < arr.length; i++) {
    const key = arr[i];
    let j = i - 1;

    loop2: while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j];
      j = j - 1;
    }
    arr[j + 1] = key;
  }
  return arr;
}

このプログラムのフローチャートは次のようになります:

挿入ソートで実装されたプログラムのフローチャート

ここから次の簡略化した制御フローグラフを得ます:

挿入ソートで実装されたプログラムの制御フローグラフ図

入力ごとの制御フロー上のパスは以下のようになります:

入力 パス
[] 1→2→3→6
[0] 1→2→3→6
[1] 1→2→3→6
[2] 1→2→3→6
... 1→2→3→6
[0, 0] 1→2→3→4→5→2→3→6
[0, 1] 1→2→3→4→5→2→3→6
[0, 2] 1→2→3→4→5→2→3→6
... 1→2→3→4→5→2→3→6
[1, 0] 1→2→3→4→5→4→5→2→3→6
[1, 1] 1→2→3→4→5→2→3→6
[1, 2] 1→2→3→4→5→2→3→6
... ...

このパスの一致による同値関係で同値パーティションが定義されます。すなわち同値パーティションは以下のようになります:

  1. {[], [0], [1], [2], ...} (1→2→3→6)
  2. {[0,0], [0, 1], [0, 2], ..., [1, 1], [1, 2], ...} (1→2→3→4→5→2→3→6)
  3. {[1, 0], [2, 1], [2, 0], ...} (1→2→3→4→5→4→5→2→3→6)
  4. ...

もし想定される実装が他のソート(例えばクイックソート)ならば制御フローグラフは別になるので別の同値パーティションに分割されます。

基準グループC: 仕様のデータフローグラフのパスの一致を同値関係とする

この基準グループでは先ほどのソートの仕様のドメインテストを考えることはできなさそうです。なぜならこの定義を採用する文献がドメインテスト(本記事でいう同値分割によるテスト)のモデルとして採用しているのは条件分岐が先頭にあるルーチンだからです*20

ルーチン内では,処理実行の前に入力データを分類し,データの集合ごとに対応したパスに振り分ける。図6.1 にこの概念モデルを示す。処理は「分類」部で始まり,入力データを値に応じて各ケースに分ける。不正入力データ(たとえば,範囲外の値)は,たとえば「reject」という特別のケースにまとめる。入力データは仮想のサブルーチンやパスに流れ,対応する処理を実行する。ドメインテストでは,ルーチン内の計算や処理ではなく,入力データの分類機能に焦点を当てている。

[ 入力 ] --> [ 分類 ] --> O --> [ DO CASE 1 ] --+--> [ 出力 ]
                         |                     |
                         +--> [ DO CASE 2 ] ---+
                         |                     |
                         +--> [ DO CASE 3 ] ---+
                         |                     |
                         +--> [ DO CASE 4 ] ---+
                         |                     |
                         :                     :
                         |                     |
                         +--> [ DO CASE n ] ---+

図6.1 ドメインテストの概念モデル

── ソフトウェアテスト技法 (p.141-142)

今回の例はこのモデルに当てはまる仕様ではありません。したがって今回の例は本書の定義する同値分割の対象とならなさそうです。

基準グループD: 関数的な仕様の出力の一致を同値関係とする

出力の値の一致による同値関係で定義される同値パーティションは無数にあります:

  1. {[]}
  2. {[0]}
  3. {[1]}
  4. ...
  5. {[0, 0]}
  6. {[0, 1], [1, 0]}
  7. ...

ある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいといえる理由

ある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいといえる場合、同値パーティションの代表値のみのテストでその同値パーティション全体をテストしたことにできます。もしそう言えない場合には代表値のみのテストでは欠陥を見逃すことになります。

前述の文献に 入力空間の分割はある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいといえる十分な根拠を説明している文献はありません。 唯一 ソフトウェアテスト技法 のみが条件分岐の条件式の正しさにのみ根拠を与えています(第6章で条件式にありうるバグが定義されそれを見つけるのに十分な入力を議論している)。しかし条件分岐後の処理(e.g. if x >= 0 then A x else B xA x および B x )については根拠が説明されていません。

基準グループA: 関数的な仕様の出力を無効な入力と有効な入力に分ける

根拠が説明されている文献はありません。

例1、例2をみても同値パーティションの代表値の出力が正しければその同値パーティションの他の値も正しいといえるような分割にはなっていないように感じます。たとえばよくある閉包関係バグ(if x > 0if x >= 0 と書いてしまうバグ)が埋め込まれていてももし分岐先の両方が有効同値パーティションだった場合、両方が無効同値パーティションだった場合に気づけません。そのため多くの文献で他の基準グループと混ざった複合的な基準を採用しているのでしょう。

したがって私は基準グループA のみによる同値分割ではある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいとはいえないと結論しました。

基準グループB: 想定される実装の制御フローのパスの一致を同値関係とする

根拠が説明されている文献はありません。

閉包関係バグは見つけられそうですが、現場でよく見かける分岐の実装漏れ(e.g. 18歳未満は別の処理をするよう仕様に書かれていたが実装でこれを忘れて18歳以上と同じ処理にしてしまった)は代表値の取り方の運がよくないと見つけられなさそうです。

したがって私は基準グループB による同値分割ではある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいとはいえないと結論しました。

基準グループC: 仕様のデータフローグラフのパスの一致を同値関係とする

ソフトウェアテスト技法 が部分的に根拠を与えています。ソフトウェアテスト技法は入力を分類しその分類に応じた処理が実行されるモデルを仮定しています(仮定1)。この入力の分類ごとに出力が異なる(仮定2)とすればその出力から分類の正しさを判断できます。入力の分類のほとんどは線形不等式系もしくはそれへと変換できる式であることから線形不等式系を仮定します(仮定3)。このとき例えば 2 次元の線型不等式系で想定されるバグ(境界のズレ、境界の傾き、閉包関係のバグ、余分な境界、境界のヌケ)は 2つの on ポイント、2 つの off ポイントという 4 つの代表値を選べばそのほとんどを極めて高い確率*21で見つけられます。2 次元以外の次元でも同様に説明できます。したがってこれらの代表値の出力が正しければ同じ同値パーティション内の他の値の出力も正しいといえます。

ただしここで正しさが言えるのは、3つの仮定がすべて成り立っている仕様についてしかも入力の分類部分だけです。入力が分類されたあとについて(例えば下図の「DO CASE 1〜N」について)は、ある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいといえる根拠はありません。

[ 入力 ] --> [ 分類 ] --> O --> [ DO CASE 1 ] --+--> [ 出力 ]
                         |                     |
                         +--> [ DO CASE 2 ] ---+
                         |                     |
                         +--> [ DO CASE 3 ] ---+
                         |                     |
                         +--> [ DO CASE 4 ] ---+
                         |                     |
                         :                     :
                         |                     |
                         +--> [ DO CASE n ] ---+

図6.1 ドメインテストの概念モデル

── ソフトウェアテスト技法 (p.141-142)

基準グループD: 関数的な仕様の出力の一致を同値関係とする

Partition Testing Does Not Inspire Confidence, Dick Hamlet, and Ross Taylor (1990) は仕様の出力の一致を同値関係とした場合、仕様の出力の一致による同値関係と実装の出力の一致による同値関係の異なる部分(論文でいう off-diagonal な部分)について狙い撃ちするなら効果があるがそうでなければランダムテスト(本記事でいうモンキーテスト)と変わらないと結論しています。

考察

この記事の論点を再掲します。次に示す 1 の分割基準は不明確で、かつ 2 はなぜそういえるのかがよくわからないということです:

  1. 入力空間を分割する(分割された空間のことを同値パーティションまたは同値クラスまたはドメインという)
  2. ある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいとする

1 についてはソフトウェアテスト技法以外に一般的でかつ明確な同値分割の基準を説明した文献はなかったことから同値分割は明確には定義されていないといえるでしょう。そのため不明確な定義から解釈を重ねて同値分割の基準グループに分類せざるをえませんでした。

2 については特に基準グループ A, B, D ではその根拠が説明されていないといわざるをえません。基準グループ A は 何が有効で何が無効なのかの明確な定義を持っておらず 仮に仕様で有効と無効な値が明示的に指示されたとしても 2 の根拠を説明できません。 基準グループ B と D は明確な定義をもちますが 2 の根拠を説明していません。 基準グループ C のみが明確に定義されかつ 2 について仮定ありで部分的とはいえ根拠を説明しています。つまり基準グループ A, B, D の理解をしていた場合、想像するより信頼性は高くない可能性があります。

基準グループ C についてはソフトウェアテスト技法に詳しい説明があるのでぜひ読んでみてください。

ではなぜ定義も不明確で 2 の説明がなくその信頼性のわからない同値分割を誰もが受け入れ実践しているのでしょうか。そこには何か理由があるように思えます。私はプログラマなのでプログラマでの視点での説明を試みます。

プログラムを書く際、ソフトウェアテスト技法 が定義するモデルのように入力を分類し、分類に応じた処理をおこなうコンポーネントはよくあります(FizzBuzz 関数はその例です)。例えば次の example という関数は cond1 ... condN により値を分類し、doSomething1 ... doSomethingN で処理します。この例は前述の条件にあてはまるコンポーネントです:

function example(x) {
    if (cond1(x)) return doSomething1(x);
    if (cond2(x)) return doSomething2(x);
    ...
    if (condN(x)) return doSomethingN(x);
    throw Error();
}

このときcond1 ... condN については ソフトウェアテスト技法 が同値分割によるテストの信頼性を説明していたのでした。そして境界条件にバグが多く潜むという経験則から察するに cond1 ... condN の誤りが多いのでしょう。それを重視して doSomething1 ... doSomethingN が正しいとはいえないものの cond1 ... condN については正しさがいえるのでやったほうがマシだ、ということなのでしょう。

ただ私が引っ掛かっているのは doSomething1 ... doSomething2 について信頼性の説明がないことです。これを説明するには doSomething1 ... doSomethingN においてそれぞれの前提条件を満たす適当な x の出力が正しいならばそれ以外の x についても出力が正しいことを言う必要があります。例えばある関数的なコンポーネントについて1 つの入力の結果を見ないと出力全体がどうなるかわからないが 1 つの入力の結果がわかると出力全体を推測できることが多いという仮説が考えられます。典型的には doSomething1 ... doSomethingN が定数関数であるときです(e.g. FizzBuzz の例の Fizz、Buzz、FizzBuzz を返す部分)。この場合は確かに 1 つ試せば十分でしょう。他にも doSomething1 ... doSomethingN が1次関数であれば 1 つを試せば十分です。このようにプログラムの意味がいくつかの代表値を試すだけで確定する場合は同値テストが有効であるといえます。ただそういう状況は多くないと思います。実際に例2の自然数列のソートはそうでないわけです。したがって私は doSomething1 ... doSomethingN についてのテストは同値分割だけでは不十分であると考えています。

まとめると今の私の考えは 同値分割はいくつかの仮定を満たすときのみ有効でありその仮定のいずれかでも成り立たないときはさして信頼できないということです。 仮定のいずれかが成り立たない場合はサンプル数の多い Property-based testing や形式検証などの別の手段に頼らないと信頼性を高めることはできないのだと思います。慣れたプログラマーであればシステム内の多くのコンポーネントを Property-based testing で確かめられるようにコンポーネントを設計できるので、同値分割に頼るより Property-based testing に頼ろうかなと思うようになりました。

謝辞

このような複数の文献の定義による比較と議論ができるのは、それぞれの理解を文献として公開してくれた先達がいるからです。またこの記事は複数の SWET の同僚とのディスカッションで得た示唆にもとづいています。どちらについても大変感謝しております。

*1:確認したのは Kindle 版のためウィンドウの大きさによってページ数が変わっているかもしれません。

*2:確認したのは Kindle 版のためウィンドウの大きさによってページ数が変わっているかもしれません。

*3:ここでは入力空間全体から無作為に決定した入力に対して人間が具体的な期待出力を与えるテストとします。具体的な期待出力の代わりに入力と出力の間に成り立つ関係を与えると property-based testing になります。

*4:同値パーティションの境界にバグが多いことは経験的に知られておりこれを発見する境界値テストがモンキーテストより有効なのはいうまでもありません。ここでは境界値ではなく同値分割の代表値のみのテストがモンキーテストに比べて優位といえなくなるということです。言い換えると、境界値のテストをしないなら同値分割でもモンキーテストのどちらでも信頼性に変わりはないということです。

*5:この他にも、事後条件を 加法標準形 へ変換したのちそれぞれの積項を満たす入力から同値パーティションを構成する方法も考えられます。この方法は Wikipedia のモデルベーステストの記事 に引用箇所不明で記述されています。ただこの方法を紹介している文献はありませんでした。

*6:どういう出力が無効とされるのかは未定義でした。多くの文献の例ではエラーメッセージの表示がされるものを無効としているようです。仕様でそのようにエラーメッセージを出力しないといけないことが規定されているため契約による設計の事前条件違反とは異なるようです。

*7:概要が示されているのみで具体的な基準への言及はありません。仕様に基づいた技法とされていることから基準グループA, C, D のいずれかを意図していそうです。

*8:例示があるのみで一般的な基準の言及はありません。概要の説明にによれば「同じ処理」となる入力が同値パーティションを構成するようですが「同じ処理」が何を意図するか明らかでありません。説明文は基準グループA(出力がエラーか否か)の基準に読めますが、例は基準グループC(仕様のデータフローのパスの一致)、基準グループD(出力の一致)の両方の分割と一致します。どれを意図しているのかがよくわかりません。

*9:具体的な基準の記述があります。ただし網羅的ではないように見えます。また不明確な用語が使われています。そのため解釈を1つに絞れませんが、1, 2, 4 は基準グループA、3 は基準グループB に属していると私は解釈しました。

*10:概要が示されているのみで具体的な基準の説明はありません。私は基準グループAに近い印象を抱きました。

*11:第3章に複数の例が提示されるものの一般的な分割基準は説明されません。ただし、第3章冒頭の記述から、テスト対象となるプログラムの制御フロー構造にもとづく分割(基準グループB)を意図している記述が見られます。

*12:一般的な基準の説明がありません。しかし境界値分析の説明にソフトウェアテスト技法を参照していることからソフトウェアテスト技法と同じ基準に基づいていると解釈しました。

*13:一般的な基準の説明がありません。

*14:一般的な基準の説明がありません。1.1 は基準グループ A と D の両方で説明ができますが、1.2 が基準グループAでないと説明できないため A を採用していると解釈しました。

*15:一般的な基準の説明はありません。例題がいずれも有効/無効同値パーティションでの分割と一致することから A と解釈しました。

*16:一般的な基準の一部が説明されています:「同じ入力変数を含む」「プログラム上で同様の結果となる」「同じ出力変数を処理する」「一つもエラーとならないか、全てエラーとなる」。このうち2番目はD、4番目はAに対応するものと解釈できます。1番目と3番目は仮定されている仕様のモデルが不明なため解釈できませんでした。

*17:これを部分正当性といいます。ここでは簡単のため停止性には言及していません。

*18:この実装が x ≦ 0 のときを考慮していないのはバグだ!という主張はあたりません。前述の通り fizzbuzz_pre x が真となるようなすべての x について実装に x を入力して y が出力されその x と y が fizbuzz_post x y を満たすとき実装は仕様を満たしたといえます。そしてこの仕様ではどんな x ≦ 0 でも fizzbuzz_pre x は偽です。つまりこのような x については実装がどんな出力をしても仕様を満たしたといえます。

*19:この仕様は事前条件が満たされないとき null を返し、そうでなければ期待するただ1つを返す関数として表現しています。このように非決定性を許さない関数的な仕様表現での部分正当性は ∀x y. fizzbuzz_spec x = y ∧ y ≠ null ⟶ fizzbuzz_impl x = y と表せます。

*20:知識ゼロから学ぶ ソフトウェアテストではモデルの仮定は直接的に言及していません。ただしソフトウェアテスト技法を参考文献としており、かつモデルの仮定と整合する記載があるため同様の仮定をもつと推測しました。

*21:不幸にもバグにより境界が90°回転して 2 つの off ポイントを通過するとバグを見逃します(ソフトウェアテスト技法 (p.161))。