関数の正しさを調べる——単体テスト・境界値・不変条件

小さな整数関数を題材に、仕様から期待値を決める単体テスト、境界値の選択、不変条件による論証、プロパティテストによる広い入力の検査を使い分ける。

この単元の目標
  • 入力の前提条件と出力の事後条件を分け、仕様から期待値を決められる。
  • 同値分割と境界値分析を用いて、理由のあるテストケースを選べる。
  • ループ不変条件の初期化・維持・終了を説明できる。
  • 具体例のテストとプロパティテストの違い、および合格から結論できる範囲を説明できる。
📑 目次
  1. 1.先に「何を正しいと呼ぶか」を決める
  2. 2.単体テストは準備・実行・確認に分ける
  3. 3.同値分割と境界値——数を増やす前に選び方を考える
  4. 4.不変条件は「途中で何が保たれるか」
  5. 5.プロパティテスト——個々の答えだけでなく関係を調べる
  6. 6.失敗を再現し、合格の意味を限定する

1.先に「何を正しいと呼ぶか」を決める

単体テストは、関数などの小さな単位を取り出し、入力に対する結果が仕様どおりかを調べる方法です。ここでは外部通信もファイル操作もない、自作の整数関数だけを扱います。実装の結果をそのまま期待値に写すのではなく、問題文・表・独立した計算から期待値を求めます。この正否を判定する基準をテストオラクルと呼びます。

仕様の部分この教材の clamp10(x)
前提条件x は −100 以上100以下の整数。単位のない値とする
事後条件x < 0 なら0、0 ≤ x ≤ 10 なら x、x > 10 なら10を返す
副作用外部状態を変更せず、同じ入力なら同じ出力を返す
対象外小数・文字列・NaN・無限大・範囲外の整数の振る舞いはこの仕様では定めない
c(x)={0(x<0)x(0≤x≤10)10(x>10)x∈{−100,−99,…,100}c(x)=\begin{cases}0 & (x<0)\\x & (0\le x\le 10)\\10 & (x>10)\end{cases}\qquad x\in\{-100,-99,\ldots,100\}

正常な JavaScript 実装の小例は function clamp10(x) { return Math.min(10, Math.max(0, x)); } です。Math.max(0, x) が下限0を、Math.min(10, …) が上限10を適用します。この関数自体は前提条件を検査しません。入力の受け付け方まで設計するなら、対象外の入力を拒否するかなども別の仕様として定めます。以下の整数計算は小さい範囲なので、JavaScript の number で厳密に扱えます。

2.単体テストは準備・実行・確認に分ける

  1. 準備(Arrange):入力 x = 7 と、仕様から求めた期待値7を用意する。
  2. 実行(Act):clamp10(7) を1回呼び、結果 actual に保存する。
  3. 確認(Assert):actual === 7 を確かめる。自動テストでは不一致を失敗として報告する。

Node.js の標準機能なら import assert from "node:assert/strict"; と読み込み、assert.equal(clamp10(7), 7); と書けます。この例では整数の一致を確認するので許容誤差は不要です。「関数を呼んだが、何も比較していない」だけでは出力の正しさを調べたことになりません。また、期待値にも Math.min(10, Math.max(0, x)) をコピーすると、仕様の読み違いまで共有するおそれがあります。

3.同値分割と境界値——数を増やす前に選び方を考える

入力を、仕様上同じ規則が適用されるまとまりに分けるのが同値分割です。clamp10 なら「0未満」「0以上10以下」「10超」の3群です。これは各群で実装が必ず同じように正しく動くという保証ではなく、代表例を選ぶための仮説です。境界値分析では規則が切り替わる値とその近傍を調べます。整数なので0の直前・直後は −1 と1、10なら9と11です。実数には数学的な意味で「直後の実数」はないため、同じ選び方を無条件には移せません。

例題1:境界テストの入力と期待値

clamp10 の境界0・10について、それぞれ直前・境界上・直後の入力を選び、期待値を列挙せよ。さらに入力範囲の両端も調べよ。

解答・解説

入力 −1, 0, 1, 9, 10, 11 に対する期待値は順に 0, 0, 1, 9, 10, 10。両端 −100 と100では0と10なので、重複なしで計8ケースです。たとえば11は上限10を超えるため10、9は範囲内なので9となります。8ケースは境界付近と両端を押さえますが、入力7などの内部値を含む全201入力を網羅したわけではありません。

分岐の境界を通ったことと、誤りを必ず見つけることは別です。境界上では両側の式が同じ値になる仕様もあります。行や分岐のカバレッジは「通った場所」の指標で、期待値が妥当か、必要な入力を選べたかまでは保証しません。

4.不変条件は「途中で何が保たれるか」

次に、配列 a の合計を返す sum(a) を考えます。配列は長さ0〜100、各要素は −100〜100 の整数で、欠けた要素のない通常の配列とします。正常な実装は function sum(a) { let s = 0; for (let i = 0; i < a.length; i++) { s += a[i]; } return s; } です。入力は変更しません。空配列の合計は0と定めます。各途中和の絶対値は最大10000で、ここでも丸め誤差はありません。

0≤i≤n,s=∑k=0i−1ak,n=length⁡(a)0\le i\le n,\qquad s=\sum_{k=0}^{i-1}a_k,\qquad n=\operatorname{length}(a)

この式が、ループ条件を判定する直前に成立する不変条件です。「s はこれから読む要素を含めず、すでに読んだ先頭 i 個の合計である」と読みます。i = 0 のとき右辺は空の和で0です。不変条件は出力が一定という意味ではありません。i と s は変わっても、その関係が保たれるという意味です。

  1. 初期化:最初は i = 0、s = 0。先頭0個の合計は0なので不変条件が成立する。
  2. 維持:i < n で要素 a[i] を加え、その後 i を1増やす。s は先頭 i + 1 個の合計となり、次の判定時にも同じ関係が成立する。
  3. 終了:条件 i < n が偽になり、不変条件の i ≤ n と合わせて i = n。したがって s は全要素の合計である。
  4. 停止性:反復ごとに非負整数 n − i が1ずつ減るので、有限回で終了する。不変条件だけでは停止までは示せない。
例題2:途中和を追跡する

a = [3, −2, 4] に sum を適用する。各ループ条件判定時の (i, s) と、終了時の値を求めよ。

解答・解説

(0, 0) → (1, 3) → (2, 1) → (3, 5)。2個を読んだ時点では 3 + (−2) = 1、終了時は 1 + 4 = 5 です。i = 2 のとき、まだ a[2] = 4 を加えていない点が重要です。空配列なら最初から i = n = 0 なので反復せず0を返します。これは最初から終了する境界ケースの確認にもなります。

5.プロパティテスト——個々の答えだけでなく関係を調べる

例示ベースのテストは「入力7なら答え7」のように具体的な期待値を置きます。プロパティテストは「指定した入力領域のどの値にも成り立つ性質」を書き、多数の入力で検査します。ランダム生成を使う場合もあれば、小さい領域を列挙する場合もあります。生成器が前提条件を満たすこと、境界や空配列にも届くことが重要です。

clamp10 の性質調べる内容成り立つ理由
範囲0 ≤ c(x) ≤ 10下限と上限で制限される
範囲内の値を保存0 ≤ x ≤ 10 なら c(x) = x事後条件の中央の規則
冪等性(べきとうせい)c(c(x)) = c(x)1回の出力はすでに0〜10の範囲内
単調性x ≤ y なら c(x) ≤ c(y)下限・上限で制限しても大小は逆転しない
例題3:満たす性質があっても仕様全体とは限らない

clamp10 について範囲と冪等性だけを検査した。すべての入力に5を返す別の関数 g は、この2性質を満たすか。clamp10 と同じ仕様を満たすか。

解答・解説

0 ≤ 5 ≤ 10 なので範囲を満たし、g(g(x)) = g(5) = 5 = g(x) なので冪等性も満たします。しかし入力7の期待値は7なのに g(7) = 5 なので、clamp10 の仕様は満たしません。性質が弱いと別の関数も合格します。具体的な期待値や、x < 0 なら0、x > 10 なら10という仕様そのものの検査も組み合わせます。

sum には「a と b を連結した配列の合計は sum(a) + sum(b) と等しい」という性質があります。ただし a と b の長さの合計も100以下とし、各要素の範囲も守ります。たとえば a = [3, −2]、b = [4] なら、左辺は5、右辺は1 + 4 = 5です。入力を変換した前後の結果を比較する方法をメタモルフィックテストと呼び、この性質もその一例です。これだけでは常に0を返す関数を区別できないので、sum([4]) = 4 などの既知の例も必要です。

6.失敗を再現し、合格の意味を限定する

  • 生成テストで失敗したら、入力・期待する性質・実際の結果を保存する。乱数のシードに加え、生成器の版や設定も残すと再現しやすい。
  • シュリンクは、失敗を保ったまま入力を小さく・単純にする探索。前提条件を保つ必要があり、必ず唯一の最小例になるとは限らない。
  • 原因を修正したら、その具体例を回帰テストに加える。回帰テストは以前確認した振る舞いが変更で崩れないかを調べる。
  • clamp10 の指定領域は 100 − (−100) + 1 = 201 個。固定された決定的実装について全入力で仕様との一致を確かめれば、この領域の出力は網羅できる。ただし領域外や外部状態のある別の関数までは保証しない。
ランダムな1000ケースが合格しても、一般には全入力での正しさの証明ではありません。同じ入力が重複することもあります。論証は前提の下で一般性を与え、テストは実行された実装の具体的な振る舞いを確かめます。両者を補完的に使い、単体テストとは別に部品同士の接続を確かめる結合テストも考えます。
練習問題——期待値と理由を言葉にする
  1. clamp10 の入力を −5, 0, 6, 10, 15 としたときの期待値と、適用される規則を答えよ。

    解答・解説

    順に0, 0, 6, 10, 10。−5 は0未満なので下限0、0・6・10は閉区間0〜10の中なので元の値、15は10超なので上限10を返す。0と10が範囲内に含まれる点にも注意する。

  2. sum([−4, 4, 2]) で、条件判定時の i = 2 のときの s と、返り値を求めよ。

    解答・解説

    i = 2 では先頭2個を加えた s = −4 + 4 = 0。次に2を加えるので返り値は2。i は「すでに処理した個数」であり、i = 2 の判定時には3個目をまだ加えていない。

  3. sum の連結の性質を、長さ60の配列 a と長さ50の配列 b で検査してよいか。この教材の前提条件から答えよ。

    解答・解説

    この仕様のままでは不可。a と b 自体は長さ100以下でも、連結後は110となり、長さ上限100を超える。合計長100以下の組を生成するか、入力仕様と正確な計算範囲を見直す必要がある。

  4. clamp10 について「出力は0以上10以下」を全201入力で確認した。この結果だけで仕様との完全な一致を主張できるか。

    解答・解説

    できない。全入力を通しても検査しているのは範囲だけで、常に5を返す関数も合格する。網羅する入力の範囲だけでなく、判定する性質の十分さも必要である。仕様から求めた期待値との一致を各入力で調べるなら、この有限領域の出力の一致を確認できる。

確認クイズ

問1. 整数入力の境界10について、直前・境界上・直後を調べる組はどれか。

問2. sum の条件判定時の不変条件として正しいものはどれか。

問3. c(c(x)) = c(x) という性質の名前はどれか。

問4. 生成テストで見つけた失敗への対応として最も適切なのはどれか。

根拠と適用範囲:例の関数・配列・テストケースはこの教材用の自作です。期待値は明記した区分的な仕様と整数の加算から計算し、不変条件は初期化・維持・終了で論証しました。統計的な故障率や製品の品質を示す実測データではありません。JavaScript の小例は前提を満たす入力用であり、汎用ライブラリの完全な入力検証を実装するものではありません。
📝 メモこの端末のみに保存
メモ欄はページ読み込み後に使えるようになります。